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