001/*
002 * (C) Copyright 2014 Nuxeo SA (http://nuxeo.com/) and contributors.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser General Public License
006 * (LGPL) version 2.1 which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/lgpl-2.1.html
008 *
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * Contributors:
015 *     Florent Guillaume
016 */
017package org.nuxeo.ecm.core.storage;
018
019import java.io.Serializable;
020import java.util.ArrayList;
021import java.util.Arrays;
022import java.util.Calendar;
023import java.util.Collection;
024import java.util.Collections;
025import java.util.HashMap;
026import java.util.Iterator;
027import java.util.LinkedHashSet;
028import java.util.List;
029import java.util.Map;
030import java.util.Map.Entry;
031import java.util.Set;
032import java.util.concurrent.ConcurrentHashMap;
033
034import org.apache.commons.logging.Log;
035import org.apache.commons.logging.LogFactory;
036import org.nuxeo.ecm.core.api.model.Delta;
037
038import com.google.common.collect.ImmutableSet;
039
040/**
041 * Abstraction for a Map<String, Serializable> that is Serializable.
042 * <p>
043 * Internal storage is optimized to avoid a full {@link HashMap} when there is a small number of keys.
044 *
045 * @since 5.9.5
046 */
047public class State implements StateAccessor, Serializable {
048
049    private static final long serialVersionUID = 1L;
050
051    protected static final Log log = LogFactory.getLog(State.class);
052
053    private static final int HASHMAP_DEFAULT_INITIAL_CAPACITY = 16;
054
055    private static final float HASHMAP_DEFAULT_LOAD_FACTOR = 0.75f;
056
057    // maximum size to use an array after which we switch to a full HashMap
058    public static final int ARRAY_MAX = 5;
059
060    private static final int DEBUG_MAX_STRING = 100;
061
062    private static final int DEBUG_MAX_ARRAY = 10;
063
064    public static final State EMPTY = new State(Collections.<String, Serializable> emptyMap());
065
066    private static final String[] EMPTY_STRING_ARRAY = new String[0];
067
068    /** Initial key order for the {@link #toString} method. */
069    private static final Set<String> TO_STRING_KEY_ORDER = new LinkedHashSet<>(Arrays.asList(new String[] { "ecm:id",
070            "ecm:primaryType", "ecm:name", "ecm:parentId", "ecm:isVersion", "ecm:isProxy" }));
071
072    /**
073     * A diff for a {@link State}.
074     * <p>
075     * Each value is applied to the existing {@link State}. An element can be:
076     * <ul>
077     * <li>a {@link StateDiff}, to be applied on a {@link State},
078     * <li>a {@link ListDiff}, to be applied on an array/{@link List},
079     * <li>an actual value to be set (including {@code null}).
080     * </ul>
081     *
082     * @since 5.9.5
083     */
084    public static class StateDiff extends State {
085        private static final long serialVersionUID = 1L;
086
087        @Override
088        public void put(String key, Serializable value) {
089            // for a StateDiff, we don't have concurrency problems
090            // and we want to store nulls explicitly
091            putEvenIfNull(key, value);
092        }
093    }
094
095    /**
096     * Singleton marker.
097     */
098    private static enum Nop {
099        NOP
100    }
101
102    /**
103     * Denotes no change to an element.
104     */
105    public static final Nop NOP = Nop.NOP;
106
107    /**
108     * A diff for an array or {@link List}.
109     * <p>
110     * This diff is applied onto an existing array/{@link List} in the following manner:
111     * <ul>
112     * <li>{@link #diff}, if any, is applied,
113     * <li>{@link #rpush}, if any, is applied.
114     * </ul>
115     *
116     * @since 5.9.5
117     */
118    public static class ListDiff implements Serializable {
119
120        private static final long serialVersionUID = 1L;
121
122        /**
123         * Whether this {@link ListDiff} applies to an array ({@code true}) or a {@link List} ({@code false}).
124         */
125        public boolean isArray;
126
127        /**
128         * If diff is not {@code null}, each element of the list is applied to the existing array/{@link List}. An
129         * element can be:
130         * <ul>
131         * <li>a {@link StateDiff}, to be applied on a {@link State},
132         * <li>an actual value to be set (including {@code null}),
133         * <li>{@link #NOP} if no change is needed.
134         * </ul>
135         */
136        public List<Object> diff;
137
138        /**
139         * If rpush is not {@code null}, this is appended to the right of the existing array/{@link List}.
140         */
141        public List<Object> rpush;
142
143        @Override
144        public String toString() {
145            return getClass().getSimpleName() + '(' + (isArray ? "array" : "list")
146                    + (diff == null ? "" : ", DIFF " + diff) + (rpush == null ? "" : ", RPUSH " + rpush) + ')';
147        }
148    }
149
150
151    // if map != null then use it
152    protected Map<String, Serializable> map;
153
154    // else use keys / values
155    protected List<String> keys;
156
157    protected List<Serializable> values;
158
159    /**
160     * Private constructor with explicit map.
161     */
162    private State(Map<String, Serializable> map) {
163        this.map = map;
164    }
165
166    /**
167     * Constructor with default capacity.
168     */
169    public State() {
170        this(0, false);
171    }
172
173    /**
174     * Constructor with default capacity, optionally thread-safe.
175     *
176     * @param threadSafe if {@code true}, then a {@link ConcurrentHashMap} is used
177     */
178    public State(boolean threadSafe) {
179        this(0, threadSafe);
180    }
181
182    /**
183     * Constructor for a given default size.
184     */
185    public State(int size) {
186        this(size, false);
187    }
188
189    /**
190     * Constructor for a given default size, optionally thread-safe.
191     *
192     * @param threadSafe if {@code true}, then a {@link ConcurrentHashMap} is used
193     */
194    public State(int size, boolean threadSafe) {
195        if (threadSafe) {
196            map = new ConcurrentHashMap<String, Serializable>(initialCapacity(size));
197        } else {
198            if (size > ARRAY_MAX) {
199                map = new HashMap<>(initialCapacity(size));
200            } else {
201                keys = new ArrayList<String>(size);
202                values = new ArrayList<Serializable>(size);
203            }
204        }
205    }
206
207    protected static int initialCapacity(int size) {
208        return Math.max((int) (size / HASHMAP_DEFAULT_LOAD_FACTOR) + 1, HASHMAP_DEFAULT_INITIAL_CAPACITY);
209    }
210
211    /**
212     * Gets the number of elements.
213     */
214    public int size() {
215        if (map != null) {
216            return map.size();
217        } else {
218            return keys.size();
219        }
220    }
221
222    /**
223     * Checks if the state is empty.
224     */
225    public boolean isEmpty() {
226        if (map != null) {
227            return map.isEmpty();
228        } else {
229            return keys.isEmpty();
230        }
231    }
232
233    /**
234     * Gets a value for a key, or {@code null} if the key is not present.
235     */
236    public Serializable get(Object key) {
237        if (map != null) {
238            return map.get(key);
239        } else {
240            int i = keys.indexOf(key);
241            return i >= 0 ? values.get(i) : null;
242        }
243    }
244
245    /**
246     * Sets a key/value.
247     */
248    public void putInternal(String key, Serializable value) {
249        if (value == null) {
250            // if we're using a ConcurrentHashMap
251            // then null values are forbidden
252            // this is ok given our semantics of null vs absent key
253            if (map != null) {
254                map.remove(key);
255            } else {
256                int i = keys.indexOf(key);
257                if (i >= 0) {
258                    // cost is not trivial but we don't use this often, if at all
259                    keys.remove(i);
260                    values.remove(i);
261                }
262            }
263        } else {
264            putEvenIfNull(key, value);
265        }
266    }
267
268    protected void putEvenIfNull(String key, Serializable value) {
269        if (map != null) {
270            map.put(key.intern(), value);
271        } else {
272            int i = keys.indexOf(key);
273            if (i >= 0) {
274                // existing key
275                values.set(i, value);
276            } else {
277                // new key
278                if (keys.size() < ARRAY_MAX) {
279                    keys.add(key.intern());
280                    values.add(value);
281                } else {
282                    // upgrade to a full HashMap
283                    map = new HashMap<>(initialCapacity(keys.size() + 1));
284                    for (int j = 0; j < keys.size(); j++) {
285                        map.put(keys.get(j), values.get(j));
286                    }
287                    map.put(key.intern(), value);
288                    keys = null;
289                    values = null;
290                }
291            }
292        }
293    }
294
295    /**
296     * Sets a key/value, dealing with deltas.
297     */
298    public void put(String key, Serializable value) {
299        Serializable oldValue = get(key);
300        if (oldValue instanceof Delta) {
301            Delta oldDelta = (Delta) oldValue;
302            if (value instanceof Delta) {
303                if (value != oldDelta) {
304                    // add a delta to another delta
305                    value = oldDelta.add((Delta) value);
306                }
307            } else if (oldDelta.getFullValue().equals(value)) {
308                // don't overwrite a delta with the full value
309                // that actually comes from it
310                return;
311            }
312        }
313        putInternal(key, value);
314    }
315
316    /**
317     * Removes the mapping for a key.
318     *
319     * @return the previous value associated with the key, or {@code null} if there was no mapping for the key
320     */
321    public Serializable remove(Object key) {
322        if (map != null) {
323            return map.remove(key);
324        } else {
325            int i = keys.indexOf(key);
326            if (i >= 0) {
327                keys.remove(i);
328                return values.remove(i);
329            } else {
330                return null;
331            }
332        }
333    }
334
335    /**
336     * Gets the key set. IT MUST NOT BE MODIFIED.
337     */
338    public Set<String> keySet() {
339        if (map != null) {
340            return map.keySet();
341        } else {
342            return ImmutableSet.copyOf(keys);
343        }
344    }
345
346    /**
347     * Gets an array of keys.
348     */
349    public String[] keyArray() {
350        if (map != null) {
351            return map.keySet().toArray(EMPTY_STRING_ARRAY);
352        } else {
353            return keys.toArray(EMPTY_STRING_ARRAY);
354        }
355    }
356
357    /**
358     * Checks if there is a mapping for the given key.
359     */
360    public boolean containsKey(Object key) {
361        if (map != null) {
362            return map.containsKey(key);
363        } else {
364            return keys.contains(key);
365        }
366    }
367
368    /**
369     * Gets the entry set. IT MUST NOT BE MODIFIED.
370     */
371    public Set<Entry<String, Serializable>> entrySet() {
372        if (map != null) {
373            return map.entrySet();
374        } else {
375            return new ArraysEntrySet();
376        }
377    }
378
379    /** EntrySet optimized to just return a simple Iterator on the entries. */
380    protected class ArraysEntrySet implements Set<Entry<String, Serializable>> {
381
382        @Override
383        public int size() {
384            return keys.size();
385        }
386
387        @Override
388        public boolean isEmpty() {
389            return keys.isEmpty();
390        }
391
392        @Override
393        public Iterator<Entry<String, Serializable>> iterator() {
394            return new ArraysEntryIterator();
395        }
396
397        @Override
398        public boolean contains(Object o) {
399            throw new UnsupportedOperationException();
400        }
401
402        @Override
403        public Object[] toArray() {
404            throw new UnsupportedOperationException();
405        }
406
407        @Override
408        public <T> T[] toArray(T[] a) {
409            throw new UnsupportedOperationException();
410        }
411
412        @Override
413        public boolean add(Entry<String, Serializable> e) {
414            throw new UnsupportedOperationException();
415        }
416
417        @Override
418        public boolean remove(Object o) {
419            throw new UnsupportedOperationException();
420        }
421
422        @Override
423        public boolean containsAll(Collection<?> c) {
424            throw new UnsupportedOperationException();
425        }
426
427        @Override
428        public boolean addAll(Collection<? extends Entry<String, Serializable>> c) {
429            throw new UnsupportedOperationException();
430        }
431
432        @Override
433        public boolean retainAll(Collection<?> c) {
434            throw new UnsupportedOperationException();
435        }
436
437        @Override
438        public boolean removeAll(Collection<?> c) {
439            throw new UnsupportedOperationException();
440        }
441
442        @Override
443        public void clear() {
444            throw new UnsupportedOperationException();
445        }
446    }
447
448    public class ArraysEntryIterator implements Iterator<Entry<String, Serializable>> {
449
450        private int index;
451
452        @Override
453        public boolean hasNext() {
454            return index < keys.size();
455        }
456
457        @Override
458        public Entry<String, Serializable> next() {
459            return new ArraysEntry(index++);
460        }
461    }
462
463    public class ArraysEntry implements Entry<String, Serializable> {
464
465        private final int index;
466
467        public ArraysEntry(int index) {
468            this.index = index;
469        }
470
471        @Override
472        public String getKey() {
473            return keys.get(index);
474        }
475
476        @Override
477        public Serializable getValue() {
478            return values.get(index);
479        }
480
481        @Override
482        public Serializable setValue(Serializable value) {
483            throw new UnsupportedOperationException();
484        }
485    }
486
487    /**
488     * Overridden to display Calendars and arrays better, and truncate long strings and arrays.
489     * <p>
490     * Also displays some keys first (ecm:id, ecm:name, ecm:primaryType)
491     */
492    @Override
493    public String toString() {
494        if (isEmpty()) {
495            return "{}";
496        }
497        StringBuilder buf = new StringBuilder();
498        buf.append('{');
499        boolean empty = true;
500        // some keys go first
501        for (String key : TO_STRING_KEY_ORDER) {
502            if (containsKey(key)) {
503                if (!empty) {
504                    buf.append(", ");
505                }
506                empty = false;
507                buf.append(key);
508                buf.append('=');
509                toString(buf, get(key));
510            }
511        }
512        // sort keys
513        String[] keys = keyArray();
514        Arrays.sort(keys);
515        for (String key : keys) {
516            if (TO_STRING_KEY_ORDER.contains(key)) {
517                // already done
518                continue;
519            }
520            if (!empty) {
521                buf.append(", ");
522            }
523            empty = false;
524            buf.append(key);
525            buf.append('=');
526            toString(buf, get(key));
527        }
528        buf.append('}');
529        return buf.toString();
530    }
531
532    @SuppressWarnings("boxing")
533    protected static void toString(StringBuilder buf, Object value) {
534        if (value instanceof String) {
535            String v = (String) value;
536            if (v.length() > DEBUG_MAX_STRING) {
537                v = v.substring(0, DEBUG_MAX_STRING) + "...(" + v.length() + " chars)...";
538            }
539            buf.append(v);
540        } else if (value instanceof Calendar) {
541            Calendar cal = (Calendar) value;
542            char sign;
543            int offset = cal.getTimeZone().getOffset(cal.getTimeInMillis()) / 60000;
544            if (offset < 0) {
545                offset = -offset;
546                sign = '-';
547            } else {
548                sign = '+';
549            }
550            buf.append(String.format("Calendar(%04d-%02d-%02dT%02d:%02d:%02d.%03d%c%02d:%02d)", cal.get(Calendar.YEAR), //
551                    cal.get(Calendar.MONTH) + 1, //
552                    cal.get(Calendar.DAY_OF_MONTH), //
553                    cal.get(Calendar.HOUR_OF_DAY), //
554                    cal.get(Calendar.MINUTE), //
555                    cal.get(Calendar.SECOND), //
556                    cal.get(Calendar.MILLISECOND), //
557                    sign, offset / 60, offset % 60));
558        } else if (value instanceof Object[]) {
559            Object[] v = (Object[]) value;
560            buf.append('[');
561            for (int i = 0; i < v.length; i++) {
562                if (i > 0) {
563                    buf.append(',');
564                    if (i > DEBUG_MAX_ARRAY) {
565                        buf.append("...(" + v.length + " items)...");
566                        break;
567                    }
568                }
569                toString(buf, v[i]);
570            }
571            buf.append(']');
572        } else {
573            buf.append(value);
574        }
575    }
576
577    @Override
578    public Object getSingle(String name) {
579        Serializable object = get(name);
580        if (object instanceof Object[]) {
581            Object[] array = (Object[]) object;
582            if (array.length == 0) {
583                return null;
584            } else if (array.length == 1) {
585                // data migration not done in database, return a simple value anyway
586                return array[0];
587            } else {
588                log.warn("Property " + name + ": expected a simple value but read an array: " + Arrays.toString(array));
589                return array[0];
590            }
591        } else {
592            return object;
593        }
594    }
595
596    @Override
597    public Object[] getArray(String name) {
598        Serializable object = get(name);
599        if (object == null) {
600            return null;
601        } else if (object instanceof Object[]) {
602            return (Object[]) object;
603        } else {
604            // data migration not done in database, return an array anyway
605            return new Object[] { object };
606        }
607    }
608
609    @Override
610    public void setSingle(String name, Object value) {
611        put(name, (Serializable) value);
612    }
613
614    @Override
615    public void setArray(String name, Object[] value) {
616        put(name, value);
617    }
618
619}