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;
038
039import com.google.common.collect.ImmutableSet;
040
041/**
042 * Abstraction for a Map<String, Serializable> that is Serializable.
043 * <p>
044 * Internal storage is optimized to avoid a full {@link HashMap} when there is a small number of keys.
045 *
046 * @since 5.9.5
047 */
048public class State implements StateAccessor, Serializable {
049
050    private static final long serialVersionUID = 1L;
051
052    protected static final Log log = LogFactory.getLog(State.class);
053
054    private static final int HASHMAP_DEFAULT_INITIAL_CAPACITY = 16;
055
056    private static final float HASHMAP_DEFAULT_LOAD_FACTOR = 0.75f;
057
058    // maximum size to use an array after which we switch to a full HashMap
059    public static final int ARRAY_MAX = 5;
060
061    private static final int DEBUG_MAX_STRING = 100;
062
063    private static final int DEBUG_MAX_ARRAY = 10;
064
065    public static final State EMPTY = new State(Collections.<String, Serializable> emptyMap());
066
067    private static final String[] EMPTY_STRING_ARRAY = new String[0];
068
069    /** Initial key order for the {@link #toString} method. */
070    private static final Set<String> TO_STRING_KEY_ORDER = new LinkedHashSet<>(Arrays.asList(
071            new String[] { "ecm:id", "ecm:primaryType", "ecm:name", "ecm:parentId", "ecm:isVersion", "ecm:isProxy" }));
072
073    /**
074     * A diff for a {@link State}.
075     * <p>
076     * Each value is applied to the existing {@link State}. An element can be:
077     * <ul>
078     * <li>a {@link StateDiff}, to be applied on a {@link State},
079     * <li>a {@link ListDiff}, to be applied on an array/{@link List},
080     * <li>an actual value to be set (including {@code null}).
081     * </ul>
082     *
083     * @since 5.9.5
084     */
085    public static class StateDiff extends State {
086        private static final long serialVersionUID = 1L;
087
088        @Override
089        public void put(String key, Serializable value) {
090            // for a StateDiff, we don't have concurrency problems
091            // and we want to store nulls explicitly
092            putEvenIfNull(key, value);
093        }
094    }
095
096    /**
097     * Singleton marker.
098     */
099    private static enum Nop {
100        NOP
101    }
102
103    /**
104     * Denotes no change to an element.
105     */
106    public static final Nop NOP = Nop.NOP;
107
108    /**
109     * A diff for an array or {@link List}.
110     * <p>
111     * This diff is applied onto an existing array/{@link List} in the following manner:
112     * <ul>
113     * <li>{@link #diff}, if any, is applied,
114     * <li>{@link #rpush}, if any, is applied.
115     * </ul>
116     *
117     * @since 5.9.5
118     */
119    public static class ListDiff implements Serializable {
120
121        private static final long serialVersionUID = 1L;
122
123        /**
124         * Whether this {@link ListDiff} applies to an array ({@code true}) or a {@link List} ({@code false}).
125         */
126        public boolean isArray;
127
128        /**
129         * If diff is not {@code null}, each element of the list is applied to the existing array/{@link List}. An
130         * element can be:
131         * <ul>
132         * <li>a {@link StateDiff}, to be applied on a {@link State},
133         * <li>an actual value to be set (including {@code null}),
134         * <li>{@link #NOP} if no change is needed.
135         * </ul>
136         */
137        public List<Object> diff;
138
139        /**
140         * If rpush is not {@code null}, this is appended to the right of the existing array/{@link List}.
141         */
142        public List<Object> rpush;
143
144        @Override
145        public String toString() {
146            return getClass().getSimpleName() + '(' + (isArray ? "array" : "list")
147                    + (diff == null ? "" : ", DIFF " + diff) + (rpush == null ? "" : ", RPUSH " + rpush) + ')';
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 put(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, 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);
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, value);
288                    keys = null;
289                    values = null;
290                }
291            }
292        }
293    }
294
295    /**
296     * Removes the mapping for a key.
297     *
298     * @return the previous value associated with the key, or {@code null} if there was no mapping for the key
299     */
300    public Serializable remove(Object key) {
301        if (map != null) {
302            return map.remove(key);
303        } else {
304            int i = keys.indexOf(key);
305            if (i >= 0) {
306                keys.remove(i);
307                return values.remove(i);
308            } else {
309                return null;
310            }
311        }
312    }
313
314    /**
315     * Gets the key set. IT MUST NOT BE MODIFIED.
316     */
317    public Set<String> keySet() {
318        if (map != null) {
319            return map.keySet();
320        } else {
321            return ImmutableSet.copyOf(keys);
322        }
323    }
324
325    /**
326     * Gets an array of keys.
327     */
328    public String[] keyArray() {
329        if (map != null) {
330            return map.keySet().toArray(EMPTY_STRING_ARRAY);
331        } else {
332            return keys.toArray(EMPTY_STRING_ARRAY);
333        }
334    }
335
336    /**
337     * Checks if there is a mapping for the given key.
338     */
339    public boolean containsKey(Object key) {
340        if (map != null) {
341            return map.containsKey(key);
342        } else {
343            return keys.contains(key);
344        }
345    }
346
347    /**
348     * Gets the entry set. IT MUST NOT BE MODIFIED.
349     */
350    public Set<Entry<String, Serializable>> entrySet() {
351        if (map != null) {
352            return map.entrySet();
353        } else {
354            return new ArraysEntrySet();
355        }
356    }
357
358    /** EntrySet optimized to just return a simple Iterator on the entries. */
359    protected class ArraysEntrySet implements Set<Entry<String, Serializable>> {
360
361        @Override
362        public int size() {
363            return keys.size();
364        }
365
366        @Override
367        public boolean isEmpty() {
368            return keys.isEmpty();
369        }
370
371        @Override
372        public Iterator<Entry<String, Serializable>> iterator() {
373            return new ArraysEntryIterator();
374        }
375
376        @Override
377        public boolean contains(Object o) {
378            throw new UnsupportedOperationException();
379        }
380
381        @Override
382        public Object[] toArray() {
383            throw new UnsupportedOperationException();
384        }
385
386        @Override
387        public <T> T[] toArray(T[] a) {
388            throw new UnsupportedOperationException();
389        }
390
391        @Override
392        public boolean add(Entry<String, Serializable> e) {
393            throw new UnsupportedOperationException();
394        }
395
396        @Override
397        public boolean remove(Object o) {
398            throw new UnsupportedOperationException();
399        }
400
401        @Override
402        public boolean containsAll(Collection<?> c) {
403            throw new UnsupportedOperationException();
404        }
405
406        @Override
407        public boolean addAll(Collection<? extends Entry<String, Serializable>> c) {
408            throw new UnsupportedOperationException();
409        }
410
411        @Override
412        public boolean retainAll(Collection<?> c) {
413            throw new UnsupportedOperationException();
414        }
415
416        @Override
417        public boolean removeAll(Collection<?> c) {
418            throw new UnsupportedOperationException();
419        }
420
421        @Override
422        public void clear() {
423            throw new UnsupportedOperationException();
424        }
425    }
426
427    public class ArraysEntryIterator implements Iterator<Entry<String, Serializable>> {
428
429        private int index;
430
431        @Override
432        public boolean hasNext() {
433            return index < keys.size();
434        }
435
436        @Override
437        public Entry<String, Serializable> next() {
438            return new ArraysEntry(index++);
439        }
440    }
441
442    public class ArraysEntry implements Entry<String, Serializable> {
443
444        private final int index;
445
446        public ArraysEntry(int index) {
447            this.index = index;
448        }
449
450        @Override
451        public String getKey() {
452            return keys.get(index);
453        }
454
455        @Override
456        public Serializable getValue() {
457            return values.get(index);
458        }
459
460        @Override
461        public Serializable setValue(Serializable value) {
462            throw new UnsupportedOperationException();
463        }
464    }
465
466    /**
467     * Overridden to display Calendars and arrays better, and truncate long strings and arrays.
468     * <p>
469     * Also displays some keys first (ecm:id, ecm:name, ecm:primaryType)
470     */
471    @Override
472    public String toString() {
473        if (isEmpty()) {
474            return "{}";
475        }
476        StringBuilder buf = new StringBuilder();
477        buf.append('{');
478        boolean empty = true;
479        // some keys go first
480        for (String key : TO_STRING_KEY_ORDER) {
481            if (containsKey(key)) {
482                if (!empty) {
483                    buf.append(", ");
484                }
485                empty = false;
486                buf.append(key);
487                buf.append('=');
488                toString(buf, get(key));
489            }
490        }
491        // sort keys
492        String[] keys = keyArray();
493        Arrays.sort(keys);
494        for (String key : keys) {
495            if (TO_STRING_KEY_ORDER.contains(key)) {
496                // already done
497                continue;
498            }
499            if (!empty) {
500                buf.append(", ");
501            }
502            empty = false;
503            buf.append(key);
504            buf.append('=');
505            toString(buf, get(key));
506        }
507        buf.append('}');
508        return buf.toString();
509    }
510
511    @SuppressWarnings("boxing")
512    protected static void toString(StringBuilder buf, Object value) {
513        if (value instanceof String) {
514            String v = (String) value;
515            if (v.length() > DEBUG_MAX_STRING) {
516                v = v.substring(0, DEBUG_MAX_STRING) + "...(" + v.length() + " chars)...";
517            }
518            buf.append(v);
519        } else if (value instanceof Calendar) {
520            Calendar cal = (Calendar) value;
521            char sign;
522            int offset = cal.getTimeZone().getOffset(cal.getTimeInMillis()) / 60000;
523            if (offset < 0) {
524                offset = -offset;
525                sign = '-';
526            } else {
527                sign = '+';
528            }
529            buf.append(String.format("Calendar(%04d-%02d-%02dT%02d:%02d:%02d.%03d%c%02d:%02d)", cal.get(Calendar.YEAR), //
530                    cal.get(Calendar.MONTH) + 1, //
531                    cal.get(Calendar.DAY_OF_MONTH), //
532                    cal.get(Calendar.HOUR_OF_DAY), //
533                    cal.get(Calendar.MINUTE), //
534                    cal.get(Calendar.SECOND), //
535                    cal.get(Calendar.MILLISECOND), //
536                    sign, offset / 60, offset % 60));
537        } else if (value instanceof Object[]) {
538            Object[] v = (Object[]) value;
539            buf.append('[');
540            for (int i = 0; i < v.length; i++) {
541                if (i > 0) {
542                    buf.append(',');
543                    if (i > DEBUG_MAX_ARRAY) {
544                        buf.append("...(" + v.length + " items)...");
545                        break;
546                    }
547                }
548                toString(buf, v[i]);
549            }
550            buf.append(']');
551        } else {
552            buf.append(value);
553        }
554    }
555
556    @Override
557    public Object getSingle(String name) {
558        Serializable object = get(name);
559        if (object instanceof Object[]) {
560            Object[] array = (Object[]) object;
561            if (array.length == 0) {
562                return null;
563            } else if (array.length == 1) {
564                // data migration not done in database, return a simple value anyway
565                return array[0];
566            } else {
567                log.warn("Property " + name + ": expected a simple value but read an array: " + Arrays.toString(array));
568                return array[0];
569            }
570        } else {
571            return object;
572        }
573    }
574
575    @Override
576    public Object[] getArray(String name) {
577        Serializable object = get(name);
578        if (object == null) {
579            return null;
580        } else if (object instanceof Object[]) {
581            return (Object[]) object;
582        } else {
583            // data migration not done in database, return an array anyway
584            return new Object[] { object };
585        }
586    }
587
588    @Override
589    public void setSingle(String name, Object value) {
590        put(name, (Serializable) value);
591    }
592
593    @Override
594    public void setArray(String name, Object[] value) {
595        put(name, value);
596    }
597
598    @Override
599    public boolean equals(Object other) {
600        return StateHelper.equalsStrict(this, other);
601    }
602
603}