/*
* Copyright (c) 2011-2014 Jeppetto and Jonathan Thompson
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.iternine.jeppetto.dao.mongodb.enhance;
import org.iternine.jeppetto.dao.JeppettoException;
import com.mongodb.DBCollection;
import org.bson.BSONObject;
import java.util.AbstractSet;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
/**
* Despite the lack of explicit generic types, this acts like a Map<String, Object> due to the way
* MongoDB expects the keys to be expressed. ClassCastExceptions may occur if this is violated.
*/
@SuppressWarnings({ "unchecked" })
public class DirtyableDBObjectMap
implements Map, DirtyableDBObject {
//-------------------------------------------------------------
// Variables - Private
//-------------------------------------------------------------
private Map<String, Object> delegate;
private Set<String> addedOrUpdatedKeys = new HashSet<String>();
private Set<String> removedKeys = new HashSet<String>();
private DBCollection persistentCollection;
//-------------------------------------------------------------
// Constructors
//-------------------------------------------------------------
public DirtyableDBObjectMap() {
this(new HashMap<String, Object>());
}
public DirtyableDBObjectMap(Map<String, Object> delegate) {
this.delegate = delegate;
}
//-------------------------------------------------------------
// Implementation - Map
//-------------------------------------------------------------
@Override
public Object put(Object key, Object value) {
String stringKey = (String) key;
addedOrUpdatedKeys.add(stringKey);
if (removedKeys.size() > 0) {
removedKeys.remove(stringKey);
}
return delegate.put(stringKey, value);
}
@Override
public Object remove(Object key) {
String stringKey = (String) key;
Object result = delegate.remove(stringKey);
if (result != null) {
removedKeys.add(stringKey);
if (addedOrUpdatedKeys.size() > 0) {
addedOrUpdatedKeys.remove(stringKey);
}
}
return result;
}
@Override
public void putAll(Map m) {
for (Object o : m.entrySet()) {
Entry entry = (Entry) o;
put(entry.getKey(), entry.getValue());
}
}
@Override
public void clear() {
removedKeys.addAll(delegate.keySet());
addedOrUpdatedKeys.clear();
delegate.clear();
}
@Override
public int size() {
return delegate.size();
}
@Override
public boolean isEmpty() {
return delegate.isEmpty();
}
@Override
public boolean containsKey(Object key) {
return delegate.containsKey(key);
}
@Override
public boolean containsValue(Object value) {
return delegate.containsValue(value);
}
@Override
public Object get(Object key) {
Object value = delegate.get(key);
if (value == null || value instanceof DirtyableDBObject || DBObjectUtil.needsNoConversion(value.getClass())) {
return value;
}
// TODO: revisit whether these semantics makes sense
Object converted = DBObjectUtil.toDBObject(value);
delegate.put((String) key, converted);
return converted;
}
@Override
public Set keySet() {
// TODO: allow key set to be modified
return Collections.unmodifiableSet(delegate.keySet());
}
@Override
public Collection values() {
// TODO: allow values to be modified
return Collections.unmodifiableCollection(delegate.values());
}
@Override
public Set entrySet() {
return new AbstractSet() {
@Override
public Iterator iterator() {
return new Iterator() {
private Iterator<Entry<String, Object>> delegateIterator = delegate.entrySet().iterator();
private String currentKey;
@Override
public boolean hasNext() {
return delegateIterator.hasNext();
}
@Override
public Object next() {
Entry<String, Object> current = delegateIterator.next();
this.currentKey = current.getKey();
final Object currentValue;
if (current.getValue() instanceof DirtyableDBObject
|| current.getValue() == null
|| DBObjectUtil.needsNoConversion(current.getValue().getClass())) {
currentValue = current.getValue();
} else {
currentValue = DBObjectUtil.toDBObject(current.getValue());
delegate.put(currentKey, currentValue);
}
return new Entry<String, Object>() {
@Override
public String getKey() {
return currentKey;
}
@Override
public Object getValue() {
return currentValue;
}
@Override
public Object setValue(Object value) {
throw new UnsupportedOperationException();
}
};
}
@Override
public void remove() {
DirtyableDBObjectMap.this.remove(currentKey);
}
};
}
@Override
public int size() {
return delegate.size();
}
@Override
public boolean remove(Object o) {
return DirtyableDBObjectMap.this.remove(o) != null;
}
@Override
public void clear() {
DirtyableDBObjectMap.this.clear();
}
};
}
//-------------------------------------------------------------
// Implementation - DirtyableDBObject
//-------------------------------------------------------------
@Override
public boolean isDirty() {
return !addedOrUpdatedKeys.isEmpty() || !removedKeys.isEmpty() || getDirtyKeys().hasNext();
}
@Override
public void markPersisted(DBCollection dbCollection) {
addedOrUpdatedKeys.clear();
removedKeys.clear();
for (Object o : delegate.entrySet()) {
Entry entry = (Entry) o;
//noinspection SuspiciousMethodCalls
if (addedOrUpdatedKeys.contains(entry.getKey())) {
continue;
}
if (entry.getValue() instanceof DirtyableDBObject) {
DirtyableDBObject dirtyableDBObject = (DirtyableDBObject) entry.getValue();
dirtyableDBObject.markPersisted(dbCollection);
}
}
persistentCollection = dbCollection;
}
@Override
public boolean isPersisted(DBCollection dbCollection) {
return dbCollection.equals(persistentCollection);
}
@Override
public Iterator<String> getDirtyKeys() {
return new Iterator<String>() {
private Iterator<Entry<String, Object>> entries = entrySet().iterator();
private Entry<String, Object> entry;
@Override
public boolean hasNext() {
while (entries.hasNext()) {
entry = entries.next();
// At this point, every value in the map is either a DirtyableDBObject or an immutable
// type (such as String). The exception is the byte[] type, which doesn't get converted and
// we don't know if it changed, so we'll assume it's dirty.
if (addedOrUpdatedKeys.contains(entry.getKey())
|| (entry.getValue() instanceof DirtyableDBObject
&& (((DirtyableDBObject) entry.getValue()).isDirty()
|| !((DirtyableDBObject) entry.getValue()).isPersisted(persistentCollection)))
|| entry.getValue() instanceof byte[]) {
return true;
}
}
return false;
}
@Override
public String next() {
return entry.getKey();
}
@Override
public void remove() {
throw new JeppettoException("Can't remove items from dirtyKeys");
}
};
}
@Override
public Object getDelegate() {
return delegate;
}
//-------------------------------------------------------------
// Implementation - DBObject
//-------------------------------------------------------------
@Override
public void markAsPartialObject() {
throw new JeppettoException("Can't mark DirtyableDBObjectMap as partial");
}
@Override
public boolean isPartialObject() {
return false;
}
@Override
public Object put(String key, Object value) {
return delegate.put(key, value);
}
@Override
public void putAll(BSONObject bsonObject) {
for (Object o : bsonObject.toMap().entrySet()) {
Entry entry = (Entry) o;
delegate.put((String) entry.getKey(), entry.getValue());
}
}
@Override
public Object get(String key) {
return get((Object) key);
}
@Override
public Map toMap() {
return this;
}
@Override
public Object removeField(String key) {
return remove(key);
}
@Override
public boolean containsKey(String key) {
return delegate.containsKey(key);
}
@Override
public boolean containsField(String field) {
return delegate.containsKey(field);
}
//-------------------------------------------------------------
// Methods - Public
//-------------------------------------------------------------
public Set getRemovedKeys() {
return removedKeys;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Map)) {
return false;
}
Map thatMap = o instanceof DirtyableDBObjectMap ? ((DirtyableDBObjectMap) o).delegate : (Map) o;
return delegate.equals(thatMap);
}
@Override
public int hashCode() {
return delegate.hashCode();
}
}