/*
* Copyright: Almende B.V. (2014), Rotterdam, The Netherlands
* License: The Apache Software License, Version 2.0
*/
package com.almende.eve.state.mongo;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.jongo.MongoCollection;
import org.jongo.marshall.jackson.oid.Id;
import com.almende.eve.state.AbstractState;
import com.almende.eve.state.State;
import com.almende.eve.state.StateService;
import com.almende.eve.state.mongo.MongoStateBuilder.MongoStateProvider;
import com.almende.util.jackson.JOM;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.NullNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.mongodb.MongoException;
import com.mongodb.WriteResult;
/**
* The Class MongoState.
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public class MongoState extends AbstractState<JsonNode> implements State {
/**
* internal exception signifying update conflict.
*
* @author ronny
*/
class UpdateConflictException extends Exception {
/**
*
*/
private static final long serialVersionUID = -8714877645567521282L;
/**
* timestamp of last update
*/
private final Long timestamp;
/**
* default constructor for class specific exception.
*
* @param timestamp
* the timestamp
*/
public UpdateConflictException(final Long timestamp) {
this.timestamp = timestamp;
}
/*
* (non-Javadoc)
* @see java.lang.Throwable#getMessage()
*/
@Override
public String getMessage() {
return "Document updated on [" + timestamp
+ "] is no longer the latest version.";
}
}
private static final Logger LOG = Logger.getLogger("MongoState");
/* mapping object that contains variables used by the agent */
@JsonIgnore
private Map<String, JsonNode> properties = Collections
.synchronizedMap(new HashMap<String, JsonNode>());
@JsonProperty("properties")
private String propertiesJson = null;
private Long timestamp;
@JsonIgnore
private MongoStateProvider provider = null;
/*
* (non-Javadoc)
* @see com.almende.eve.state.AbstractState#getId()
*/
@Id
@Override
public String getId() {
return super.getId();
}
/**
* Default constructor
*/
public MongoState() {
timestamp = System.nanoTime();
}
/**
* Instantiates a new mongo state.
*
* @param id
* the state's id
* @param mongoStateProvider
* the service
* @param params
* the params
*/
public MongoState(final String id,
final MongoStateProvider mongoStateProvider, final ObjectNode params) {
super(id, mongoStateProvider, params);
provider = mongoStateProvider;
timestamp = System.nanoTime();
}
/**
* Gets the timestamp.
*
* @return the timestamp
*/
public Long getTimestamp() {
return timestamp;
}
@Override
public StateService getService() {
return provider;
}
@Override
public void setService(StateService service) {
if (service instanceof MongoStateProvider) {
provider = (MongoStateProvider) service;
}
}
/*
* (non-Javadoc)
* @see com.almende.eve.state.State#remove(java.lang.String)
*/
@Override
public synchronized Object remove(final String key) {
Object result = null;
try {
result = properties.remove(key);
updateProperties(false);
} catch (final Exception e) {
LOG.log(Level.WARNING, "remove error", e);
}
return result;
}
/*
* (non-Javadoc)
* @see com.almende.eve.state.State#containsKey(java.lang.String)
*/
@Override
public boolean containsKey(final String key) {
boolean result = false;
try {
result = properties.containsKey(key);
} catch (final Exception e) {
LOG.log(Level.WARNING, "containsKey error", e);
}
return result;
}
/*
* (non-Javadoc)
* @see com.almende.eve.state.State#keySet()
*/
@Override
public Set<String> keySet() {
Set<String> result = null;
try {
result = properties.keySet();
} catch (final Exception e) {
LOG.log(Level.WARNING, "keySet error", e);
}
return result;
}
/*
* (non-Javadoc)
* @see com.almende.eve.state.State#clear()
*/
@Override
public void clear() {
try {
properties.clear();
updateProperties(true);
} catch (final Exception e) {
LOG.log(Level.WARNING, "clear error", e);
}
}
/*
* (non-Javadoc)
* @see com.almende.eve.state.State#size()
*/
@Override
public int size() {
int result = 0;
try {
result = properties.size();
} catch (final Exception e) {
LOG.log(Level.WARNING, "size error", e);
}
return result;
}
/*
* (non-Javadoc)
* @see com.almende.eve.state.AbstractState#get(java.lang.String)
*/
@Override
@JsonIgnore
public JsonNode get(final String key) {
JsonNode result = null;
try {
result = properties.get(key);
if (result == null) {
reloadProperties();
result = properties.get(key);
}
} catch (final Exception e) {
LOG.log(Level.WARNING, "get error", e);
}
return result;
}
/*
* (non-Javadoc)
* @see com.almende.eve.state.AbstractState#locPut(java.lang.String,
* com.fasterxml.jackson.databind.JsonNode)
*/
@Override
public synchronized JsonNode locPut(final String key, final JsonNode value) {
JsonNode result = null;
try {
result = properties.put(key, value);
updateProperties(false);
} catch (final UpdateConflictException e) {
LOG.log(Level.WARNING, e.getMessage() + " Adding [" + key + "="
+ value + "]");
reloadProperties();
// go recursive if update conflict occurs
result = locPut(key, value);
} catch (final Exception e) {
LOG.log(Level.WARNING, "locPut error: Adding [" + key + "=" + value
+ "] " + properties, e);
}
return result;
}
/*
* (non-Javadoc)
* @see
* com.almende.eve.state.AbstractState#locPutIfUnchanged(java.lang.String,
* com.fasterxml.jackson.databind.JsonNode,
* com.fasterxml.jackson.databind.JsonNode)
*/
@Override
public synchronized boolean locPutIfUnchanged(final String key,
final JsonNode newVal, JsonNode oldVal) {
boolean result = false;
try {
JsonNode cur = NullNode.getInstance();
if (properties.containsKey(key)) {
cur = properties.get(key);
}
if (oldVal == null) {
oldVal = NullNode.getInstance();
}
// Poor man's equality as some Numbers are compared incorrectly:
// e.g.
// IntNode versus LongNode
if (oldVal.equals(cur) || oldVal.toString().equals(cur.toString())) {
properties.put(key, newVal);
result = updateProperties(false);
}
} catch (final UpdateConflictException e) {
LOG.log(Level.WARNING, e.getMessage());
reloadProperties();
// retry if update conflict occurs
locPutIfUnchanged(key, newVal, oldVal);
} catch (final Exception e) {
LOG.log(Level.WARNING, "locPutIfUnchanged error", e);
}
return result;
}
/**
* returns agent properties as a mapped collection of JSON nodes.
*
* @return the properties
*/
@JsonIgnore
public Map<String, JsonNode> getProperties() {
return properties;
}
/**
* Gets the serialized version of {@link MongoState#properties}.
*
* @return the properties json
*/
@SuppressWarnings("javadoc")
@JsonProperty("properties")
public String getPropertiesJSON() {
if (properties != null) {
try {
propertiesJson = JOM.getInstance().writeValueAsString(
properties);
return propertiesJson;
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
return "{}";
}
/**
* Sets the properties.
*
* @param propertiesJson
* the new properties
*/
@JsonProperty("properties")
public void setProperties(String propertiesJson) {
try {
if (propertiesJson != null) {
this.propertiesJson = propertiesJson;
properties = JOM.getInstance().readValue(propertiesJson,
new TypeReference<Map<String, JsonNode>>() {});
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* set all property values from a collection. (Warning, keys in the
* properties map should be MongoDB safe: no '$', nor '.' in the keys!)
*
* @param properties
* the properties
* @throws JsonProcessingException
* the json processing exception
*/
@JsonIgnore
public void setProperties(final Map<String, JsonNode> properties)
throws JsonProcessingException {
this.properties.clear();
this.properties.putAll(properties);
try {
updateProperties(true);
} catch (final UpdateConflictException e) {
// should never happen
LOG.log(Level.WARNING, "setProperties error", e);
}
}
/**
* Refreshes the state according to the latest version as a preceding step
* before recursive call.
* With this mechanism, changes on different State property will be safely
* merged. However, with
* interwoven execution order among multiple threads, multiple updates on
* the same State property
* is not guaranteed to be executed properly.
*/
private synchronized void reloadProperties() {
final MongoCollection collection = provider.getInstance();
final MongoState updatedState = collection.findOne("{_id: #}", getId())
.as(MongoState.class);
if (updatedState != null) {
timestamp = updatedState.timestamp;
properties = updatedState.properties;
} else {
properties = Collections
.synchronizedMap(new HashMap<String, JsonNode>());
timestamp = System.nanoTime();
}
}
/**
* updating the entire properties object at the same time, with force flag
* to allow overwriting of updates
* from other instances of the state
*
* @param force
* @throws UpdateConflictException
* | will not throw anything when $force flag is true
*/
private synchronized boolean updateProperties(final boolean force)
throws UpdateConflictException {
final Long now = System.nanoTime();
propertiesJson = getPropertiesJSON();
final MongoCollection collection = provider.getInstance();
/* write to database */
final WriteResult result = (force) ? collection.update("{_id: #}",
getId()).with("{$set: {properties: #, timestamp: #}}",
propertiesJson, now) : collection.update(
"{_id: #, timestamp: #}", getId(), timestamp).with(
"{$set: {properties: #, timestamp: #}}", propertiesJson, now);
/* check results */
final Boolean updatedExisting = (Boolean) result
.getField("updatedExisting");
if (result.getN() == 0 && result.getError() == null) {
throw new UpdateConflictException(timestamp);
} else if (result.getN() != 1) {
throw new MongoException(result.getError() + " <--- "
+ propertiesJson);
}
timestamp = now;
return updatedExisting;
}
}