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