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