package li.strolch.persistence.api;

import li.strolch.agent.api.*;
import li.strolch.agent.impl.*;
import li.strolch.exception.StrolchException;
import li.strolch.exception.StrolchModelException;
import li.strolch.handler.operationslog.OperationsLog;
import li.strolch.model.*;
import li.strolch.model.activity.Action;
import li.strolch.model.activity.Activity;
import li.strolch.model.activity.IActivityElement;
import li.strolch.model.audit.AccessType;
import li.strolch.model.audit.Audit;
import li.strolch.model.log.LogMessage;
import li.strolch.model.log.LogMessageState;
import li.strolch.model.log.LogSeverity;
import li.strolch.model.parameter.Parameter;
import li.strolch.model.parameter.StringListParameter;
import li.strolch.model.parameter.StringParameter;
import li.strolch.model.policy.PolicyDef;
import li.strolch.model.timedstate.StrolchTimedState;
import li.strolch.model.timevalue.IValue;
import li.strolch.policy.PolicyHandler;
import li.strolch.policy.StrolchPolicy;
import li.strolch.privilege.base.AccessDeniedException;
import li.strolch.privilege.base.PrivilegeException;
import li.strolch.privilege.model.Certificate;
import li.strolch.privilege.model.PrivilegeContext;
import li.strolch.privilege.model.Restrictable;
import li.strolch.runtime.privilege.PrivilegeHandler;
import li.strolch.runtime.privilege.TransactedRestrictable;
import li.strolch.service.api.Command;
import li.strolch.utils.collections.MapOfMaps;
import li.strolch.utils.dbc.DBC;
import li.strolch.utils.helper.StringHelper;
import li.strolch.utils.objectfilter.ObjectFilter;
import li.strolch.utils.objectfilter.ObjectFilterStatistics;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;

import static java.text.MessageFormat.format;
import static li.strolch.agent.api.StrolchAgent.getUniqueId;
import static li.strolch.model.StrolchModelConstants.*;
import static li.strolch.model.Tags.*;
import static li.strolch.utils.collections.SynchronizedCollections.synchronizedMapOfMaps;
import static li.strolch.utils.helper.ExceptionHelper.getExceptionMessage;
import static li.strolch.utils.helper.StringHelper.formatNanoDuration;

/**
 * @author Robert von Burg <eitch@eitchnet.ch>
 */
public abstract class AbstractTransaction implements StrolchTransaction {

	protected static final Logger logger = LoggerFactory.getLogger(AbstractTransaction.class);

	private final ComponentContainer container;
	private final PrivilegeHandler privilegeHandler;
	private final InternalStrolchRealm realm;
	private final TransactionResult txResult;

	private final ObjectFilter objectFilter;
	private ObjectFilterStatistics objectFilterStatistics;
	private final MapOfMaps<String, String, Resource> resourceCache;
	private final MapOfMaps<String, String, Order> orderCache;
	private final MapOfMaps<String, String, Activity> activityCache;

	private TransactionCloseStrategy closeStrategy;
	private long silentThreshold;
	private boolean suppressUpdates;
	private boolean suppressAudits;
	private boolean suppressAuditsForAudits;

	private List<Command> commands;
	private final List<Command> flushedCommands;
	private final Set<Locator> lockedElements;

	private AuditingOrderMap orderMap;
	private AuditingResourceMap resourceMap;
	private AuditingActivityMap activityMap;
	private AuditingAuditMapFacade auditTrail;

	private final String action;
	private final Certificate certificate;
	private PrivilegeContext privilegeContext;

	public AbstractTransaction(ComponentContainer container, StrolchRealm realm, Certificate certificate, String action,
			boolean readOnly) {
		DBC.PRE.assertNotNull("container must be set!", container);
		DBC.PRE.assertNotNull("realm must be set!", realm);
		DBC.PRE.assertNotNull("certificate must be set!", certificate);
		DBC.PRE.assertNotNull("action must be set!", action);

		TransactionThreadLocal.setTx(this);

		this.container = container;
		this.privilegeHandler = container.getPrivilegeHandler();
		this.realm = (InternalStrolchRealm) realm;
		this.action = action;
		this.certificate = certificate;

		this.commands = new ArrayList<>();
		this.flushedCommands = new ArrayList<>();
		this.lockedElements = new HashSet<>();
		this.closeStrategy = readOnly ? TransactionCloseStrategy.READ_ONLY : TransactionCloseStrategy.DEFAULT;
		this.txResult = new TransactionResult(getRealmName(), System.nanoTime(), new Date());
		this.txResult.setState(TransactionState.OPEN);

		this.objectFilter = new ObjectFilter();
		this.resourceCache = synchronizedMapOfMaps(new MapOfMaps<>(1));
		this.orderCache = synchronizedMapOfMaps(new MapOfMaps<>(1));
		this.activityCache = synchronizedMapOfMaps(new MapOfMaps<>(1));
	}

	@Override
	public String getAction() {
		return this.action;
	}

	public TransactionResult getTxResult() {
		return this.txResult;
	}

	@Override
	public TransactionState getState() {
		return this.txResult.getState();
	}

	@Override
	public boolean isOpen() {
		return this.txResult.getState().isOpen();
	}

	@Override
	public boolean isRollingBack() {
		return this.txResult.getState().isRollingBack();
	}

	@Override
	public boolean isCommitting() {
		return this.txResult.getState().isCommitting();
	}

	@Override
	public boolean isClosing() {
		return this.txResult.getState().isClosing();
	}

	@Override
	public boolean isClosed() {
		return this.txResult.getState().isClosed();
	}

	@Override
	public boolean isFailed() {
		return this.txResult.getState().isFailed();
	}

	@Override
	public String getRealmName() {
		return this.realm.getRealm();
	}

	public StrolchRealm getRealm() {
		return this.realm;
	}

	@Override
	public Certificate getCertificate() {
		return this.certificate;
	}

	@Override
	public Locale getLocale() {
		return this.certificate.getLocale();
	}

	@Override
	public PrivilegeContext getPrivilegeContext() {
		if (this.privilegeContext == null) {
			this.privilegeContext = this.privilegeHandler.validate(this.certificate);
		}
		return this.privilegeContext;
	}

	@Override
	public void validateAction(Restrictable restrictable) throws PrivilegeException {
		getPrivilegeContext().validateAction(restrictable);
	}

	@Override
	public TransactionCloseStrategy getCloseStrategy() {
		return this.closeStrategy;
	}

	private void setCloseStrategy(TransactionCloseStrategy closeStrategy) {
		this.closeStrategy = closeStrategy;
	}

	@Override
	public StrolchTransaction readOnly() {
		setCloseStrategy(TransactionCloseStrategy.READ_ONLY);
		return this;
	}

	@Override
	public StrolchTransaction doNothingOnClose() {
		setCloseStrategy(TransactionCloseStrategy.READ_ONLY);
		return this;
	}

	@Override
	public StrolchTransaction commitOnClose() {
		setCloseStrategy(TransactionCloseStrategy.COMMIT);
		return this;
	}

	@Override
	public StrolchTransaction rollbackOnClose() {
		setCloseStrategy(TransactionCloseStrategy.ROLLBACK);
		return this;
	}

	@Override
	public StrolchTransactionException fail(String string) {
		setCloseStrategy(TransactionCloseStrategy.ROLLBACK);
		return new StrolchTransactionException(string);
	}

	@Override
	public StrolchTransaction silentThreshold(long silentThreshold, TimeUnit timeUnit) {
		this.silentThreshold = timeUnit.toNanos(silentThreshold);
		return this;
	}

	@Override
	public long getSilentThreshold() {
		return TimeUnit.NANOSECONDS.toMillis(this.silentThreshold);
	}

	@Override
	public StrolchTransaction suppressUpdates() {
		this.suppressUpdates = true;
		return this;
	}

	@Override
	public void setSuppressUpdates(boolean suppressUpdates) {
		this.suppressUpdates = suppressUpdates;
	}

	@Override
	public boolean isSuppressUpdates() {
		return this.suppressUpdates;
	}

	@Override
	public void setSuppressAudits(boolean suppressAudits) {
		this.suppressAudits = suppressAudits;
	}

	@Override
	public void setSuppressAuditsForAudits(boolean suppressAuditsForAudits) {
		this.suppressAuditsForAudits = suppressAuditsForAudits;
	}

	@Override
	public boolean isSuppressAudits() {
		return this.suppressAudits;
	}

	@Override
	public boolean isSuppressAuditsForAudits() {
		return this.suppressAuditsForAudits;
	}

	@Override
	public boolean isVersioningEnabled() {
		return this.realm.isVersioningEnabled();
	}

	@Override
	public boolean needsCommit() {
		return (!this.objectFilter.isEmpty()) || !this.commands.isEmpty() || !this.flushedCommands.isEmpty();
	}

