001/*
002 * (C) Copyright 2006-2016 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Florent Guillaume
018 */
019package org.nuxeo.ecm.core.storage.sql.jdbc;
020
021import java.io.Serializable;
022import java.sql.ResultSet;
023import java.sql.SQLException;
024import java.util.ArrayList;
025import java.util.Calendar;
026import java.util.Collection;
027import java.util.Collections;
028import java.util.LinkedList;
029import java.util.List;
030import java.util.Map;
031import java.util.Set;
032import java.util.stream.Collectors;
033
034import org.apache.commons.logging.Log;
035import org.apache.commons.logging.LogFactory;
036import org.nuxeo.common.utils.StringUtils;
037import org.nuxeo.ecm.core.api.model.Delta;
038import org.nuxeo.ecm.core.storage.sql.Model;
039import org.nuxeo.ecm.core.storage.sql.Row;
040import org.nuxeo.ecm.core.storage.sql.jdbc.db.Column;
041
042/**
043 * Logger used for debugging.
044 */
045public class JDBCLogger {
046
047    public static final Log log = LogFactory.getLog(JDBCLogger.class);
048
049    public static final int DEBUG_MAX_STRING = 100;
050
051    public static final int DEBUG_MAX_ARRAY = 10;
052
053    public final String instance;
054
055    public JDBCLogger(String instance) {
056        this.instance = instance;
057    }
058
059    public boolean isLogEnabled() {
060        return log.isTraceEnabled();
061    }
062
063    public String formatMessage(String message) {
064        return "(" + instance + ") SQL: " + message;
065    }
066
067    public void error(String message) {
068        log.error(formatMessage(message));
069    }
070
071    public void error(String message, Throwable t) {
072        log.error(formatMessage(message), t);
073    }
074
075    public void warn(String message) {
076        log.warn(formatMessage(message));
077    }
078
079    public void info(String message) {
080        log.info(formatMessage(message));
081    }
082
083    public void log(String message) {
084        log.trace(formatMessage(message));
085    }
086
087    public void logCount(int count) {
088        if (count > 0 && isLogEnabled()) {
089            log("  -> " + count + " row" + (count > 1 ? "s" : ""));
090        }
091    }
092
093    public void logResultSet(ResultSet rs, List<Column> columns) throws SQLException {
094        List<String> res = new LinkedList<>();
095        int i = 0;
096        for (Column column : columns) {
097            i++;
098            Serializable v = column.getFromResultSet(rs, i);
099            res.add(column.getKey() + "=" + loggedValue(v));
100        }
101        log("  -> " + String.join(", ", res));
102    }
103
104    public void logMap(Map<String, Serializable> map) throws SQLException {
105        String result = map.entrySet()
106                           .stream()
107                           .map(entry -> entry.getKey() + "=" + loggedValue(entry.getValue()))
108                           .collect(Collectors.joining(", "));
109        log("  -> " + result);
110    }
111
112    public void logIds(List<Serializable> ids, boolean countTotal, long totalSize) {
113        List<Serializable> debugIds = ids;
114        String end = "";
115        if (ids.size() > DEBUG_MAX_ARRAY) {
116            debugIds = new ArrayList<>(DEBUG_MAX_ARRAY);
117            int i = 0;
118            for (Serializable id : ids) {
119                debugIds.add(id);
120                i++;
121                if (i == DEBUG_MAX_ARRAY) {
122                    break;
123                }
124            }
125            end = "...(" + ids.size() + " ids)...";
126        }
127        if (countTotal) {
128            end += " (total " + totalSize + ')';
129        }
130        log("  -> " + debugIds + end);
131    }
132
133    public void logSQL(String sql, List<Column> columns, Row row) {
134        logSQL(sql, columns, row, Collections.emptyList(), Collections.emptyMap());
135    }
136
137    public void logSQL(String sql, List<Column> columns, Row row, List<Column> whereColumns,
138            Map<String, Serializable> conditions) {
139        List<Serializable> values = new ArrayList<>();
140        for (Column column : columns) {
141            String key = column.getKey();
142            Serializable value = row.get(key);
143            if (value instanceof Delta) {
144                Delta delta = (Delta) value;
145                if (delta.getBase() != null) {
146                    value = delta.getDeltaValue();
147                }
148            }
149            values.add(value);
150        }
151        for (Column column : whereColumns) {
152            String key = column.getKey();
153            Serializable value;
154            if (column.getKey().equals(Model.MAIN_KEY)) {
155                value = row.get(key);
156            } else {
157                value = conditions.get(key);
158            }
159            values.add(value);
160        }
161        logSQL(sql, values);
162    }
163
164    // callable statement with one return value
165    private static final String CALLABLE_START = "{?=";
166
167    public void logSQL(String sql, Collection<Serializable> values) {
168        StringBuilder buf = new StringBuilder();
169        int start = 0;
170        if (sql.startsWith(CALLABLE_START)) {
171            buf.append(CALLABLE_START);
172            start = CALLABLE_START.length();
173        }
174        for (Serializable v : values) {
175            int index = sql.indexOf('?', start);
176            if (index == -1) {
177                // mismatch between number of ? and number of values
178                break;
179            }
180            buf.append(sql, start, index);
181            buf.append(loggedValue(v));
182            start = index + 1;
183        }
184        buf.append(sql, start, sql.length());
185        log(buf.toString());
186    }
187
188    /**
189     * Returns a loggable value using pseudo-SQL syntax.
190     */
191    @SuppressWarnings("boxing")
192    public static String loggedValue(Object value) {
193        if (value == null) {
194            return "NULL";
195        }
196        if (value instanceof String) {
197            String v = (String) value;
198            if (v.length() > DEBUG_MAX_STRING) {
199                v = v.substring(0, DEBUG_MAX_STRING) + "...(" + v.length() + " chars)...";
200            }
201            return "'" + v.replace("'", "''") + "'";
202        }
203        if (value instanceof Calendar) {
204            Calendar cal = (Calendar) value;
205            char sign;
206            int offset = cal.getTimeZone().getOffset(cal.getTimeInMillis()) / 60000;
207            if (offset < 0) {
208                offset = -offset;
209                sign = '-';
210            } else {
211                sign = '+';
212            }
213            return String.format("TIMESTAMP '%04d-%02d-%02dT%02d:%02d:%02d.%03d%c%02d:%02d'", cal.get(Calendar.YEAR), //
214                    cal.get(Calendar.MONTH) + 1, //
215                    cal.get(Calendar.DAY_OF_MONTH), //
216                    cal.get(Calendar.HOUR_OF_DAY), //
217                    cal.get(Calendar.MINUTE), //
218                    cal.get(Calendar.SECOND), //
219                    cal.get(Calendar.MILLISECOND), //
220                    sign, offset / 60, offset % 60);
221        }
222        if (value instanceof java.sql.Date) {
223            return "DATE '" + value.toString() + "'";
224        }
225        if (value instanceof Object[]) {
226            Object[] v = (Object[]) value;
227            StringBuilder b = new StringBuilder();
228            b.append('[');
229            for (int i = 0; i < v.length; i++) {
230                if (i > 0) {
231                    b.append(',');
232                    if (i > DEBUG_MAX_ARRAY) {
233                        b.append("...(").append(v.length).append(" items)...");
234                        break;
235                    }
236                }
237                b.append(loggedValue(v[i]));
238            }
239            b.append(']');
240            return b.toString();
241        }
242        return value.toString();
243    }
244}