/*
* Copyright (c) 2014 Nuxeo SA (http://nuxeo.com/) and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Florent Guillaume
*/
package org.nuxeo.ecm.core.storage.mem;
import static java.lang.Boolean.TRUE;
import static org.nuxeo.ecm.core.storage.State.NOP;
import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_ID;
import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_IS_PROXY;
import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_NAME;
import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_PARENT_ID;
import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_PROXY_IDS;
import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_PROXY_TARGET_ID;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicLong;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nuxeo.ecm.core.api.ConcurrentUpdateDocumentException;
import org.nuxeo.ecm.core.api.DocumentException;
import org.nuxeo.ecm.core.api.model.Delta;
import org.nuxeo.ecm.core.model.Repository;
import org.nuxeo.ecm.core.query.sql.model.Expression;
import org.nuxeo.ecm.core.query.sql.model.OrderByClause;
import org.nuxeo.ecm.core.storage.State.ListDiff;
import org.nuxeo.ecm.core.storage.State.StateDiff;
import org.nuxeo.ecm.core.storage.StateHelper;
import org.nuxeo.ecm.core.storage.PartialList;
import org.nuxeo.ecm.core.storage.State;
import org.nuxeo.ecm.core.storage.dbs.DBSDocument;
import org.nuxeo.ecm.core.storage.dbs.DBSExpressionEvaluator;
import org.nuxeo.ecm.core.storage.dbs.DBSExpressionEvaluator.OrderByComparator;
import org.nuxeo.ecm.core.storage.dbs.DBSRepositoryBase;
/**
* In-memory implementation of a {@link Repository}.
* <p>
* Internally, the repository is a map from id to document object.
* <p>
* A document object is a JSON-like document stored as a Map recursively
* containing the data, see {@link DBSDocument} for the description of the
* document.
*
* @since 5.9.4
*/
public class MemRepository extends DBSRepositoryBase {
private static final Log log = LogFactory.getLog(MemRepository.class);
// for debug
private final AtomicLong temporaryIdCounter = new AtomicLong(0);
/**
* The content of the repository, a map of document id -> object.
*/
protected Map<String, State> states;
public MemRepository(String repositoryName) {
super(repositoryName);
initRepository();
}
@Override
public void shutdown() {
super.shutdown();
states = null;
}
protected void initRepository() {
states = new ConcurrentHashMap<>();
initRoot();
}
@Override
public String generateNewId() {
if (DEBUG_UUIDS) {
return "UUID_" + temporaryIdCounter.incrementAndGet();
} else {
return UUID.randomUUID().toString();
}
}
@Override
public State readState(String id) {
State state = states.get(id);
if (state != null) {
if (log.isTraceEnabled()) {
log.trace("read " + id + ": " + state);
}
}
return state;
}
@Override
public List<State> readStates(List<String> ids) {
List<State> list = new ArrayList<>();
for (String id : ids) {
list.add(readState(id));
}
return list;
}
@Override
public void createState(State state) throws DocumentException {
String id = (String) state.get(KEY_ID);
if (log.isTraceEnabled()) {
log.trace("create " + id + ": " + state);
}
if (states.containsKey(id)) {
throw new DocumentException("Already exists: " + id);
}
state = StateHelper.deepCopy(state, true); // thread-safe
StateHelper.resetDeltas(state);
states.put(id, state);
}
@Override
public void updateState(String id, StateDiff diff) throws DocumentException {
if (log.isTraceEnabled()) {
log.trace("update " + id + ": " + diff);
}
State state = states.get(id);
if (state == null) {
throw new ConcurrentUpdateDocumentException("Missing: " + id);
}
applyDiff(state, diff);
}
@Override
public void deleteStates(Set<String> ids) throws DocumentException {
if (log.isTraceEnabled()) {
log.trace("delete " + ids);
}
for (String id : ids) {
if (states.remove(id) == null) {
log.debug("Missing on remove: " + id);
}
}
}
@Override
public State readChildState(String parentId, String name,
Set<String> ignored) {
// TODO optimize by maintaining a parent/child index
for (State state : states.values()) {
if (ignored.contains(state.get(KEY_ID))) {
continue;
}
if (!parentId.equals(state.get(KEY_PARENT_ID))) {
continue;
}
if (!name.equals(state.get(KEY_NAME))) {
continue;
}
return state;
}
return null;
}
@Override
public boolean hasChild(String parentId, String name, Set<String> ignored) {
return readChildState(parentId, name, ignored) != null;
}
@Override
public List<State> queryKeyValue(String key, String value,
Set<String> ignored) {
List<State> list = new ArrayList<>();
for (State state : states.values()) {
String id = (String) state.get(KEY_ID);
if (ignored.contains(id)) {
continue;
}
if (!value.equals(state.get(key))) {
continue;
}
list.add(state);
}
return list;
}
@Override
public void queryKeyValueArray(String key, Object value, Set<String> ids,
Map<String, String> proxyTargets,
Map<String, Object[]> targetProxies) {
STATE: for (State state : states.values()) {
Object[] array = (Object[]) state.get(key);
String id = (String) state.get(KEY_ID);
if (array != null) {
for (Object v : array) {
if (value.equals(v)) {
ids.add(id);
if (proxyTargets != null
&& TRUE.equals(state.get(KEY_IS_PROXY))) {
String targetId = (String) state.get(KEY_PROXY_TARGET_ID);
proxyTargets.put(id, targetId);
}
if (targetProxies != null) {
Object[] proxyIds = (Object[]) state.get(KEY_PROXY_IDS);
if (proxyIds != null) {
targetProxies.put(id, proxyIds);
}
}
continue STATE;
}
}
}
}
}
@Override
public boolean queryKeyValuePresence(String key, String value,
Set<String> ignored) {
for (State state : states.values()) {
String id = (String) state.get(KEY_ID);
if (ignored.contains(id)) {
continue;
}
if (value.equals(state.get(key))) {
return true;
}
}
return false;
}
@Override
public PartialList<State> queryAndFetch(Expression expression,
DBSExpressionEvaluator evaluator, OrderByClause orderByClause,
int limit, int offset, int countUpTo, boolean deepCopy,
boolean fulltextScore) {
List<State> maps = new ArrayList<>();
for (State state : states.values()) {
if (evaluator.matches(state)) {
if (deepCopy) {
state = StateHelper.deepCopy(state);
}
maps.add(state);
}
}
// ORDER BY
if (orderByClause != null) {
Collections.sort(maps, new OrderByComparator(orderByClause,
evaluator));
}
// LIMIT / OFFSET
int totalSize = maps.size();
if (countUpTo == -1) {
// count full size
} else if (countUpTo == 0) {
// no count
totalSize = -1; // not counted
} else {
// count only if less than countUpTo
if (totalSize > countUpTo) {
totalSize = -2; // truncated
}
}
if (limit != 0) {
int size = maps.size();
maps.subList(0, offset > size ? size : offset).clear();
size = maps.size();
if (limit < size) {
maps.subList(limit, size).clear();
}
}
// TODO DISTINCT
return new PartialList<>(maps, totalSize);
}
/**
* Applies a {@link StateDiff} in-place onto a base {@link State}.
* <p>
* Uses thread-safe datastructures.
*/
public static void applyDiff(State state, StateDiff stateDiff) {
for (Entry<String, Serializable> en : stateDiff.entrySet()) {
String key = en.getKey();
Serializable diffElem = en.getValue();
if (diffElem instanceof StateDiff) {
Serializable old = state.get(key);
if (old == null) {
old = new State(true); // thread-safe
state.put(key, old);
// enter the next if
}
if (!(old instanceof State)) {
throw new UnsupportedOperationException(
"Cannot apply StateDiff on non-State: " + old);
}
applyDiff((State) old, (StateDiff) diffElem);
} else if (diffElem instanceof ListDiff) {
state.put(key, applyDiff(state.get(key), (ListDiff) diffElem));
} else if (diffElem instanceof Delta) {
Delta delta = (Delta) diffElem;
Number oldValue = (Number) state.get(key);
Number value;
if (oldValue == null) {
value = delta.getFullValue();
} else {
value = delta.add(oldValue);
}
state.put(key, value);
} else {
state.put(key, diffElem);
}
}
}
/**
* Applies a {@link ListDiff} onto an array or {@link List}, and returns the
* resulting value.
* <p>
* Uses thread-safe datastructures.
*/
public static Serializable applyDiff(Serializable value, ListDiff listDiff) {
// internally work on a list
// TODO this is costly, use a separate code path for arrays
if (listDiff.isArray && value != null) {
if (!(value instanceof Object[])) {
throw new UnsupportedOperationException(
"Cannot apply ListDiff on non-array: " + value);
}
value = new CopyOnWriteArrayList<>(Arrays.asList((Object[]) value));
}
if (value == null) {
value = new CopyOnWriteArrayList<>();
}
if (!(value instanceof List)) {
throw new UnsupportedOperationException(
"Cannot apply ListDiff on non-List: " + value);
}
@SuppressWarnings("unchecked")
List<Serializable> list = (List<Serializable>) value;
if (listDiff.diff != null) {
int i = 0;
for (Object diffElem : listDiff.diff) {
if (i >= list.size()) {
// TODO log error applying diff to shorter list
break;
}
if (diffElem instanceof StateDiff) {
applyDiff((State) list.get(i), (StateDiff) diffElem);
} else if (diffElem != NOP) {
list.set(i, StateHelper.deepCopy(diffElem, true)); // thread-safe
}
i++;
}
}
if (listDiff.rpush != null) {
// deepCopy of what we'll add
List<Serializable> add = new ArrayList<>(listDiff.rpush.size());
for (Object v : listDiff.rpush) {
add.add(StateHelper.deepCopy(v, true)); // thread-safe
}
// update CopyOnWriteArrayList in one step
list.addAll(add);
}
// convert back to array if needed
if (listDiff.isArray) {
return list.isEmpty() ? null : list.toArray(new Object[0]);
} else {
return list.isEmpty() ? null : (Serializable) list;
}
}
}