	@Override
	public ObjectFilterStatistics getStatistics() {
		if (this.objectFilterStatistics == null)
			return this.objectFilter.toStatistics();
		return this.objectFilterStatistics;
	}

	@Override
	public boolean hasLock(Locator locator) throws StrolchLockException {
		return this.lockedElements.contains(locator);
	}

	@Override
	public <T extends StrolchRootElement> boolean hasLock(T element) throws StrolchLockException {
		return this.lockedElements.contains(element.getLocator());
	}

	@Override
	public void lock(Locator locator) throws StrolchLockException {
		locator = locator.trim(3);
		this.realm.lock(locator);
		this.lockedElements.add(locator);
	}

	@Override
	public <T extends StrolchRootElement> void lock(T element) throws StrolchLockException {
		Locator locator = element.getLocator();
		this.realm.lock(locator);
		this.lockedElements.add(locator);

		switch (element.getObjectType()) {
			case RESOURCE -> this.resourceCache.addElement(element.getType(), element.getId(), (Resource) element);
			case ORDER -> this.orderCache.addElement(element.getType(), element.getId(), (Order) element);
			case ACTIVITY -> this.activityCache.addElement(element.getType(), element.getId(), (Activity) element);
			default -> throw new IllegalStateException("Unexpected object type " + element.getObjectType());
		}
	}

	@Override
	public <T extends StrolchRootElement> T readLock(T element) throws StrolchLockException {
		if (hasLock(element))
			return element;

		lock(element);

		Locator locator = element.getLocator();
		removeFromCache(locator);

		StrolchRootElement freshCopy = switch (locator.get(0)) {
			case RESOURCE -> getResourceBy(locator.get(1), locator.get(2), true);
			case ORDER -> getOrderBy(locator.get(1), locator.get(2), true);
			case ACTIVITY -> getActivityBy(locator.get(1), locator.get(2), true);
			default -> throw new IllegalStateException("Unexpected object type " + locator.get(0));
		};

		@SuppressWarnings("unchecked") T t = (T) freshCopy;
		return t;
	}

	@Override
	public <T extends StrolchRootElement> void releaseLock(T element) throws StrolchLockException {
		Locator locator = element.getLocator();
		releaseLock(locator);
	}

	@Override
	public void releaseLock(Locator locator) throws StrolchLockException {
		this.realm.releaseLock(locator);
		this.lockedElements.remove(locator);
	}

	private void releaseElementLocks() {
		for (Locator locator : this.lockedElements) {
			this.realm.releaseLock(locator);
		}
		this.lockedElements.clear();
	}

	@Override
	public void addCommand(Command command) {
		add(command);
	}

	@Override
	public void add(Command command) {
		assertNotReadOnly();
		this.commands.add(command);
	}

	@Override
	public boolean isReadOnly() {
		return this.closeStrategy.isReadonly();
	}

	@Override
	public boolean isWriteable() {
		return this.closeStrategy.isWriteable();
	}

	private void assertNotReadOnly() {
		DBC.PRE.assertFalse("TX is marked as read-only, can not add commands!", isReadOnly());
	}

	@Override
	public ResourceMap getResourceMap() {
		if (this.resourceMap == null) {
			this.resourceMap = new AuditingResourceMap(this.realm.getResourceMap(), isReadOnly(),
					this.realm.isAuditTrailEnabledForRead());
		}
		return this.resourceMap;
	}

	@Override
	public OrderMap getOrderMap() {
		if (this.orderMap == null) {
			this.orderMap = new AuditingOrderMap(this.realm.getOrderMap(), isReadOnly(),
					this.realm.isAuditTrailEnabledForRead());
		}
		return this.orderMap;
	}

	@Override
	public ActivityMap getActivityMap() {
		if (this.activityMap == null) {
			this.activityMap = new AuditingActivityMap(this.realm.getActivityMap(), isReadOnly(),
					this.realm.isAuditTrailEnabledForRead());
		}
		return this.activityMap;
	}

	@Override
	public long getResourceCount() {
		return getResourceMap().querySize(this);
	}

	@Override
	public long getResourceCount(String type) {
		return getResourceMap().querySize(this, type);
	}

	@Override
	public long getOrderCount() {
		return getOrderMap().querySize(this);
	}

	@Override
	public long getOrderCount(String type) {
		return getOrderMap().querySize(this, type);
	}

	@Override
	public long getActivityCount() {
		return getActivityMap().querySize(this);
	}

	@Override
	public long getActivityCount(String type) {
		return getActivityMap().querySize(this, type);
	}

	@Override
	public AuditTrail getAuditTrail() {
		if (this.auditTrail == null) {
			this.auditTrail = new AuditingAuditMapFacade(this.realm.getAuditTrail(),
					this.realm.isAuditTrailEnabledForRead());
		}
		return this.auditTrail;
	}

	@Override
	public StrolchAgent getAgent() {
		return this.container.getAgent();
	}

	@Override
	public ComponentContainer getContainer() {
		return this.container;
	}

	@Override
	public <T extends StrolchPolicy> T getPolicy(PolicyContainer element, Class<T> clazz) {
		PolicyDef policyDef = element.getPolicyDef(clazz);
		return getContainer().getComponent(PolicyHandler.class).getPolicy(policyDef, this);
	}

	@Override
	@Deprecated
	public <T extends StrolchPolicy> T getPolicy(PolicyDef policyDef) {
		return getContainer().getComponent(PolicyHandler.class).getPolicy(policyDef, this);
	}

	@Override
	public <T extends StrolchPolicy> T getPolicy(Class<T> clazz, PolicyDef policyDef) {
		return getContainer().getComponent(PolicyHandler.class).getPolicy(policyDef, this);
	}

	@Override
	@Deprecated
	public <T extends StrolchPolicy> T getPolicy(PolicyDef policyDef, PolicyDef defaultDef) {
		return getContainer().getComponent(PolicyHandler.class).getPolicy(policyDef, defaultDef, this);
	}

	@Override
	public <T extends StrolchPolicy> T getPolicy(Class<T> clazz, PolicyDef policyDef, PolicyDef defaultDef) {
		return getContainer().getComponent(PolicyHandler.class).getPolicy(policyDef, defaultDef, this);
	}

	@Override
	public <T extends StrolchElement> T findElement(Locator locator) throws StrolchModelException, ClassCastException {
		return findElement(locator, false);
	}

	@SuppressWarnings("unchecked")
	@Override
	public <T extends StrolchElement> T findElement(Locator locator, boolean allowNull)
			throws StrolchModelException, ClassCastException {

		// Resource/<type>/<id>
		// Resource/<type>/<id>/Bag/<id>
		// Resource/<type>/<id>/Bag/<id>/<param_id>
		// Resource/<type>/<id>/State/<id>
		// Order/<type>/<id>
		// Order/<type>/<id>/Bag/<id>
		// Order/<type>/<id>/Bag/<id>/<param_id>

		if (locator.getSize() < 3) {
			String msg
					= "The locator is invalid as it does not have at least three path elements (e.g. Resource/MyType/@id): {0}";
			msg = format(msg, locator.toString());
			throw new StrolchModelException(msg);
		}

		List<String> elements = locator.getPathElements();
		StrolchRootElement rootElement;
		String objectClassType = elements.get(0);
		String type = elements.get(1);
		String id = elements.get(2);
		rootElement = switch (objectClassType) {
			case Tags.RESOURCE -> getResourceBy(type, id);
			case Tags.ORDER -> getOrderBy(type, id);
			case Tags.ACTIVITY -> getActivityBy(type, id);
			default -> throw new IllegalStateException("Unexpected object type " + objectClassType);
		};

		if (rootElement == null) {
			if (allowNull)
				return null;
			String msg = "No top level object could be found with locator {0}";
			throw new StrolchModelException(format(msg, locator));
		}

		if (elements.size() == 3)
			return (T) rootElement;

		// state or bag
		String stateOrBagOrActivity = elements.get(3);
		if (stateOrBagOrActivity.equals(Tags.BAG)) {

			String parameterBagId = elements.get(4);
			ParameterBag bag = rootElement.getParameterBag(parameterBagId);
			if (bag == null) {
				if (allowNull)
					return null;
				String msg = "Could not find ParameterBag for locator {0} on element {1}";
				throw new StrolchModelException(format(msg, locator, rootElement.getLocator()));
			}

			if (elements.size() == 5)
				return (T) bag;

			String parameterId = elements.get(5);
			Parameter<?> parameter = bag.getParameter(parameterId);
			if (parameter == null) {
				if (allowNull)
					return null;
				String msg = "Could not find Parameter for locator {0} on element {1}";
				throw new StrolchModelException(format(msg, locator, bag.getLocator()));
			}
			return (T) parameter;

		} else if (stateOrBagOrActivity.equals(Tags.TIMED_STATE)) {

			if (elements.size() != 5) {
				String msg = "Missing state Id on locator {0}";
				throw new StrolchModelException(format(msg, locator));
			}

			Resource resource = rootElement.asResource();
			String stateId = elements.get(4);

			StrolchTimedState<IValue<?>> timedState = resource.getTimedState(stateId);
			return (T) timedState;

		} else if (rootElement instanceof Activity activity) {

			Iterator<String> iter = elements.subList(3, elements.size()).iterator();
			IActivityElement element = activity;
			while (iter.hasNext()) {
				String next = iter.next();

				if (!(element instanceof Activity)) {
					String msg = "Invalid locator {0} with part {1} as not an Activity but deeper element specified";
					throw new StrolchModelException(format(msg, locator, next));
				}

				element = ((Activity) element).getElement(next);
			}

			return (T) element;
		}

		if (allowNull)
			return null;

		String msg = "Invalid locator {0} with part {1}";
		throw new StrolchModelException(format(msg, locator, stateOrBagOrActivity));
	}

