/** * Copyright (C) 2010-2017 Structr GmbH * * This file is part of Structr <http://structr.org>. * * Structr is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * Structr 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Structr. If not, see <http://www.gnu.org/licenses/>. */ package org.structr.net.repository; import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.SortedMap; import java.util.concurrent.ConcurrentSkipListMap; import org.structr.net.data.time.PseudoTime; /** * */ public class DefaultRepositoryObject implements RepositoryObject { private final SortedMap<PseudoTime, PossibleValue> history = new ConcurrentSkipListMap<>(); private final List<ObjectListener> listeners = new LinkedList<>(); private DefaultRepository parent = null; private PseudoTime creationTime = null; private PseudoTime deletionTime = null; private String deviceId = null; private String userId = null; private String type = null; private String uuid = null; public DefaultRepositoryObject(final DefaultRepository parent, final String uuid, final String type, final String deviceId, final String userId, final PseudoTime creationTime) { this.creationTime = creationTime; this.parent = parent; this.deviceId = deviceId; this.userId = userId; this.type = type; this.uuid = uuid; } @Override public int hashCode() { return uuid.hashCode(); } @Override public boolean equals(final Object other) { if (other instanceof RepositoryObject) { return hashCode() == other.hashCode(); } return false; } @Override public String getUuid() { return uuid; } @Override public String getType() { return type; } @Override public String getDeviceId() { return deviceId; } @Override public String getUserId() { return userId; } @Override public PseudoTime getCreationTime() { return creationTime; } @Override public PseudoTime getLastModificationTime() { if (!history.isEmpty()) { return history.lastKey(); } return creationTime; } public void printHistory() { for (final PseudoTime time : history.keySet()) { System.out.println(" " + time + ": " + history.get(time)); } if (!history.isEmpty()) { System.out.println(" " + history.firstKey() + ": " + getProperties(history.firstKey())); System.out.println(" " + history.lastKey() + ": " + getProperties(history.lastKey())); } } @Override public void setProperty(final PseudoTime instant, final String transactionId, final String key, final Object value) { final Object existingValue = getProperty(instant, transactionId, key); if (existingValue != null && existingValue.equals(value)) { return; } final PossibleValue possibleValue = history.get(instant); if (possibleValue != null) { if (possibleValue.hasValue(key)) { System.out.println("###### instant: " + instant.toString()); System.out.println("REDEFINE: " + uuid + ": " + key + " = " + value + ", NOT set! " + instant + ": " + history.get(instant).getData()); // redefinition of existing value! } else { // redefinition solved // TODO: check existing values from previous lookups (educing a value) possibleValue.set(key, value); } } else { final DefaultPossibility poss = parent.getPossibility(transactionId); if (poss != null) { poss.addObject(this); } history.put(instant, new PossibleValue(transactionId, key, value)); } } public Object getProperty(final PseudoTime instant, final String key) { return getProperty(instant, null, key); } @Override public Object getProperty(final PseudoTime instant, final String transactionId, final String key) { PossibleValue value = history.get(instant); // optimistic best cas first if (value != null && value.isComplete(transactionId) && value.hasValue(key)) { return value.get(key); } else { // do backwards search final List<PseudoTime> times = new LinkedList<>(history.keySet()); Collections.reverse(times); for (final PseudoTime time : times) { if (time.equals(instant) || time.before(instant)) { final PossibleValue val = history.get(time); if (val.isComplete(transactionId) && val.hasValue(key)) { return val.get(key); } else if (val.isAborted()) { history.remove(time); } } } } // nothing found, no values committed yet return null; } @Override public Map<String, Object> getProperties(final PseudoTime instant) { return getProperties(instant, null); } @Override public Map<String, Object> getProperties(final PseudoTime instant, final String transactionId) { final Map<String, Object> map = new HashMap<>(); // do backwards search final List<PseudoTime> times = new LinkedList<>(history.keySet()); Collections.reverse(times); for (final PseudoTime time : times) { if (time.equals(instant) || time.before(instant)) { final PossibleValue val = history.get(time); if (val.isComplete(transactionId)) { for (final Entry<String, Object> entry : val.getData().entrySet()) { if (!map.containsKey(entry.getKey())) { map.put(entry.getKey(), entry.getValue()); } } } else if (val.isAborted()) { // remove aborted history entries history.remove(time); } } } return map; } @Override public void onCommit(final String transactionId) { final List<PossibleValue> values = new LinkedList<>(); for (final PossibleValue val : history.values()) { if (transactionId != null && transactionId.equals(val.getTransactionId())) { values.add(val); } } // notify listeners for (final ObjectListener listener : listeners) { for (final PossibleValue value : values) { for (final Entry<String, Object> entry : value.getData().entrySet()) { listener.onPropertyChange(this, entry.getKey(), entry.getValue()); } } } } @Override public void addListener(ObjectListener listener) { listeners.add(listener); } @Override public void removeListener(ObjectListener listener) { listeners.remove(listener); } // ----- nested classes ----- private class PossibleValue { private Map<String, Object> data = new HashMap<>(); private String transactionId = null; public PossibleValue(final String transactionId, final String key, final Object value) { this.transactionId = transactionId; data.put(key, value); } @Override public String toString() { return transactionId; } public String getTransactionId() { return transactionId; } public boolean isComplete(final String otherTransactionId) { // static possibility if (transactionId == null) { return true; } // the same possibility will always get the "new" valuie if (transactionId.equals(otherTransactionId)) { return true; } final DefaultPossibility p = parent.getPossibility(transactionId); if (p != null) { return p.isComplete(); } return false; } public boolean isAborted() { // static possibility if (transactionId == null) { return false; } final DefaultPossibility p = parent.getPossibility(transactionId); if (p != null) { return p.isAborted(); } return false; } public boolean hasValue(final String key) { return data.containsKey(key); } public Object get(final String key) { return data.get(key); } public void set(final String key, final Object value) { data.put(key, value); } public Map<String, Object> getData() { return data; } } }