001/*
002 * File   : $Source: /alkacon/cvs/opencms-ocee/org.opencms.ocee.vfsdoctor/src/org/opencms/ocee/vfsdoctor/CmsVfsDoctorSqlConsole.java,v $
003 * Date   : $Date: 2010/09/06 13:38:58 $
004 * Version: $Revision: 1.21 $
005 *
006 * Copyright (c) 2005 Alkacon Software GmbH (http://www.alkacon.com)
007 * All rights reserved.
008 *
009 * This source code is the intellectual property of Alkacon Software GmbH.
010 * It is PROPRIETARY and CONFIDENTIAL.
011 * Use of this source code is subject to license terms.
012 *
013 * In order to use this source code, you need written permission from
014 * Alkacon Software GmbH. Redistribution of this source code, in modified
015 * or unmodified form, is not allowed unless written permission by
016 * Alkacon Software GmbH has been given.
017 *
018 * ALKACON SOFTWARE GMBH MAKES NO REPRESENTATIONS OR WARRANTIES ABOUT THE SUITABILITY
019 * OF THIS SOURCE CODE, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
020 * TO THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
021 * PURPOSE, OR NON-INFRINGEMENT. ALKACON SOFTWARE GMBH SHALL NOT BE LIABLE FOR ANY
022 * DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR DISTRIBUTING
023 * THIS SOURCE CODE OR ITS DERIVATIVES.
024 *
025 * For further information about Alkacon Software GmbH, please see the
026 * company website: http://www.alkacon.com
027 */
028
029package org.opencms.ui.apps.dbmanager.sqlconsole;
030
031import org.opencms.db.I_CmsVfsDriver;
032import org.opencms.db.generic.CmsSqlManager;
033import org.opencms.i18n.CmsMessageContainer;
034import org.opencms.main.CmsLog;
035import org.opencms.report.I_CmsReport;
036import org.opencms.security.CmsSecurityException;
037import org.opencms.ui.apps.Messages;
038
039import java.sql.Connection;
040import java.sql.PreparedStatement;
041import java.sql.ResultSet;
042import java.sql.ResultSetMetaData;
043import java.sql.SQLException;
044import java.util.ArrayList;
045import java.util.Arrays;
046import java.util.List;
047import java.util.stream.Collectors;
048
049import org.apache.commons.logging.Log;
050
051/**
052 * Manages the SQL console.<p>
053 *
054 * @author Michael Moossen
055 *
056 * @version $Revision: 1.21 $
057 *
058 * @since 6.0.0
059 */
060public final class CmsSqlConsoleExecutor {
061
062    /** The log object for this class. */
063    private static final Log LOG = CmsLog.getLog(CmsSqlConsoleExecutor.class);
064
065    /** SQL manager with specialized queries. */
066    private CmsSqlManager m_sqlManager;
067
068    /**
069     * Default Constructor.<p>
070     */
071    public CmsSqlConsoleExecutor() {
072
073        m_sqlManager = new CmsSqlManager();
074        m_sqlManager.init(I_CmsVfsDriver.DRIVER_TYPE_ID, "default");
075    }
076
077    /**
078     * Main method of this class. Executes the given sql query.<p>
079     *
080     * This method also checks the permissions for executing sql sentences,
081     * according to the <code>vfs-doctor.xml</code> configuration file.<p>
082     *
083     * @param sql the sql query to execute
084     * @param pool name of the db pool to use
085     * @param report the report to write the output
086     * @param errors a list to append errors to
087     *
088     * @return a <code>{@link List}</code> if the sql is a SELECT sentence, or <code>null</code>.
089     */
090    public CmsSqlConsoleResults execute(String sql, String pool, I_CmsReport report, List<Throwable> errors) {
091
092        try {
093            CmsMessageContainer message = Messages.get().container(Messages.RPT_SQLCONSOLE_BEGIN_0);
094            report.println(message, I_CmsReport.FORMAT_HEADLINE);
095            if (LOG.isInfoEnabled()) {
096                LOG.info(message.key());
097            }
098
099            List<String> sentences = normalize(sql);
100            if (sentences.size() < 1) {
101                write(
102                    report,
103                    Messages.get().getBundle(report.getLocale()).key(Messages.ERR_SQLCONSOLE_NOTHING_TO_EXECUTE_0));
104            } else {
105                for (String sentence : sentences) {
106                    if (!checkPermissions(sentence)) {
107                        writeError(
108                            report,
109                            new CmsSecurityException(
110                                Messages.get().container(
111                                    Messages.ERR_SQLCONSOLE_NO_PERMISSIONS_EXEC_SENTENCE_1,
112                                    sentence)));
113                    } else {
114                        if (!sentence.toUpperCase().startsWith("SELECT")
115                            && !sentence.toUpperCase().startsWith("SHOW")) {
116                            int res = executeUpdate(sentence, pool);
117                            message = Messages.get().container(
118                                Messages.RPT_SQLCONSOLE_ROWS_AFFECTED_1,
119                                Integer.valueOf(res));
120                            report.println(message);
121                            if (LOG.isInfoEnabled()) {
122                                LOG.info(message);
123                            }
124                        } else {
125                            CmsSqlConsoleResults res = executeQuery(sentence, pool);
126                            // writeTable(report, res);
127                            message = Messages.get().container(
128                                Messages.RPT_SQLCONSOLE_NUM_ROWS_RETRIEVED_1,
129                                Integer.valueOf(res.getData().size()));
130                            report.println(message);
131                            if (LOG.isInfoEnabled()) {
132                                LOG.info(message);
133                            }
134                            return res;
135                        }
136                    }
137                }
138            }
139        } catch (Throwable e) {
140            errors.add(e);
141        } finally {
142            CmsMessageContainer message = Messages.get().container(Messages.RPT_SQLCONSOLE_END_0);
143            report.println(message, I_CmsReport.FORMAT_HEADLINE);
144            if (LOG.isInfoEnabled()) {
145                LOG.info(message.key());
146            }
147        }
148        return null;
149    }
150
151    /**
152     * Checks the permission to be executed for a single sql sentence.<p>
153     * @param sentence the sentence to check for permission
154     *
155     * @return <code>true</code> if the sentence is allowed to be executed
156     */
157    private boolean checkPermissions(String sentence) {
158
159        sentence = sentence.toUpperCase();
160
161        // protected user from the case where they accidentally left off the condition in DELETE statements
162        // (you can still use dummy conditions like 'WHERE 1=1' if you really want to delete everything)
163        if (sentence.contains("DELETE FROM") && !sentence.contains("WHERE")) {
164            return false;
165        }
166        return true;
167    }
168
169    /**
170     * Executes a single <code>SELECT</code> sql sentence.<p>
171     *
172     * @param sentence the sentence to execute
173     * @param poolName the name of the pool to use
174     *
175     * @return the list of rows returned by the rdbms
176     *
177     * @throws SQLException in the case of a error
178     */
179    @SuppressWarnings("resource")
180    private CmsSqlConsoleResults executeQuery(String sentence, String poolName) throws SQLException {
181
182        Connection conn = null;
183        PreparedStatement stmt = null;
184        ResultSet res = null;
185
186        CmsSqlManager sqlManager = m_sqlManager;
187
188        try {
189            conn = sqlManager.getConnection(poolName);
190            stmt = sqlManager.getPreparedStatementForSql(conn, sentence);
191            res = stmt.executeQuery();
192
193            // add headings
194            ResultSetMetaData metadata = res.getMetaData();
195            List<String> heading = new ArrayList<>();
196            for (int i = 0; i < metadata.getColumnCount(); i++) {
197                heading.add(metadata.getColumnName(i + 1));
198            }
199            List<List<Object>> data = new ArrayList<List<Object>>();
200
201            // add contents
202            while (res.next()) {
203                List<Object> row = new ArrayList<Object>();
204                for (int i = 0; i < metadata.getColumnCount(); i++) {
205                    Object value = res.getObject(i + 1);
206                    if ((value instanceof String)
207                        || (value instanceof Integer)
208                        || (value instanceof Long)
209                        || (value instanceof Float)
210                        || (value instanceof Double)) {
211                        row.add(value);
212                    } else if (value == null) {
213                        row.add(null);
214                    } else {
215                        row.add(String.valueOf(value));
216                    }
217                }
218                data.add(row);
219            }
220            return new CmsSqlConsoleResults(heading, data);
221        } finally {
222            sqlManager.closeAll(null, conn, stmt, res);
223        }
224    }
225
226    /**
227     * Executes a single sql sentence.<p>
228     *
229     * For <code>SELECT</code> sentences use <code>{@link #executeQuery(String, String)}</code>.<p>
230     *
231     * @param sentence the sentence to execute
232     * @param poolName the name of the pool to use
233     *
234     * @return the number of affected rows
235     *
236     * @throws SQLException if there is an error
237     */
238    private int executeUpdate(String sentence, String poolName) throws SQLException {
239
240        Connection conn = null;
241        PreparedStatement stmt = null;
242        int ret = 0;
243
244        CmsSqlManager sqlManager = m_sqlManager;
245        try {
246            conn = sqlManager.getConnection(poolName);
247            stmt = sqlManager.getPreparedStatementForSql(conn, sentence);
248            ret = stmt.executeUpdate();
249        } finally {
250            sqlManager.closeAll(null, conn, stmt, null);
251        }
252        return ret;
253    }
254
255    /**
256     * Returns a list of single sql sentences,
257     * removing all special chars like tabs or eol's.<p>
258     *
259     * @param sql the sql query
260     *
261     * @return a list of sql sentences
262     */
263    private List<String> normalize(String sql) {
264
265        String normSql = sql.replaceAll(";", "; ");
266        normSql = normSql.replaceAll("\\s+", " ");
267        return Arrays.stream(normSql.split(";")).map(s -> s.trim()).collect(Collectors.toList());
268    }
269
270    /**
271     * Writes the given string to the report,
272     * and, if enabled, to the opencms log file.<p>
273     *
274     * @param report the report
275     * @param string the string
276     */
277    private void write(I_CmsReport report, String string) {
278
279        report.println(
280            org.opencms.report.Messages.get().container(
281                org.opencms.report.Messages.RPT_ARGUMENT_1,
282                string.replaceAll("'", "\\\\'")));
283        if (LOG.isInfoEnabled()) {
284            LOG.info("SQLConsole:" + string);
285        }
286    }
287
288    /**
289     * Writes the given error to the report,
290     * and, if enabled, to the opencms log file.<p>
291     *
292     * @param report the report to write to
293     * @param e the exception
294     */
295    private void writeError(I_CmsReport report, Throwable e) {
296
297        report.println(e);
298        if (LOG.isWarnEnabled()) {
299            LOG.warn("SQLConsole", e);
300        }
301    }
302}