	@Override
	public <U, T extends Parameter<U>> Optional<T> findParameterOnHierarchy(StrolchRootElement element,
			String parentParamKey, String paramKey) {
		return findParameterOnHierarchy(element, parentParamKey, BAG_PARAMETERS, paramKey);
	}

	@Override
	public <U, T extends Parameter<U>> Optional<T> findParameterOnHierarchy(StrolchRootElement element,
			String parentParamKey, String bagKey, String paramKey) {

		DBC.PRE.assertNotNull("element must not be null!", element);
		DBC.PRE.assertNotEmpty("parentParamKey must be set", parentParamKey);
		DBC.PRE.assertNotEmpty("bagKey must be set", bagKey);
		DBC.PRE.assertNotEmpty("paramKey must be set", paramKey);

		T t = element.getParameter(bagKey, paramKey);

		Set<StrolchRootElement> parents = new HashSet<>();

		StrolchRootElement parent = element;
		while (t == null) {
			StringParameter parentP = parent.getParameter(BAG_RELATIONS, parentParamKey);
			if (parentP == null || parentP.isEmpty())
				break;

			parent = switch (parentP.getInterpretation()) {
				case INTERPRETATION_RESOURCE_REF -> getResourceBy(parentP);
				case INTERPRETATION_ORDER_REF -> getOrderBy(parentP);
				case INTERPRETATION_ACTIVITY_REF -> getActivityBy(parentP);
				default -> throw new IllegalStateException("Unhandled element ref " + parentP.getInterpretation());
			};

			if (parent == null)
				break;
			if (!parents.add(parent))
				throw new IllegalStateException(
						format("circular dependencies from {0} to {1} on relations parameter {2}", element.getLocator(),
								parent.getLocator(), parentParamKey));

			t = parent.getParameter(bagKey, paramKey);
		}

		return Optional.ofNullable(t);
	}

	private <V> V getElementFromFilter(String key, Locator locator) {
		return this.objectFilter.getElement(key, locator);
	}

	private boolean hasElementInFilter(String key, Locator locator) {
		return this.objectFilter.hasElement(key, locator);
	}

	private Operation getElementOperation(String key, Locator locator) {
		li.strolch.utils.objectfilter.Operation op = this.objectFilter.getOperation(key, locator);
		if (op == null)
			return Operation.GET;

		return switch (op) {
			case ADD -> Operation.ADD;
			case MODIFY -> Operation.UPDATE;
			case REMOVE -> Operation.REMOVE;
		};
	}

	@Override
	public Stream<Resource> streamResources(String... types) {
		return getResourceMap().stream(this, types).map(e -> {
			// perhaps the element was changed before, so we check if it is in the filter first
			Resource element = getElementFromFilter(Tags.RESOURCE, e.getLocator());
			return element == null ? e : element;
		});
	}

	@Override
	public Stream<Order> streamOrders(String... types) {
		return getOrderMap().stream(this, types).map(e -> {
			// perhaps the element was changed before, so we check if it is in the filter first
			Order element = getElementFromFilter(Tags.ORDER, e.getLocator());
			return element == null ? e : element;
		});
	}

	@Override
	public Stream<Activity> streamActivities(String... types) {
		return getActivityMap().stream(this, types).map(e -> {
			// perhaps the element was changed before, so we check if it is in the filter first
			Activity element = getElementFromFilter(Tags.ACTIVITY, e.getLocator());
			return element == null ? e : element;
		});
	}

	@Override
	public Resource getResourceTemplate(String type) {
		return getResourceTemplate(type, false);
	}

	@Override
	public Resource getResourceTemplate(String type, boolean assertExists) throws StrolchException {
		Resource element = getElementFromFilter(Tags.RESOURCE, Resource.locatorFor(TEMPLATE, type));
		if (element != null)
			return element;
		return getResourceMap().getTemplate(this, type, assertExists);
	}

	@Override
	public Resource getConfiguration() {
		Resource configuration = getResourceBy(TYPE_CONFIGURATION, RES_CONFIGURATION, false);
		if (configuration == null) {
			configuration = new Resource(RES_CONFIGURATION, TYPE_CONFIGURATION, TYPE_CONFIGURATION);
			if (!isReadOnly())
				add(configuration);
		}
		return configuration;
	}

	@Override
	public Resource lockAndGetConfiguration() {
		lock(Resource.locatorFor(TYPE_CONFIGURATION, RES_CONFIGURATION));
		return getConfiguration();
	}

	@Override
	public Order getOrderTemplate(String type) {
		return getOrderTemplate(type, false);
	}

	@Override
	public Order getOrderTemplate(String type, boolean assertExists) throws StrolchException {
		Order element = getElementFromFilter(Tags.ORDER, Order.locatorFor(TEMPLATE, type));
		if (element == null)
			element = getOrderMap().getTemplate(this, type, assertExists);
		return element;
	}

	@Override
	public Activity getActivityTemplate(String type) {
		return getActivityTemplate(type, false);
	}

	@Override
	public Activity getActivityTemplate(String type, boolean assertExists) throws StrolchException {
		Activity element = getElementFromFilter(Tags.ACTIVITY, Activity.locatorFor(TEMPLATE, type));
		if (element != null)
			return element;
		return getActivityMap().getTemplate(this, type, assertExists);
	}

	@Override
	public Order getOrderBy(String type, String id) {
		return getOrderBy(type, id, false);
	}

	@Override
	public synchronized Order getOrderBy(String type, String id, boolean assertExists) throws StrolchException {
		Order element = getElementFromFilter(Tags.ORDER, Order.locatorFor(type, id));
		if (element != null)
			return element;

		element = getCachedOrder(type, id);
		if (element != null)
			return element;

		element = getOrderMap().getBy(this, type, id, assertExists);
		if (element != null)
			this.orderCache.addElement(type, id, element);

		return element;
	}

	@Override
	public Order getOrderBy(StringParameter refP) throws StrolchException {
		return getOrderBy(refP, false);
	}

	@Override
	public synchronized Order getOrderBy(StringParameter refP, boolean assertExists) throws StrolchException {
		DBC.PRE.assertNotNull("refP", refP);
		ElementMapHelpers.assertIsRefParam(INTERPRETATION_ORDER_REF, refP);

		if (assertExists && refP.isEmpty()) {
			String msg = "The Order with type \"{0}\" and id \"{1}\" does not exist for param \"{2}\"";
			throw new StrolchException(format(msg, refP.getUom(), refP.getValue(), refP.getLocator()));
		}

		if (refP.isEmpty())
			return null;

		Order element = getElementFromFilter(Tags.ORDER, Order.locatorFor(refP.getUom(), refP.getValue()));
		if (element != null)
			return element;

		element = getCachedOrder(refP.getUom(), refP.getValue());
		if (element != null)
			return element;

		element = getOrderMap().getBy(this, refP, assertExists);
		if (element != null)
			this.orderCache.addElement(refP.getUom(), refP.getValue(), element);

		return element;
	}

	@Override
	public List<Order> getOrdersBy(StringListParameter refP) throws StrolchException {
		return getOrdersBy(refP, false);
	}

	@Override
	public List<Order> getOrdersBy(StringListParameter refP, boolean assertExists) throws StrolchException {
		DBC.PRE.assertNotNull("refP", refP);
		ElementMapHelpers.assertIsRefParam(INTERPRETATION_ORDER_REF, refP);
		if (refP.isEmpty())
			return new ArrayList<>();

		List<Order> elements = new ArrayList<>();
		for (String id : refP.getValue()) {
			Order element = getOrderBy(refP.getUom(), id, assertExists);
			if (element != null)
				elements.add(element);
		}
		return elements;
	}

