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