com.almende.eve.state.mongo.MongoState.java Source code

Java tutorial

Introduction

Here is the source code for com.almende.eve.state.mongo.MongoState.java

Source

/*
 * Copyright: Almende B.V. (2014), Rotterdam, The Netherlands
 * License: The Apache Software License, Version 2.0
 */
package com.almende.eve.state.mongo;

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.fasterxml.jackson.annotation.JsonIgnore;
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.
 */
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 */
    private Map<String, JsonNode> properties = Collections.synchronizedMap(new HashMap<String, JsonNode>());
    private Long timestamp;

    @JsonIgnore
    private MongoCollection collection;

    /*
     * (non-Javadoc)
     * 
     * @see com.almende.eve.state.AbstractState#getId()
     */
    @Id
    @Override
    public String getId() {
        return super.getId();
    }

    /**
     * Instantiates a new memory state.
     */
    public MongoState() {
        timestamp = System.nanoTime();
    }

    /**
     * Instantiates a new mongo state.
     * 
     * @param id
     *            the state's id
     * @param service
     *            the service
     * @param params
     *            the params
     */
    public MongoState(final String id, final StateService service, final ObjectNode params) {
        super(id, service, params);
        timestamp = System.nanoTime();
    }

    /**
     * Sets the collection.
     * 
     * @param collection
     *            the new collection
     */
    @JsonIgnore
    public void setCollection(final MongoCollection collection) {
        this.collection = collection;
        // assuming this is called only once after creation, simply save the
        // entire state
        collection.save(this);
        collection.ensureIndex("{ _id: 1}");
        collection.ensureIndex("{ _id: 1, timestamp:1 }");
    }

    /**
     * Gets the collection.
     * 
     * @return the collection
     */
    @JsonIgnore
    public MongoCollection getCollection() {
        return collection;
    }

    /**
     * Gets the timestamp.
     * 
     * @return the timestamp
     */
    public Long getTimestamp() {
        return timestamp;
    }

    /*
     * (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;
    }

    /**
     * Esc.
     * 
     * @param key
     *            the key
     * @return the string
     */
    private String esc(final String key) {
        return key.replace('$', '\uFF04').replace('.', '\uFF0E');
    }

    /*
     * (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(esc(key));
            if (result == null) {
                reloadProperties();
                result = properties.get(esc(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(esc(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(esc(key))) {
                cur = properties.get(esc(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(esc(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
     */
    public Map<String, JsonNode> getProperties() {
        return properties;
    }

    /**
     * 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
     */
    public void setProperties(final Map<String, JsonNode> properties) {
        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 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();
        /* write to database */
        final WriteResult result = (force)
                ? collection.update("{_id: #}", getId()).with("{$set: {properties: #, timestamp: #}}", properties,
                        now)
                : collection.update("{_id: #, timestamp: #}", getId(), timestamp)
                        .with("{$set: {properties: #, timestamp: #}}", properties, 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() + " <--- " + properties);
        }
        timestamp = now;
        return updatedExisting;
    }

}