	@Override
	public Order getOrderByRelation(StrolchRootElement element, String refId) throws StrolchException {
		DBC.PRE.assertNotNull("element", element);
		DBC.PRE.assertNotEmpty("refId", refId);
		StringParameter refP = element.getParameter(BAG_RELATIONS, refId);
		if (refP == null)
			return null;
		return getOrderBy(refP);
	}

	@Override
	public Order getOrderByRelation(StrolchRootElement element, String refId, boolean assertExists)
			throws StrolchException {
		DBC.PRE.assertNotNull("element", element);
		DBC.PRE.assertNotEmpty("refId", refId);
		StringParameter refP = element.getParameter(BAG_RELATIONS, refId, assertExists);
		return getOrderBy(refP, assertExists);
	}

	@Override
	public List<Order> getOrdersByRelation(StrolchRootElement element, String refId) throws StrolchException {
		DBC.PRE.assertNotNull("element", element);
		DBC.PRE.assertNotEmpty("refId", refId);
		StringListParameter refsP = element.getParameter(BAG_RELATIONS, refId);
		if (refsP == null)
			return null;
		return getOrdersBy(refsP);
	}

	@Override
	public List<Order> getOrdersByRelation(StrolchRootElement element, String refId, boolean assertExists)
			throws StrolchException {
		DBC.PRE.assertNotNull("element", element);
		DBC.PRE.assertNotEmpty("refId", refId);
		StringListParameter refsP = element.getParameter(BAG_RELATIONS, refId, assertExists);
		return getOrdersBy(refsP, assertExists);
	}

	@Override
	public Resource getResourceBy(String type, String id) {
		return getResourceBy(type, id, false);
	}

	@Override
	public synchronized Resource getResourceBy(String type, String id, boolean assertExists) throws StrolchException {
		Resource element = getElementFromFilter(Tags.RESOURCE, Resource.locatorFor(type, id));
		if (element != null)
			return element;

		element = getCachedResource(type, id);
		if (element != null)
			return element;

		element = getResourceMap().getBy(this, type, id, assertExists);
		if (element != null)
			this.resourceCache.addElement(type, id, element);

		return element;
	}

	@Override
	public Resource getResourceBy(StringParameter refP) throws StrolchException {
		return getResourceBy(refP, false);
	}

	@Override
	public synchronized Resource getResourceBy(StringParameter refP, boolean assertExists) throws StrolchException {
		DBC.PRE.assertNotNull("refP", refP);
		ElementMapHelpers.assertIsRefParam(INTERPRETATION_RESOURCE_REF, refP);

		if (assertExists && refP.isEmpty()) {
			String msg = "The Resource with type \"{0}\" and id \"{1}\" does not exist for param \"{2}\"";
			throw new StrolchException(format(msg, refP.getUom(), refP.getValue(), refP.getLocator()));
		}

		if (refP.isEmpty())
			return null;

		Resource element = getElementFromFilter(Tags.RESOURCE, Resource.locatorFor(refP.getUom(), refP.getValue()));
		if (element != null)
			return element;

		element = getCachedResource(refP.getUom(), refP.getValue());
		if (element != null)
			return element;

		element = getResourceMap().getBy(this, refP, assertExists);
		if (element != null)
			this.resourceCache.addElement(refP.getUom(), refP.getValue(), element);

		return element;
	}

	@Override
	public List<Resource> getResourcesBy(StringListParameter refP) throws StrolchException {
		return getResourcesBy(refP, false);
	}

	@Override
	public List<Resource> getResourcesBy(StringListParameter refP, boolean assertExists) throws StrolchException {
		DBC.PRE.assertNotNull("refP", refP);
		ElementMapHelpers.assertIsRefParam(INTERPRETATION_RESOURCE_REF, refP);
		if (refP.isEmpty())
			return new ArrayList<>();

		List<Resource> elements = new ArrayList<>();
		for (String id : refP.getValue()) {
			Resource element = getResourceBy(refP.getUom(), id, assertExists);
			if (element != null)
				elements.add(element);
		}
		return elements;
	}

	@Override
	public Resource getResourceByRelation(StrolchRootElement element, String refId) throws StrolchException {
		DBC.PRE.assertNotNull("element", element);
		DBC.PRE.assertNotEmpty("refId", refId);
		StringParameter refP = element.getParameter(BAG_RELATIONS, refId);
		if (refP == null)
			return null;
		return getResourceBy(refP);
	}

	@Override
	public Resource getResourceByRelation(StrolchRootElement element, String refId, boolean assertExists)
			throws StrolchException {
		DBC.PRE.assertNotNull("element", element);
		DBC.PRE.assertNotEmpty("refId", refId);
		StringParameter refP = element.getParameter(BAG_RELATIONS, refId, assertExists);
		if (!assertExists && refP == null)
			return null;
		return getResourceBy(refP, assertExists);
	}

	@Override
	public List<Resource> getResourcesByRelation(StrolchRootElement element, String refId) throws StrolchException {
		DBC.PRE.assertNotNull("element", element);
		DBC.PRE.assertNotEmpty("refId", refId);
		StringListParameter refsP = element.getParameter(BAG_RELATIONS, refId);
		if (refsP == null)
			return null;
		return getResourcesBy(refsP);
	}

	@Override
	public List<Resource> getResourcesByRelation(StrolchRootElement element, String refId, boolean assertExists)
			throws StrolchException {
		DBC.PRE.assertNotNull("element", element);
		DBC.PRE.assertNotEmpty("refId", refId);
		StringListParameter refsP = element.getParameter(BAG_RELATIONS, refId, assertExists);
		if (!assertExists && refsP == null)
			return null;
		return getResourcesBy(refsP, assertExists);
	}

	@Override
	public Resource getResourceFor(Action action) throws StrolchException {
		return getResourceFor(action, false);
	}

	@Override
	public Resource getResourceFor(Action action, boolean assertExists) throws StrolchException {
		DBC.PRE.assertNotEmpty("action.resourceType must be set", action.getResourceType());
		DBC.PRE.assertNotEmpty("action.resourceId must be set", action.getResourceId());
		return getResourceBy(action.getResourceType(), action.getResourceId(), assertExists);
	}

	@Override
	public Activity getActivityBy(String type, String id) {
		return getActivityBy(type, id, false);
	}

	@Override
	public synchronized Activity getActivityBy(String type, String id, boolean assertExists) throws StrolchException {
		Activity element = getElementFromFilter(Tags.ACTIVITY, Activity.locatorFor(type, id));
		if (element != null)
			return element;

		element = getCachedActivity(type, id);
		if (element != null)
			return element;

		element = getActivityMap().getBy(this, type, id, assertExists);
		if (element != null)
			this.activityCache.addElement(type, id, element);

		return element;
	}

	@Override
	public Activity getActivityBy(StringParameter refP) throws StrolchException {
		return getActivityBy(refP, false);
	}

	@Override
	public synchronized Activity getActivityBy(StringParameter refP, boolean assertExists) throws StrolchException {
		DBC.PRE.assertNotNull("refP", refP);
		ElementMapHelpers.assertIsRefParam(INTERPRETATION_ACTIVITY_REF, refP);

		if (assertExists && refP.isEmpty()) {
			String msg = "The Activity with type \"{0}\" and id \"{1}\" does not exist for param \"{2}\"";
			throw new StrolchException(format(msg, refP.getUom(), refP.getValue(), refP.getLocator()));
		}

		if (refP.isEmpty())
			return null;

		Activity element = getElementFromFilter(Tags.ACTIVITY, Activity.locatorFor(refP.getUom(), refP.getValue()));
		if (element != null)
			return element;

		element = getCachedActivity(refP.getUom(), refP.getValue());
		if (element != null)
			return element;

		element = getActivityMap().getBy(this, refP, assertExists);
		if (element != null)
			this.activityCache.addElement(refP.getUom(), refP.getValue(), element);

		return element;
	}

	@Override
	public List<Activity> getActivitiesBy(StringListParameter refP) throws StrolchException {
		return getActivitiesBy(refP, false);
	}

	@Override
	public List<Activity> getActivitiesBy(StringListParameter refP, boolean assertExists) throws StrolchException {
		DBC.PRE.assertNotNull("refP", refP);
		ElementMapHelpers.assertIsRefParam(INTERPRETATION_ACTIVITY_REF, refP);
		if (refP.isEmpty())
			return new ArrayList<>();

		List<Activity> elements = new ArrayList<>();
		for (String id : refP.getValue()) {
			Activity element = getActivityBy(refP.getUom(), id, assertExists);
			if (element != null)
				elements.add(element);
		}
		return elements;
	}

	@Override
	public Activity getActivityByRelation(StrolchRootElement element, String refId) throws StrolchException {
		DBC.PRE.assertNotNull("element", element);
		DBC.PRE.assertNotEmpty("refId", refId);
		StringParameter refP = element.getParameter(BAG_RELATIONS, refId);
		if (refP == null)
			return null;
		return getActivityBy(refP);
	}

