com.almende.eve.state.ConcurrentJsonFileState.java Source code

Java tutorial

Introduction

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

Source

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

import java.io.EOFException;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.almende.eve.rpc.jsonrpc.jackson.JOM;
import com.almende.eve.rpc.jsonrpc.jackson.JsonNullAwareDeserializer;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.node.NullNode;

/**
 * The Class ConcurrentJsonFileState.
 * 
 * @class FileState
 * 
 *        A persistent state for an Eve Agent, which stores the data on disk.
 *        Data is stored in the path provided by the configuration file.
 * 
 *        The state provides general information for the agent (about itself,
 *        the environment, and the system configuration), and the agent can
 *        store its state in the state. The state extends a standard Java
 *        Map.
 * 
 *        All operations on this FileState are thread-safe. It also provides two
 *        aditional methods: PutIfNotChanged() and PutAllIfNotChanged().
 * 
 *        Usage:<br>
 *        AgentHost factory = AgentHost.getInstance(config);<br>
 *        ConcurrentFileState state = new
 *        ConcurrentFileState("agentId",".eveagents");<br>
 *        state.put("key", "value");<br>
 *        System.out.println(state.get("key")); // "value"<br>
 * @author jos
 * @author ludo
 */
public class ConcurrentJsonFileState extends AbstractState<JsonNode> {
    private static final Logger LOG = Logger.getLogger("ConcurrentFileState");
    private String filename = null;
    private FileChannel channel = null;
    private FileLock lock = null;
    private InputStream fis = null;
    private OutputStream fos = null;
    private ObjectMapper om = null;
    private static Map<String, Boolean> locked = new ConcurrentHashMap<String, Boolean>();
    private final Map<String, JsonNode> properties = Collections.synchronizedMap(new HashMap<String, JsonNode>());

    /**
     * Instantiates a new concurrent json file state.
     * 
     * @param agentId
     *            the agent id
     * @param filename
     *            the filename
     */
    public ConcurrentJsonFileState(final String agentId, final String filename) {
        super(agentId);
        this.filename = filename;
        om = JOM.getInstance();
    }

    /*
     * (non-Javadoc)
     * 
     * @see java.lang.Object#finalize()
     */
    @Override
    public void finalize() throws Throwable {
        closeFile();
        super.finalize();
    }

    /**
     * Open file.
     * 
     * @throws IOException
     *             Signals that an I/O exception has occurred.
     */
    @SuppressWarnings("resource")
    protected void openFile() throws IOException {
        synchronized (locked) {
            while (locked.containsKey(filename) && locked.get(filename)) {
                try {
                    locked.wait();
                } catch (final InterruptedException e) {
                }
            }
            locked.put(filename, true);

            final File file = new File(filename);
            if (!file.exists()) {
                locked.put(filename, false);
                locked.notifyAll();
                throw new IllegalStateException("Warning: File doesn't exist (anymore):'" + filename + "'");
            }

            channel = new RandomAccessFile(file, "rw").getChannel();
            try {
                // TODO: add support for shared locks, allowing parallel reading
                // operations.
                lock = channel.lock();

            } catch (final Exception e) {
                channel.close();
                channel = null;
                lock = null;
                locked.put(filename, false);
                locked.notifyAll();
                throw new IllegalStateException("error, couldn't obtain file lock on:" + filename, e);
            }
            fis = Channels.newInputStream(channel);
            fos = Channels.newOutputStream(channel);
        }
    }

    /**
     * Close file.
     */
    protected void closeFile() {
        synchronized (locked) {

            if (lock != null && lock.isValid()) {
                try {

                    lock.release();
                } catch (final IOException e) {
                    LOG.log(Level.WARNING, "", e);
                }
            }
            try {
                if (fos != null) {
                    fos.close();
                }
                if (fis != null) {
                    fis.close();
                }

                if (channel != null) {
                    channel.close();
                }

            } catch (final IOException e) {
                LOG.log(Level.WARNING, "", e);
            }
            channel = null;
            fis = null;
            fos = null;
            lock = null;
            locked.put(filename, false);
            locked.notifyAll();
        }
    }

    /**
     * write properties to disk.
     * 
     * @throws IOException
     *             Signals that an I/O exception has occurred.
     */
    private void write() throws IOException {
        if (channel != null) {
            channel.position(0);
        }
        om.writeValue(fos, properties);
        fos.flush();

        if (channel != null) {
            channel.truncate(channel.position());
        }

    }

