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