	@Override
	public Activity getActivityByRelation(StrolchRootElement element, String refId, boolean assertExists)
			throws StrolchException {
		DBC.PRE.assertNotNull("element", element);
		DBC.PRE.assertNotEmpty("refId", refId);
		StringParameter refP = element.getParameter(BAG_RELATIONS, refId, assertExists);
		return getActivityBy(refP, assertExists);
	}

	@Override
	public List<Activity> getActivitiesByRelation(StrolchRootElement element, String refId) throws StrolchException {
		DBC.PRE.assertNotNull("element", element);
		DBC.PRE.assertNotEmpty("refId", refId);
		StringListParameter refsP = element.getParameter(BAG_RELATIONS, refId);
		if (refsP == null)
			return null;
		return getActivitiesBy(refsP);
	}

	@Override
	public List<Activity> getActivitiesByRelation(StrolchRootElement element, String refId, boolean assertExists)
			throws StrolchException {
		DBC.PRE.assertNotNull("element", element);
		DBC.PRE.assertNotEmpty("refId", refId);
		StringListParameter refsP = element.getParameter(BAG_RELATIONS, refId, assertExists);
		return getActivitiesBy(refsP, assertExists);
	}

	@Override
	public synchronized void removeFromCache(Locator locator) {
		if (locator.getSize() != 3)
			throw new IllegalStateException("Locator is invalid for cache removal: " + locator);
		switch (locator.get(0)) {
			case RESOURCE -> {
				if (this.resourceCache != null)
					this.resourceCache.removeElement(locator.get(1), locator.get(2));
			}
			case ORDER -> {
				if (this.orderCache != null)
					this.orderCache.removeElement(locator.get(1), locator.get(2));
			}
			case ACTIVITY -> {
				if (this.activityCache != null)
					this.activityCache.removeElement(locator.get(1), locator.get(2));
			}
			default -> throw new IllegalStateException("Unexpected object type " + locator.get(0));
		}

		this.objectFilter.removeObjectCache(locator.get(0), locator);
	}

	@Override
	public boolean isResourceCached(String type, String id) {
		return this.resourceCache.containsElement(type, id);
	}

	@Override
	public boolean isOrderCached(String type, String id) {
		return this.orderCache.containsElement(type, id);
	}

	@Override
	public boolean isActivityCached(String type, String id) {
		return this.activityCache.containsElement(type, id);
	}

	@Override
	public Resource getCachedResource(String type, String id) {
		return this.resourceCache.getElement(type, id);
	}

	@Override
	public Order getCachedOrder(String type, String id) {
		return this.orderCache.getElement(type, id);
	}

	@Override
	public Activity getCachedActivity(String type, String id) {
		return this.activityCache.getElement(type, id);
	}

	@Override
	public Stream<Resource> streamCachedResources(String... types) {
		if (types.length == 0)
			return this.resourceCache.streamValues();
		if (types.length == 1) {
			String type = types[0];
			if (!this.resourceCache.containsMap(type))
				return Stream.empty();
			return this.resourceCache.getMap(type).values().stream();
		}
		return this.resourceCache.streamValues().filter(element -> {
			String resType = element.getType();
			for (String type : types) {
				if (resType.equals(type))
					return true;
			}
			return false;
		});
	}

	@Override
	public Stream<Order> streamCachedOrders(String... types) {
		if (types.length == 0)
			return this.orderCache.streamValues();
		if (types.length == 1) {
			String type = types[0];
			if (!this.orderCache.containsMap(type))
				return Stream.empty();
			return this.orderCache.getMap(type).values().stream();
		}
		return this.orderCache.streamValues().filter(element -> {
			String resType = element.getType();
			for (String type : types) {
				if (resType.equals(type))
					return true;
			}
			return false;
		});
	}

	@Override
	public Stream<Activity> streamCachedActivities(String... types) {
		if (types.length == 0)
			return this.activityCache.streamValues();
		if (types.length == 1) {
			String type = types[0];
			if (!this.activityCache.containsMap(type))
				return Stream.empty();
			return this.activityCache.getMap(type).values().stream();
		}
		return this.activityCache.streamValues().filter(element -> {
			String resType = element.getType();
			for (String type : types) {
				if (resType.equals(type))
					return true;
			}
			return false;
		});
	}

	@Override
	public boolean hasResource(String type, String id) {
		boolean inFilter = hasElementInFilter(Tags.RESOURCE, Resource.locatorFor(type, id));
		return inFilter || getResourceMap().hasElement(this, type, id);
	}

	@Override
	public boolean hasOrder(String type, String id) {
		boolean inFilter = hasElementInFilter(Tags.ORDER, Order.locatorFor(type, id));
		return inFilter || getOrderMap().hasElement(this, type, id);
	}

	@Override
	public boolean hasActivity(String type, String id) {
		boolean inFilter = hasElementInFilter(Tags.ACTIVITY, Activity.locatorFor(type, id));
		return inFilter || getActivityMap().hasElement(this, type, id);
	}

	@Override
	public boolean isUpdated(Resource resource) {
		return getElementOperation(Tags.RESOURCE, resource.getLocator()) == Operation.UPDATE;
	}

	@Override
	public boolean isUpdated(Order order) {
		return getElementOperation(Tags.ORDER, order.getLocator()) == Operation.UPDATE;
	}

	@Override
	public boolean isUpdated(Activity activity) {
		return getElementOperation(Tags.ACTIVITY, activity.getLocator()) == Operation.UPDATE;
	}

	@Override
	public boolean isRemoved(Resource resource) {
		return getElementOperation(Tags.RESOURCE, resource.getLocator()) == Operation.REMOVE;
	}

	@Override
	public boolean isRemoved(Order order) {
		return getElementOperation(Tags.ORDER, order.getLocator()) == Operation.REMOVE;
	}

	@Override
	public boolean isRemoved(Activity activity) {
		return getElementOperation(Tags.ACTIVITY, activity.getLocator()) == Operation.REMOVE;
	}

	@Override
	public void assertHasPrivilege(Operation operation, StrolchRootElement element) throws AccessDeniedException {
		DBC.PRE.assertNotNull("operation must not be null", operation);
		DBC.PRE.assertNotNull("element must not be null", element);
		getPrivilegeContext().validateAction(
				new TransactedRestrictable(this, operation.getPrivilegeName(element), element));
	}

	@Override
	public void assertHasPrivilege(String privilegeName, String privilegeValue) throws AccessDeniedException {
		DBC.PRE.assertNotEmpty("privilegeName must not be empty", privilegeValue);
		DBC.PRE.assertNotEmpty("privilegeValue must not be empty", privilegeValue);
		getPrivilegeContext().validateAction(privilegeName, privilegeValue);
	}

	@Override
	public void assertHasPrivilege(Restrictable restrictable) throws AccessDeniedException {
		DBC.PRE.assertNotNull("restrictable must not be null", restrictable);
		getPrivilegeContext().validateAction(restrictable);
	}

	@Override
	public boolean hasPrivilege(String privilegeName, String privilegeValue) throws AccessDeniedException {
		DBC.PRE.assertNotEmpty("privilegeName must not be empty", privilegeValue);
		DBC.PRE.assertNotEmpty("privilegeValue must not be empty", privilegeValue);
		return getPrivilegeContext().hasPrivilege(privilegeName, privilegeValue);
	}

	@Override
	public void assertHasRole(String roleName) throws AccessDeniedException {
		DBC.PRE.assertNotNull("roleName must not be null", roleName);
		getPrivilegeContext().assertHasRole(roleName);
	}

	@Override
	public void assertHasAnyRole(String... roleNames) throws AccessDeniedException {
		DBC.PRE.assertNotNull("roleNames must not be null", roleNames);
		getPrivilegeContext().assertHasAnyRole(roleNames);
	}

	@Override
	public void addOrUpdate(Resource resource) throws StrolchModelException {
		assertNotReadOnly();
		DBC.PRE.assertNotNull("resource must not be null", resource);
		resource.assertNotReadonly();
		if (hasResource(resource.getType(), resource.getId())) {
			li.strolch.utils.objectfilter.Operation operation = this.objectFilter.getOperation(Tags.RESOURCE,
					resource.getLocator());
			if (operation != li.strolch.utils.objectfilter.Operation.REMOVE)
				this.objectFilter.update(Tags.RESOURCE, resource.getLocator(), resource);
			else
				this.objectFilter.add(Tags.RESOURCE, resource.getLocator(), resource);
		} else {
			lock(resource);
			this.objectFilter.add(Tags.RESOURCE, resource.getLocator(), resource);
		}

		this.resourceCache.addElement(resource.getType(), resource.getId(), resource);
	}