    /**
     * read properties from disk.
     * 
     * @throws IOException
     *             Signals that an I/O exception has occurred.
     * @throws ClassNotFoundException
     *             the class not found exception
     */
    @JsonDeserialize(using = JsonNullAwareDeserializer.class)
    private void read() throws IOException, ClassNotFoundException {
        try {
            if (channel != null) {
                channel.position(0);
            }
            properties.clear();
            final JsonNode data = om.readTree(fis);
            final Iterator<Entry<String, JsonNode>> fieldIter = data.fields();

            while (fieldIter.hasNext()) {
                final Entry<String, JsonNode> item = fieldIter.next();
                properties.put(item.getKey(), item.getValue());
            }
        } catch (final EOFException eof) {
            // empty file, new agent?
        } catch (final JsonMappingException jme) {
            // empty file, new agent?
        }
    }

    /**
     * init is executed once before the agent method is invoked.
     */
    @Override
    public void init() {
    }

    /**
     * destroy is executed once after the agent method is invoked if the
     * properties are changed, they will be saved.
     */
    @Override
    public void destroy() {
    }

    /*
     * (non-Javadoc)
     * 
     * @see com.almende.eve.state.State#clear()
     */
    @Override
    public synchronized void clear() {
        try {
            openFile();
            final String agentType = properties.get(KEY_AGENT_TYPE).textValue();
            properties.clear();
            properties.put(KEY_AGENT_TYPE, JOM.getInstance().valueToTree(agentType));
            write();
        } catch (final Exception e) {
            LOG.log(Level.WARNING, "", e);
        }
        closeFile();
    }

    /*
     * (non-Javadoc)
     * 
     * @see com.almende.eve.state.State#keySet()
     */
    @Override
    public synchronized Set<String> keySet() {
        Set<String> result = null;
        try {
            openFile();
            read();
            result = new HashSet<String>(properties.keySet());
        } catch (final Exception e) {
            LOG.log(Level.WARNING, "", e);
        }
        closeFile();
        return result;
    }

    /*
     * (non-Javadoc)
     * 
     * @see com.almende.eve.state.State#containsKey(java.lang.String)
     */
    @Override
    public synchronized boolean containsKey(final String key) {
        boolean result = false;
        try {
            openFile();
            read();
            result = properties.containsKey(key);
        } catch (final Exception e) {
            LOG.log(Level.WARNING, "", e);
        }
        closeFile();
        return result;
    }

    /*
     * (non-Javadoc)
     * 
     * @see com.almende.eve.state.AbstractState#get(java.lang.String)
     */
    @Override
    public synchronized JsonNode get(final String key) {
        JsonNode result = NullNode.getInstance();
        boolean open = false;
        try {
            if (properties == null) {
                open = true;
                openFile();
                read();
            }
            result = properties.get(key);
        } catch (final Exception e) {
            LOG.log(Level.WARNING, "", e);
        }
        if (open) {
            closeFile();
        }
        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, JsonNode value) {
        JsonNode result = null;
        try {
            openFile();
            read();
            if (value == null) {
                value = NullNode.getInstance();
            }
            result = properties.put(key, value);
            write();
        } catch (final Exception e) {
            LOG.log(Level.WARNING, "", e);
        }
        closeFile();
        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 {
            openFile();
            read();

            JsonNode cur = NullNode.getInstance();
            if (properties.containsKey(key)) {
                cur = properties.get(key);
            }
            if (oldVal == null) {
                oldVal = NullNode.getInstance();
            }

            // Poor mans 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);
                write();
                result = true;
            }
        } catch (final Exception e) {
            LOG.log(Level.WARNING, "", e);
            // Don't let users loop if exception is thrown. They
            // would get into a deadlock....
            result = true;
        }
        closeFile();
        return result;
    }

    /*
     * (non-Javadoc)
     * 
     * @see com.almende.eve.state.State#remove(java.lang.String)
     */
    @Override
    public synchronized Object remove(final String key) {
        Object result = null;
        try {
            openFile();
            read();
            result = properties.remove(key);

            write();
        } catch (final Exception e) {
            LOG.log(Level.WARNING, "", e);
        }
        closeFile();
        return result;
    }

    /*
     * (non-Javadoc)
     * 
     * @see com.almende.eve.state.State#size()
     */
    @Override
    public synchronized int size() {
        int result = -1;
        try {
            openFile();
            read();
            result = properties.size();

        } catch (final Exception e) {
            LOG.log(Level.WARNING, "", e);
        }
        closeFile();
        return result;
    }

}