001/*
002 * (C) Copyright 2006-2011 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;
020
021import java.io.Serializable;
022import java.util.ArrayList;
023import java.util.Calendar;
024import java.util.List;
025import java.util.Map;
026import java.util.Map.Entry;
027
028import org.nuxeo.ecm.core.api.model.Delta;
029
030/**
031 * The data of a single row in a table (keys/values form a map), or of multiple rows with the same id (values is an
032 * array of Serializable).
033 * <p>
034 * The id of the row is distinguished internally from other columns. For fragments corresponding to created data, the
035 * initial id is a temporary one, and it will be changed after database insert.
036 */
037public final class Row extends RowId implements Serializable, Cloneable {
038
039    private static final long serialVersionUID = 1L;
040
041    private static final int DEFAULT = 5;
042
043    private enum OpaqueValue {
044        OPAQUE_VALUE
045    }
046
047    /**
048     * A database value we don't care about reading. When present in a fragment, it won't be written, but any other
049     * value will be.
050     */
051    public static final Serializable OPAQUE = OpaqueValue.OPAQUE_VALUE;
052
053    /**
054     * The row keys, for single row.
055     */
056    protected String[] keys;
057
058    /**
059     * The row values.
060     */
061    public Serializable[] values;
062
063    /**
064     * The size of the allocated part of {@link #values}, for single rows.
065     */
066    protected int size;
067
068    /** Copy constructor. */
069    private Row(Row row) {
070        super(row);
071        keys = row.keys == null ? null : row.keys.clone();
072        values = row.values == null ? null : row.values.clone();
073        size = row.size;
074    }
075
076    @Override
077    public Row clone() {
078        return new Row(this);
079    }
080
081    /**
082     * Constructs an empty {@link Row} for the given table with the given id (may be {@code null}).
083     */
084    public Row(String tableName, Serializable id) {
085        super(tableName, id);
086        keys = new String[DEFAULT];
087        values = new Serializable[DEFAULT];
088        // size = 0;
089    }
090
091    /**
092     * Constructs a new {@link Row} from a map.
093     *
094     * @param map the initial data to use
095     */
096    public Row(String tableName, Map<String, Serializable> map) {
097        super(tableName, null); // id set through map
098        keys = new String[map.size()];
099        values = new Serializable[map.size()];
100        // size = 0;
101        for (Entry<String, Serializable> entry : map.entrySet()) {
102            putNew(entry.getKey(), entry.getValue());
103        }
104    }
105
106    /**
107     * Constructs a new {@link Row} from an array of values.
108     *
109     * @param array the initial data to use
110     */
111    public Row(String tableName, Serializable id, Serializable[] array) {
112        super(tableName, id);
113        values = array.clone();
114        keys = null;
115        size = -1;
116    }
117
118    public boolean isCollection() {
119        return size == -1;
120    }
121
122    private void ensureCapacity(int minCapacity) {
123        if (minCapacity > values.length) {
124            Serializable[] k = keys;
125            Serializable[] d = values;
126            int newCapacity = (values.length * 3) / 2 + 1;
127            if (newCapacity < minCapacity) {
128                newCapacity = minCapacity;
129            }
130            keys = new String[newCapacity];
131            values = new Serializable[newCapacity];
132            System.arraycopy(d, 0, values, 0, size);
133            System.arraycopy(k, 0, keys, 0, size);
134        }
135    }
136
137    /**
138     * Puts a key/value.
139     *
140     * @param key the key
141     * @param value the value
142     */
143    public void put(String key, Serializable value) {
144        if (key.equals(Model.MAIN_KEY)) {
145            id = value;
146            return;
147        }
148        // linear search but the array is small
149        for (int i = 0; i < size; i++) {
150            if (key.equals(keys[i])) {
151                Serializable oldValue = values[i];
152                if (oldValue instanceof Delta) {
153                    Delta oldDelta = (Delta) oldValue;
154                    if (value instanceof Delta) {
155                        if (value != oldDelta) {
156                            // add a delta to another delta
157                            value = oldDelta.add((Delta) value);
158                        }
159                    } else if (oldDelta.getFullValue().equals(value)) {
160                        // don't overwrite a delta with the full value
161                        // that actually comes from it
162                        return;
163                    }
164                }
165                values[i] = value;
166                return;
167            }
168        }
169        ensureCapacity(size + 1);
170        keys[size] = key == null ? null: key.intern();
171        values[size++] = value;
172    }
173
174    /**
175     * Puts a key/value, assuming the key is not already there.
176     *
177     * @param key the key
178     * @param value the value
179     */
180    public void putNew(String key, Serializable value) {
181        if (key.equals(Model.MAIN_KEY)) {
182            id = value;
183            return;
184        }
185        ensureCapacity(size + 1);
186        keys[size] = key == null ? null : key.intern();
187        values[size++] = value;
188    }
189
190    /**
191     * Gets a value from a key.
192     *
193     * @param key the key
194     * @return the value
195     */
196    public Serializable get(String key) {
197        if (key.equals(Model.MAIN_KEY)) {
198            return id;
199        }
200        // linear search but the array is small
201        for (int i = 0; i < size; i++) {
202            if (key.equals(keys[i])) {
203                return values[i];
204            }
205        }
206        return null;
207    }
208
209    /**
210     * Gets the list of keys. The id is not included.
211     */
212    public List<String> getKeys() {
213        List<String> list = new ArrayList<String>(size);
214        for (int i = 0; i < size; i++) {
215            list.add(keys[i]);
216        }
217        return list;
218    }
219
220    /**
221     * Gets the list of values. The id is not included.
222     */
223    public List<Serializable> getValues() {
224        List<Serializable> list = new ArrayList<Serializable>(size);
225        for (int i = 0; i < size; i++) {
226            list.add(values[i]);
227        }
228        return list;
229    }
230
231    @Override
232    public String toString() {
233        StringBuilder buf = new StringBuilder();
234        buf.append(getClass().getSimpleName());
235        buf.append('(');
236        buf.append(tableName);
237        buf.append(", ");
238        buf.append(id);
239        if (size != -1) {
240            // single row
241            buf.append(", {");
242            for (int i = 0; i < size; i++) {
243                if (i > 0) {
244                    buf.append(", ");
245                }
246                buf.append(keys[i]);
247                buf.append('=');
248                printValue(values[i], buf);
249            }
250            buf.append('}');
251        } else {
252            // multiple rows
253            buf.append(", [");
254            for (int i = 0; i < values.length; i++) {
255                if (i > 0) {
256                    buf.append(", ");
257                }
258                printValue(values[i], buf);
259            }
260            buf.append(']');
261        }
262        buf.append(')');
263        return buf.toString();
264    }
265
266    public static final int MAX_STRING = 100;
267
268    public static final int MAX_ARRAY = 10;
269
270    @SuppressWarnings("boxing")
271    public static void printValue(Serializable value, StringBuilder buf) {
272        if (value == null) {
273            buf.append("NULL");
274        } else if (value instanceof String) {
275            String v = (String) value;
276            if (v.length() > MAX_STRING) {
277                v = v.substring(0, MAX_STRING) + "...(" + v.length() + " chars)...";
278            }
279            buf.append('"');
280            buf.append(v);
281            buf.append('"');
282        } else if (value instanceof Calendar) {
283            Calendar cal = (Calendar) value;
284            char sign;
285            int offset = cal.getTimeZone().getOffset(cal.getTimeInMillis()) / 60000;
286            if (offset < 0) {
287                offset = -offset;
288                sign = '-';
289            } else {
290                sign = '+';
291            }
292            buf.append(String.format("Calendar(%04d-%02d-%02dT%02d:%02d:%02d.%03d%c%02d:%02d)", cal.get(Calendar.YEAR), //
293                    cal.get(Calendar.MONTH) + 1, //
294                    cal.get(Calendar.DAY_OF_MONTH), //
295                    cal.get(Calendar.HOUR_OF_DAY), //
296                    cal.get(Calendar.MINUTE), //
297                    cal.get(Calendar.SECOND), //
298                    cal.get(Calendar.MILLISECOND), //
299                    sign, offset / 60, offset % 60));
300        } else if (value.getClass().isArray()) {
301            Serializable[] v = (Serializable[]) value;
302            buf.append('[');
303            for (int i = 0; i < v.length; i++) {
304                if (i > 0) {
305                    buf.append(',');
306                    if (i > MAX_ARRAY) {
307                        buf.append("...(");
308                        buf.append(v.length);
309                        buf.append(" items)...");
310                        break;
311                    }
312                }
313                printValue(v[i], buf);
314            }
315            buf.append(']');
316        } else {
317            buf.append(value.toString());
318        }
319    }
320
321}