	@Override
	public void addOrUpdate(Order order) throws StrolchModelException {
		assertNotReadOnly();
		DBC.PRE.assertNotNull("order must not be null", order);
		order.assertNotReadonly();
		if (hasOrder(order.getType(), order.getId())) {
			li.strolch.utils.objectfilter.Operation operation = this.objectFilter.getOperation(Tags.ORDER,
					order.getLocator());
			if (operation != li.strolch.utils.objectfilter.Operation.REMOVE)
				this.objectFilter.update(Tags.ORDER, order.getLocator(), order);
			else
				this.objectFilter.add(Tags.ORDER, order.getLocator(), order);
		} else {
			lock(order);
			this.objectFilter.add(Tags.ORDER, order.getLocator(), order);
		}

		this.orderCache.addElement(order.getType(), order.getId(), order);
	}

	@Override
	public void addOrUpdate(Activity activity) throws StrolchModelException {
		assertNotReadOnly();
		DBC.PRE.assertNotNull("activity must not be null", activity);
		activity.assertNotReadonly();
		if (hasActivity(activity.getType(), activity.getId())) {
			li.strolch.utils.objectfilter.Operation operation = this.objectFilter.getOperation(Tags.ACTIVITY,
					activity.getLocator());
			if (operation != li.strolch.utils.objectfilter.Operation.REMOVE)
				this.objectFilter.update(Tags.ACTIVITY, activity.getLocator(), activity);
			else
				this.objectFilter.add(Tags.ACTIVITY, activity.getLocator(), activity);
		} else {
			lock(activity);
			this.objectFilter.add(Tags.ACTIVITY, activity.getLocator(), activity);
		}

		this.activityCache.addElement(activity.getType(), activity.getId(), activity);
	}

	@Override
	public void add(Resource resource) throws StrolchModelException {
		assertNotReadOnly();
		DBC.PRE.assertNotNull("resource must not be null", resource);
		resource.assertNotReadonly();
		lock(resource);
		this.objectFilter.add(Tags.RESOURCE, resource.getLocator(), resource);
		this.resourceCache.addElement(resource.getType(), resource.getId(), resource);
	}

	@Override
	public void add(Order order) throws StrolchException {
		assertNotReadOnly();
		DBC.PRE.assertNotNull("order must not be null", order);
		order.assertNotReadonly();
		lock(order);
		this.objectFilter.add(Tags.ORDER, order.getLocator(), order);
		this.orderCache.addElement(order.getType(), order.getId(), order);
	}

	@Override
	public void add(Activity activity) throws StrolchException {
		assertNotReadOnly();
		DBC.PRE.assertNotNull("activity must not be null", activity);
		activity.assertNotReadonly();
		lock(activity);
		this.objectFilter.add(Tags.ACTIVITY, activity.getLocator(), activity);
		this.activityCache.addElement(activity.getType(), activity.getId(), activity);
	}

	@Override
	public void update(Resource resource) throws StrolchException {
		assertNotReadOnly();
		DBC.PRE.assertNotNull("resource must not be null", resource);
		resource.assertNotReadonly();
		this.objectFilter.update(Tags.RESOURCE, resource.getLocator(), resource);
		this.resourceCache.addElement(resource.getType(), resource.getId(), resource);
	}

	@Override
	public void update(Order order) {
		assertNotReadOnly();
		DBC.PRE.assertNotNull("order must not be null", order);
		order.assertNotReadonly();
		this.objectFilter.update(Tags.ORDER, order.getLocator(), order);
		this.orderCache.addElement(order.getType(), order.getId(), order);
	}

	@Override
	public void update(Activity activity) throws StrolchException {
		assertNotReadOnly();
		DBC.PRE.assertNotNull("activity must not be null", activity);
		activity.assertNotReadonly();
		this.objectFilter.update(Tags.ACTIVITY, activity.getLocator(), activity);
		this.activityCache.addElement(activity.getType(), activity.getId(), activity);
	}

	@Override
	public synchronized void remove(Resource resource) throws StrolchException {
		assertNotReadOnly();
		DBC.PRE.assertNotNull("resource must not be null", resource);
		this.objectFilter.remove(Tags.RESOURCE, resource.getLocator(), resource);
		if (this.resourceCache != null) {
			this.resourceCache.removeElement(resource.getType(), resource.getId());
		}
	}

	@Override
	public synchronized void remove(Order order) throws StrolchException {
		assertNotReadOnly();
		DBC.PRE.assertNotNull("order must not be null", order);
		this.objectFilter.remove(Tags.ORDER, order.getLocator(), order);
		if (this.orderCache != null) {
			this.orderCache.removeElement(order.getType(), order.getId());
		}
	}

	@Override
	public synchronized void remove(Activity activity) throws StrolchException {
		assertNotReadOnly();
		DBC.PRE.assertNotNull("activity must not be null", activity);
		this.objectFilter.remove(Tags.ACTIVITY, activity.getLocator(), activity);
		if (this.activityCache != null) {
			this.activityCache.removeElement(activity.getType(), activity.getId());
		}
	}

	private void addModelChangeCommands() {
		if (this.objectFilter.isEmpty())
			return;

		/*
		 * Resources
		 */
		// removed
		List<Resource> changedR = this.objectFilter.getRemoved(Resource.class, Tags.RESOURCE);
		if (changedR.size() == 1) {
			RemoveResourceCommand cmd = new RemoveResourceCommand(this);
			cmd.setResource(changedR.getFirst());
			add(cmd);
		} else if (changedR.size() > 1) {
			RemoveResourcesCommand cmd = new RemoveResourcesCommand(this);
			cmd.setResources(changedR);
			add(cmd);
		}

		// updated
		changedR = this.objectFilter.getUpdated(Resource.class, Tags.RESOURCE);
		if (changedR.size() == 1) {
			UpdateResourceCommand cmd = new UpdateResourceCommand(this);
			cmd.setResource(changedR.getFirst());
			add(cmd);
		} else if (changedR.size() > 1) {
			UpdateResourcesCommand cmd = new UpdateResourcesCommand(this);
			cmd.setResources(changedR);
			add(cmd);
		}

		// added
		changedR = this.objectFilter.getAdded(Resource.class, Tags.RESOURCE);
		if (changedR.size() == 1) {
			AddResourceCommand cmd = new AddResourceCommand(this);
			cmd.setResource(changedR.getFirst());
			add(cmd);
		} else if (changedR.size() > 1) {
			AddResourcesCommand cmd = new AddResourcesCommand(this);
			cmd.setResources(changedR);
			add(cmd);
		}

		/*
		 * Orders
		 */
		// removed
		List<Order> changedO = this.objectFilter.getRemoved(Order.class, Tags.ORDER);
		if (changedO.size() == 1) {
			RemoveOrderCommand cmd = new RemoveOrderCommand(this);
			cmd.setOrder(changedO.getFirst());
			add(cmd);
		} else if (changedO.size() > 1) {
			RemoveOrdersCommand cmd = new RemoveOrdersCommand(this);
			cmd.setOrders(changedO);
			add(cmd);
		}

		// updated
		changedO = this.objectFilter.getUpdated(Order.class, Tags.ORDER);
		if (changedO.size() == 1) {
			UpdateOrderCommand cmd = new UpdateOrderCommand(this);
			cmd.setOrder(changedO.getFirst());
			add(cmd);
		} else if (changedO.size() > 1) {
			UpdateOrdersCommand cmd = new UpdateOrdersCommand(this);
			cmd.setOrders(changedO);
			add(cmd);
		}

		// added
		changedO = this.objectFilter.getAdded(Order.class, Tags.ORDER);
		if (changedO.size() == 1) {
			AddOrderCommand cmd = new AddOrderCommand(this);
			cmd.setOrder(changedO.getFirst());
			add(cmd);
		} else if (changedO.size() > 1) {
			AddOrdersCommand cmd = new AddOrdersCommand(this);
			cmd.setOrders(changedO);
			add(cmd);
		}

		/*
		 * Activities
		 */
		// removed
		List<Activity> changedA = this.objectFilter.getRemoved(Activity.class, Tags.ACTIVITY);
		if (changedA.size() == 1) {
			RemoveActivityCommand cmd = new RemoveActivityCommand(this);
			cmd.setActivity(changedA.getFirst());
			add(cmd);
		} else if (changedA.size() > 1) {
			RemoveActivitiesCommand cmd = new RemoveActivitiesCommand(this);
			cmd.setActivities(changedA);
			add(cmd);
		}

		// updated
		changedA = this.objectFilter.getUpdated(Activity.class, Tags.ACTIVITY);
		if (changedA.size() == 1) {
			UpdateActivityCommand cmd = new UpdateActivityCommand(this);
			cmd.setActivity(changedA.getFirst());
			add(cmd);
		} else if (changedA.size() > 1) {
			UpdateActivitiesCommand cmd = new UpdateActivitiesCommand(this);
			cmd.setActivities(changedA);
			add(cmd);
		}

		// added
		changedA = this.objectFilter.getAdded(Activity.class, Tags.ACTIVITY);
		if (changedA.size() == 1) {
			AddActivityCommand cmd = new AddActivityCommand(this);
			cmd.setActivity(changedA.getFirst());
			add(cmd);
		} else if (changedA.size() > 1) {
			AddActivitiesCommand cmd = new AddActivitiesCommand(this);
			cmd.setActivities(changedA);
			add(cmd);
		}

		if (this.objectFilterStatistics == null) {
			this.objectFilterStatistics = this.objectFilter.toStatistics();
		} else {
			ObjectFilterStatistics statistics = this.objectFilter.toStatistics();
			this.objectFilterStatistics = this.objectFilterStatistics.merge(statistics);
		}

		// clear, so that we don't do it twice in case of a flush()
		this.objectFilter.clearCache();
	}

