/*
* (C) Copyright 2014 Nuxeo SA (http://nuxeo.com/) and contributors.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Lesser General Public License
* (LGPL) version 2.1 which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/lgpl-2.1.html
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* Contributors:
* Florent Guillaume
*/
package org.nuxeo.ecm.core.storage;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.nuxeo.ecm.core.api.model.Delta;
/**
* Abstraction for a Map<String, Serializable> that is Serializable.
*
* @since 5.9.5
*/
public class State implements Serializable {
private static final long serialVersionUID = 1L;
private static final int HASHMAP_DEFAULT_INITIAL_CAPACITY = 16;
private static final float HASHMAP_DEFAULT_LOAD_FACTOR = 0.75f;
private static final int DEBUG_MAX_STRING = 100;
private static final int DEBUG_MAX_ARRAY = 10;
public static final State EMPTY = new State(
Collections.<String, Serializable> emptyMap());
/** Initial key order for the {@link #toString} method. */
private static final Set<String> TO_STRING_KEY_ORDER = new LinkedHashSet<>(
Arrays.asList(new String[] { "ecm:id", "ecm:primaryType",
"ecm:name", "ecm:parentId", "ecm:isVersion", "ecm:isProxy" }));
/**
* A diff for a {@link State}.
* <p>
* Each value is applied to the existing {@link State}. An element can be:
* <ul>
* <li>a {@link StateDiff}, to be applied on a {@link State},
* <li>a {@link ListDiff}, to be applied on an array/{@link List},
* <li>an actual value to be set (including {@code null}).
* </ul>
*
* @since 5.9.5
*/
public static class StateDiff extends State {
private static final long serialVersionUID = 1L;
@Override
public void put(String key, Serializable value) {
// for a StateDiff, we don't have concurrency problems
// and we want to store nulls explicitly
map.put(key, value);
}
}
/**
* Singleton marker.
*/
private static enum Nop {
NOP
}
/**
* Denotes no change to an element.
*/
public static final Nop NOP = Nop.NOP;
/**
* A diff for an array or {@link List}.
* <p>
* This diff is applied onto an existing array/{@link List} in the following
* manner:
* <ul>
* <li>{@link #diff}, if any, is applied,
* <li>{@link #rpush}, if any, is applied.
* </ul>
*
* @since 5.9.5
*/
public static class ListDiff implements Serializable {
private static final long serialVersionUID = 1L;
/**
* Whether this {@link ListDiff} applies to an array ({@code true}) or a
* {@link List} ({@code false}).
*/
public boolean isArray;
/**
* If diff is not {@code null}, each element of the list is applied to
* the existing array/{@link List}. An element can be:
* <ul>
* <li>a {@link StateDiff}, to be applied on a {@link State},
* <li>an actual value to be set (including {@code null}),
* <li>{@link #NOP} if no change is needed.
* </ul>
*/
public List<Object> diff;
/**
* If rpush is not {@code null}, this is appended to the right of the
* existing array/{@link List}.
*/
public List<Object> rpush;
@Override
public String toString() {
return getClass().getSimpleName() + '('
+ (isArray ? "array" : "list")
+ (diff == null ? "" : ", DIFF " + diff)
+ (rpush == null ? "" : ", RPUSH " + rpush) + ')';
}
}
protected final Map<String, Serializable> map;
/**
* Private constructor with explicit map.
*/
private State(Map<String, Serializable> map) {
this.map = map;
}
/**
* Constructor with default capacity.
*/
public State() {
this(0, false);
}
/**
* Constructor with default capacity, optionally thread-safe.
*
* @param threadSafe if {@code true}, then a {@link ConcurrentHashMap} is
* used
*/
public State(boolean threadSafe) {
this(0, threadSafe);
}
/**
* Constructor for a given default size.
*/
public State(int size) {
this(size, false);
}
/**
* Constructor for a given default size, optionally thread-safe.
*
* @param threadSafe if {@code true}, then a {@link ConcurrentHashMap} is
* used
*/
public State(int size, boolean threadSafe) {
int initialCapacity = Math.max(
(int) (size / HASHMAP_DEFAULT_LOAD_FACTOR) + 1,
HASHMAP_DEFAULT_INITIAL_CAPACITY);
float loadFactor = HASHMAP_DEFAULT_LOAD_FACTOR;
if (threadSafe) {
map = new ConcurrentHashMap<String, Serializable>(initialCapacity,
loadFactor);
} else {
map = new HashMap<>(initialCapacity, loadFactor);
}
}
/**
* Gets the number of elements.
*/
public int size() {
return map.size();
}
/**
* Checks if the state is empty.
*/
public boolean isEmpty() {
return map.isEmpty();
}
/**
* Gets a value for a key, or {@code null} if the key is not present.
*/
public Serializable get(Object key) {
return map.get(key);
}
/**
* Sets a key/value.
*/
public void putInternal(String key, Serializable value) {
if (value == null) {
// if we're using a ConcurrentHashMap
// then null values are forbidden
// this is ok given our semantics of null vs absent key
map.remove(key);
} else {
map.put(key, value);
}
}
/**
* Sets a key/value, dealing with deltas.
*/
public void put(String key, Serializable value) {
Serializable oldValue = map.get(key);
if (oldValue instanceof Delta) {
Delta oldDelta = (Delta) oldValue;
if (value instanceof Delta) {
if (value != oldDelta) {
// add a delta to another delta
value = oldDelta.add((Delta) value);
}
} else if (oldDelta.getFullValue().equals(value)) {
// don't overwrite a delta with the full value
// that actually comes from it
return;
}
}
putInternal(key, value);
}
/**
* Removes the mapping for a key.
*
* @return the previous value associated with the key, or {@code null} if
* there was no mapping for the key
*/
public Serializable remove(Object key) {
return map.remove(key);
}
/**
* Gets the key set. IT MUST NOT BE MODIFIED.
*/
public Set<String> keySet() {
return map.keySet();
}
/**
* Checks if there is a mapping for the given key.
*/
public boolean containsKey(Object key) {
return map.containsKey(key);
}
/**
* Gets the entry set. IT MUST NOT BE MODIFIED.
*/
public Set<Entry<String, Serializable>> entrySet() {
return map.entrySet();
}
/**
* Overridden to display Calendars and arrays better, and truncate long
* strings and arrays.
* <p>
* Also displays some keys first (ecm:id, ecm:name, ecm:primaryType)
*/
@Override
public String toString() {
if (isEmpty()) {
return "{}";
}
StringBuilder buf = new StringBuilder();
buf.append('{');
boolean empty = true;
// some keys go first
for (String key : TO_STRING_KEY_ORDER) {
if (containsKey(key)) {
if (!empty) {
buf.append(", ");
}
empty = false;
buf.append(key);
buf.append('=');
toString(buf, get(key));
}
}
// sort keys
String[] keys = keySet().toArray(new String[0]);
Arrays.sort(keys);
for (String key : keys) {
if (TO_STRING_KEY_ORDER.contains(key)) {
// already done
continue;
}
if (!empty) {
buf.append(", ");
}
empty = false;
buf.append(key);
buf.append('=');
toString(buf, get(key));
}
buf.append('}');
return buf.toString();
}
@SuppressWarnings("boxing")
protected static void toString(StringBuilder buf, Object value) {
if (value instanceof String) {
String v = (String) value;
if (v.length() > DEBUG_MAX_STRING) {
v = v.substring(0, DEBUG_MAX_STRING) + "...(" + v.length()
+ " chars)...";
}
buf.append(v);
} else if (value instanceof Calendar) {
Calendar cal = (Calendar) value;
char sign;
int offset = cal.getTimeZone().getOffset(cal.getTimeInMillis()) / 60000;
if (offset < 0) {
offset = -offset;
sign = '-';
} else {
sign = '+';
}
buf.append(String.format(
"Calendar(%04d-%02d-%02dT%02d:%02d:%02d.%03d%c%02d:%02d)",
cal.get(Calendar.YEAR), //
cal.get(Calendar.MONTH) + 1, //
cal.get(Calendar.DAY_OF_MONTH), //
cal.get(Calendar.HOUR_OF_DAY), //
cal.get(Calendar.MINUTE), //
cal.get(Calendar.SECOND), //
cal.get(Calendar.MILLISECOND), //
sign, offset / 60, offset % 60));
} else if (value instanceof Object[]) {
Object[] v = (Object[]) value;
buf.append('[');
for (int i = 0; i < v.length; i++) {
if (i > 0) {
buf.append(',');
if (i > DEBUG_MAX_ARRAY) {
buf.append("...(" + v.length + " items)...");
break;
}
}
toString(buf, v[i]);
}
buf.append(']');
} else {
buf.append(value);
}
}
}