	@Override
	public synchronized void clearCache() {
		this.orderCache.clear();
		this.resourceCache.clear();
		this.activityCache.clear();
	}

	@Override
	public void reset() {
		clearCache();
		if (this.commands != null)
			this.commands.clear();
		this.objectFilter.clearCache();
		logger.info("Cache, commands and changes have been cleared!");
	}

	@Override
	public void flush() {
		try {
			addModelChangeCommands();
			doCommands();
			writeChanges();
			clearCache();
		} catch (Exception e) {
			this.closeStrategy = TransactionCloseStrategy.ROLLBACK;

			String msg = "Strolch Transaction for realm {0} failed due to {1}";
			msg = format(msg, getRealmName(), getExceptionMessage(e));
			throw new StrolchTransactionException(msg, e);
		}
	}

	@Override
	public void autoCloseableCommit() {
		long start = System.nanoTime();

		try {
			this.txResult.setState(TransactionState.COMMITTING);

			addModelChangeCommands();
			doCommands();

			// do it twice, since some commands might generate new model changes
			if (!this.objectFilter.isEmpty()) {
				addModelChangeCommands();
				doCommands();
			}

			writeChanges();
			long auditTrailDuration = writeAuditTrail();
			commit();
			long updateObserversDuration = updateObservers();

			handleCommit(start, auditTrailDuration, updateObserversDuration);
			this.txResult.setState(TransactionState.COMMITTED);

		} catch (Exception e) {
			this.txResult.setState(TransactionState.ROLLING_BACK);

			try {
				undoCommands();
			} catch (Exception ex) {
				logger.error("Failed to commit transaction and then undo commands due to " + ex.getMessage(), ex);
				try {
					rollback();
					handleRollback(start);
				} catch (Exception exc) {
					logger.error("Failed to roll back after failing to undo commands: " + exc.getMessage(), exc);
				}
				logger.error("Transaction failed due to " + e.getMessage(), e);
				handleFailure(false, start, ex);
			}

			try {
				rollback();
				handleRollback(start);
			} catch (Exception ex) {
				logger.error("Transaction failed due to " + e.getMessage(), e);
				logger.error("Failed to commit transaction and then rollback due to " + ex.getMessage(), ex);
				handleFailure(false, start, e);
			}

			this.txResult.setState(TransactionState.FAILED);

			handleFailure(true, start, e);
		}
	}

	@Override
	public void autoCloseableRollback() {
		long start = System.nanoTime();
		logger.warn(format("Rolling back TX for realm {0}...", getRealmName()));
		try {
			this.txResult.setState(TransactionState.ROLLING_BACK);
			undoCommands();
			rollback();
			handleRollback(start);
			this.txResult.setState(TransactionState.ROLLED_BACK);
		} catch (Exception e) {
			handleFailure(true, start, e);
			this.txResult.setState(TransactionState.FAILED);
		}
	}

	@Override
	public void autoCloseableReadOnly() throws StrolchTransactionException {
		long start = System.nanoTime();
		try {
			this.txResult.setState(TransactionState.CLOSING);

			if (this.closeStrategy != TransactionCloseStrategy.READ_ONLY) {
				if (!this.commands.isEmpty()) {
					autoCloseableRollback();
					String msg
							= "There are commands registered on a read-only transaction. Changing to rollback! Did you forget to commit?";
					throw new IllegalStateException(msg);
				}

				if (!this.objectFilter.isEmpty()) {
					autoCloseableRollback();
					String msg
							= "There are modified objects registered on a read-only transaction. Changing to rollback! Did you forget to commit?";
					throw new IllegalStateException(msg);
				}
			}

			long auditTrailDuration = writeAuditTrail();

			// rollback and release any resources
			rollback();

			handleReadOnly(start, auditTrailDuration);
			this.txResult.setState(TransactionState.CLOSED);

		} catch (Exception e) {
			this.txResult.setState(TransactionState.ROLLING_BACK);
			try {
				undoCommands();
				rollback();
			} catch (Exception e1) {
				logger.error("Failed to rollback after exception", e);
			}
			handleFailure(true, start, e);
			this.txResult.setState(TransactionState.FAILED);
		}
	}

	@Override
	public void close() throws StrolchTransactionException {
		try {
			this.closeStrategy.close(this);
		} finally {
			releaseElementLocks();
			TransactionThreadLocal.removeTx();
		}
	}

	protected abstract void writeChanges() throws Exception;

	protected abstract void rollback() throws Exception;

	protected abstract void commit() throws Exception;

	private void handleReadOnly(long start, long auditTrailDuration) {

		long end = System.nanoTime();
		long txDuration = end - this.txResult.getStartNanos();
		long closeDuration = end - start;

		this.txResult.setTxDuration(txDuration);
		this.txResult.setCloseDuration(closeDuration);

		if (this.silentThreshold > 0L && txDuration < this.silentThreshold)
			return;

		StringBuilder sb = new StringBuilder();
		sb.append("TX user=");
		sb.append(this.certificate.getUsername());

		sb.append(", realm=");
		sb.append(getRealmName());

		sb.append(", took=");
		sb.append(formatNanoDuration(txDuration));

		sb.append(", action=");
		sb.append(this.action);

		if (closeDuration >= 100000000L) {
			sb.append(", close=");
			sb.append(formatNanoDuration(closeDuration));
		}

		if (isAuditTrailEnabled() && auditTrailDuration >= 100000000L) {
			sb.append(", auditTrail=");
			sb.append(formatNanoDuration(auditTrailDuration));
		}

		logger.info(sb.toString());
	}

	private void handleCommit(long start, long auditTrailDuration, long observerUpdateDuration) {

		long end = System.nanoTime();
		long txDuration = end - this.txResult.getStartNanos();
		long closeDuration = end - start;

		this.txResult.setTxDuration(txDuration);
		this.txResult.setCloseDuration(closeDuration);

		if (this.silentThreshold > 0L && txDuration < this.silentThreshold)
			return;

		StringBuilder sb = new StringBuilder();
		sb.append("TX user=");
		sb.append(this.certificate.getUsername());

		sb.append(", realm=");
		sb.append(getRealmName());

		sb.append(", took=");
		sb.append(formatNanoDuration(txDuration));

		sb.append(", action=");
		sb.append(this.action);

		if (closeDuration >= 100000000L) {
			sb.append(", close=");
			sb.append(formatNanoDuration(closeDuration));
		}

		if (isAuditTrailEnabled() && auditTrailDuration >= 100000000L) {
			sb.append(", auditTrail=");
			sb.append(formatNanoDuration(auditTrailDuration));
		}

		if (isObserverUpdatesEnabled() && observerUpdateDuration >= 100000000L) {
			sb.append(", updates=");
			sb.append(formatNanoDuration(observerUpdateDuration));
		}
		logger.info(sb.toString());
	}

	private void handleRollback(long start) {

		long end = System.nanoTime();
		long txDuration = end - this.txResult.getStartNanos();
		long closeDuration = end - start;

		this.txResult.setTxDuration(txDuration);
		this.txResult.setCloseDuration(closeDuration);

		StringBuilder sb = new StringBuilder();
		sb.append("TX ROLLBACK user=");
		sb.append(this.certificate.getUsername());

		sb.append(", realm=");
		sb.append(getRealmName());

		sb.append(" failed=");
		sb.append(formatNanoDuration(txDuration));

		sb.append(", action=");
		sb.append(this.action);

		if (closeDuration >= 100000000L) {
			sb.append(", close=");
			sb.append(formatNanoDuration(closeDuration));
		}
		logger.warn(sb.toString());
	}

	private void handleFailure(boolean throwEx, long closeStartNanos, Exception e) {

		long end = System.nanoTime();
		long txDuration = end - this.txResult.getStartNanos();
		long closeDuration = end - closeStartNanos;

		this.txResult.setState(TransactionState.FAILED);
		this.txResult.setTxDuration(txDuration);
		this.txResult.setCloseDuration(closeDuration);

		StringBuilder sb = new StringBuilder();
		sb.append("TX FAILED user=");
		sb.append(this.certificate.getUsername());

		sb.append(", realm=");
		sb.append(getRealmName());

		sb.append(" failed=");
		sb.append(formatNanoDuration(txDuration));

		sb.append(", action=");
		sb.append(this.action);

		if (closeDuration >= 100000000L) {
			sb.append(", close=");
			sb.append(formatNanoDuration(closeDuration));
		}

		if (this.container.hasComponent(OperationsLog.class)) {
			OperationsLog operationsLog = container.getComponent(OperationsLog.class);
			operationsLog.addMessage(new LogMessage(this.realm.getRealm(), this.certificate.getUsername(),
					Locator.valueOf(AGENT, "tx", this.action, getUniqueId()), LogSeverity.Exception,
					LogMessageState.Information, ResourceBundle.getBundle("strolch-agent"), "agent.tx.failed")
					.withException(e)
					.value("reason", e));
		}

		String msg = "Strolch Transaction for realm {0} failed due to {1}\n{2}";
		msg = format(msg, getRealmName(), getExceptionMessage(e), sb.toString());
		StrolchTransactionException ex = new StrolchTransactionException(msg, e);

		if (throwEx)
			throw ex;
		else
			logger.error("Transaction failed in non-commit mode! Logging TX exception, so exception is not suppressed.",
					ex);
	}

	private boolean isAuditTrailEnabled() {
		return getAuditTrail().isEnabled();
	}

	private long updateObservers() {
		if (!isObserverUpdatesEnabled())
			return 0L;

		long observerUpdateStart = System.nanoTime();

		ObserverEvent event = new ObserverEvent();

		if (this.resourceMap != null) {
			Set<Resource> created = this.resourceMap.getCreated();
			if (!created.isEmpty())
				event.added.addList(Tags.RESOURCE, new ArrayList<>(created));
			Set<Resource> updated = this.resourceMap.getUpdated();
			if (!updated.isEmpty())
				event.updated.addList(Tags.RESOURCE, new ArrayList<>(updated));
			Set<Resource> deleted = this.resourceMap.getDeleted();
			if (!deleted.isEmpty())
				event.removed.addList(Tags.RESOURCE,
						deleted.stream().map(e -> (StrolchRootElement) e.ensureReadOnly()).toList());
		}

		if (this.orderMap != null) {
			Set<Order> created = this.orderMap.getCreated();
			if (!created.isEmpty())
				event.added.addList(Tags.ORDER, new ArrayList<>(created));
			Set<Order> updated = this.orderMap.getUpdated();
			if (!updated.isEmpty())
				event.updated.addList(Tags.ORDER, new ArrayList<>(updated));
			Set<Order> deleted = this.orderMap.getDeleted();
			if (!deleted.isEmpty())
				event.removed.addList(Tags.ORDER,
						deleted.stream().map(e -> (StrolchRootElement) e.ensureReadOnly()).toList());
		}

		if (this.activityMap != null) {
			Set<Activity> created = this.activityMap.getCreated();
			if (!created.isEmpty())
				event.added.addList(Tags.ACTIVITY, new ArrayList<>(created));
			Set<Activity> updated = this.activityMap.getUpdated();
			if (!updated.isEmpty())
				event.updated.addList(Tags.ACTIVITY, new ArrayList<>(updated));
			Set<Activity> deleted = this.activityMap.getDeleted();
			if (!deleted.isEmpty())
				event.removed.addList(Tags.ACTIVITY,
						deleted.stream().map(e -> (StrolchRootElement) e.ensureReadOnly()).toList());
		}

		if (!(event.added.isEmpty() && event.updated.isEmpty() && event.removed.isEmpty())) {
			ObserverHandler observerHandler = this.realm.getObserverHandler();
			observerHandler.notify(event);
		}

		return System.nanoTime() - observerUpdateStart;
	}

	private boolean isObserverUpdatesEnabled() {
		return !this.suppressUpdates && this.realm.isUpdateObservers();
	}

	private long writeAuditTrail() {
		if (!isAuditTrailEnabled())
			return 0L;
		if (isSuppressAudits())
			return 0L;

		long auditTrailStart = System.nanoTime();

		List<Audit> audits = new ArrayList<>();

		if (this.orderMap != null) {
			if (this.realm.isAuditTrailEnabledForRead())
				auditsFor(audits, AccessType.READ, this.orderMap.getRead());
			auditsFor(audits, AccessType.CREATE, this.orderMap.getCreated());
			auditsFor(audits, AccessType.UPDATE, this.orderMap.getUpdated());
			auditsFor(audits, AccessType.DELETE, this.orderMap.getDeleted());
		}

		if (this.resourceMap != null) {
			if (this.realm.isAuditTrailEnabledForRead())
				auditsFor(audits, AccessType.READ, this.resourceMap.getRead());
			auditsFor(audits, AccessType.CREATE, this.resourceMap.getCreated());
			auditsFor(audits, AccessType.UPDATE, this.resourceMap.getUpdated());
			auditsFor(audits, AccessType.DELETE, this.resourceMap.getDeleted());
		}

		if (this.activityMap != null) {
			if (this.realm.isAuditTrailEnabledForRead())
				auditsFor(audits, AccessType.READ, this.activityMap.getRead());
			auditsFor(audits, AccessType.CREATE, this.activityMap.getCreated());
			auditsFor(audits, AccessType.UPDATE, this.activityMap.getUpdated());
			auditsFor(audits, AccessType.DELETE, this.activityMap.getDeleted());
		}

		if (this.auditTrail != null && !isSuppressAuditsForAudits()) {
			if (this.realm.isAuditTrailEnabledForRead())
				auditsForAudits(audits, AccessType.READ, this.auditTrail.getRead());
			auditsForAudits(audits, AccessType.CREATE, this.auditTrail.getCreated());
			auditsForAudits(audits, AccessType.UPDATE, this.auditTrail.getUpdated());
			auditsForAudits(audits, AccessType.DELETE, this.auditTrail.getDeleted());
		}

		if (!audits.isEmpty())
			this.realm.getAuditTrail().addAll(this, audits);

		return System.nanoTime() - auditTrailStart;
	}

	private <T extends StrolchRootElement> void auditsFor(List<Audit> audits, AccessType accessType, Set<T> elements) {
		for (StrolchRootElement element : elements) {
			audits.add(auditFrom(accessType, element));
		}
	}

	private void auditsForAudits(List<Audit> audits, AccessType accessType, Set<Audit> elements) {
		for (Audit element : elements) {
			audits.add(auditFrom(accessType, Tags.AUDIT, StringHelper.DASH, element.getId().toString()));
		}
	}

	@Override
	public Audit auditFrom(AccessType accessType, StrolchRootElement element) {
		String type = element.getObjectType();
		String subType = element.getType();
		String id = element.getId();
		return auditFrom(accessType, type, subType, id);
	}

	@Override
	public Audit auditFrom(AccessType accessType, String elementType, String elementSubType, String id) {
		Audit audit = new Audit();

		audit.setId(StrolchAgent.getUniqueIdLong());
		audit.setUsername(this.certificate.getUsername());
		audit.setFirstname(this.certificate.getFirstname() == null ? this.certificate.getUsername() :
				this.certificate.getFirstname());
		audit.setLastname(this.certificate.getLastname() == null ? this.certificate.getUsername() :
				this.certificate.getLastname());
		audit.setDate(new Date());

		audit.setElementType(elementType);
		audit.setElementSubType(elementSubType);
		audit.setElementAccessed(id);

		// audit.setNewVersion();

		audit.setAction(this.action);
		audit.setAccessType(accessType);

		return audit;
	}

	/**
	 * Calls {@link Command#doCommand()} on all registered commands. This is done after the commands have been validated
	 * so chance of a runtime exception should be small
	 */
	private void doCommands() {

		// keep track of the current list of commands
		List<Command> commands = this.commands;

		// assign a new list for in case a command adds new commands
		this.commands = new ArrayList<>();

		ListIterator<Command> iter = commands.listIterator();
		while (iter.hasNext()) {
			Command command = iter.next();
			command.validate();
			this.flushedCommands.add(command);
			command.doCommand();
			iter.remove();
		}
	}

	/**
	 * Calls {@link Command#undo()} on all registered commands. This is done when an exception is caught while
	 * performing the commands
	 */
	private void undoCommands() {
		ListIterator<Command> iter = this.flushedCommands.listIterator(this.flushedCommands.size());
		while (iter.hasPrevious()) {
			Command command = iter.previous();
			command.undo();
			iter.remove();
		}
	}
}
