org.rapidcontext.core.storage.RootStorage.java Source code

Java tutorial

Introduction

Here is the source code for org.rapidcontext.core.storage.RootStorage.java

Source

/*
 * RapidContext <http://www.rapidcontext.com/>
 * Copyright (c) 2007-2013 Per Cederberg. All rights reserved.
 *
 * This program is free software: you can redistribute it and/or
 * modify it under the terms of the BSD license.
 *
 * This program 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 RapidContext LICENSE for more details.
 */

package org.rapidcontext.core.storage;

import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Iterator;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.apache.commons.lang.ObjectUtils;
import org.rapidcontext.core.data.Array;
import org.rapidcontext.core.data.Dict;
import org.rapidcontext.core.task.Scheduler;
import org.rapidcontext.core.task.Task;
import org.rapidcontext.core.type.Type;

/**
 * The root storage that provides a unified view of other storages.
 * This class provides a number of unique storage services:
 *
 * <ul>
 *   <li><strong>Mounting</strong> -- Sub-storages can be mounted
 *       on a "storage/..." subpath, providing a global namespace
 *       for objects. Storage paths are automatically converted to
 *       local paths for all storage operations.
 *   <li><strong>Unification</strong> -- Mounted storages can also
 *       be overlaid or unified with the root path, providing a
 *       storage view where objects from all storages are mixed. The
 *       mount order defines which object names take priority, in
 *       case several objects have the same paths.
 *   <li><strong>Object Initialization</strong> -- Dictionary objects
 *       will be inspected upon retrieval from a mounted and unified
 *       path (i.e. not when retrieved directly from the storage).
 *       If a matching type handler or class can be located, the
 *       corresponding object will be created, initialized and cached
 *       for future references.
 * </ul>
 *
 * @author   Per Cederberg
 * @version  1.0
 */
public class RootStorage extends Storage {

    /**
     * The class logger.
     */
    private static final Logger LOG = Logger.getLogger(RootStorage.class.getName());

    /**
     * The number of seconds between each run of the object cache
     * cleaner job.
     */
    private static final int PASSIVATE_INTERVAL_SECS = 30;

    /**
     * The meta-data storage for mount points and parent indices.
     * The mounted storages will be added to this storage under
     * their corresponding mount path (appended to form an object
     * path instead of an index path).
     */
    private MemoryStorage metadata = new MemoryStorage(true, false);

    /**
     * The sorted array of mounted storages. This array is sorted
     * every time a mount point is added or modified.
     */
    private Array mountedStorages = new Array();

    /**
     * The map of cache memory storages. For each mounted and
     * overlaid storage, a corresponding cache storage is created to
     * contain any StorableObject instances. The cache storages are
     * indexed by their parent storage path (not their own actual
     * storage paths).
     */
    private HashMap cacheStorages = new HashMap();

    /**
     * Creates a new root storage.
     *
     * @param readWrite      the read write flag
     */
    public RootStorage(boolean readWrite) {
        super("root", readWrite);
        dict.set("storages", mountedStorages);
        try {
            metadata.store(PATH_STORAGEINFO, dict);
        } catch (StorageException e) {
            LOG.severe("error while initializing virtual storage: " + e.getMessage());
        }
        Task cacheCleaner = new Task("storage cache cleaner") {
            public void execute() {
                cacheClean(false);
            }
        };
        long delay = PASSIVATE_INTERVAL_SECS * 1000L;
        Scheduler.schedule(cacheCleaner, delay, delay);
    }

    /**
     * Returns the storage at a specific storage location. If the
     * exact match flag is set, the path must exactly match the mount
     * point of a storage. If exact matching is not required, the
     * parent storage for the path will be returned. This method will
     * also search for matching cache storages.
     *
     * @param path           the storage location
     * @param exact          the exact match flag
     *
     * @return the storage found, or
     *         null if not found
     */
    private Storage getMountedStorage(Path path, boolean exact) {
        if (path == null) {
            return null;
        } else if (exact) {
            return (Storage) metadata.load(path.child("storage", false));
        } else {
            for (int i = 0; i < mountedStorages.size(); i++) {
                Storage storage = (Storage) mountedStorages.get(i);
                if (path.startsWith(storage.path())) {
                    return storage;
                }
            }
            Iterator iter = cacheStorages.values().iterator();
            while (iter.hasNext()) {
                Storage storage = (Storage) iter.next();
                if (path.startsWith(storage.path())) {
                    return storage;
                }
            }
            return null;
        }
    }

    /**
     * Creates or removes the storage metadata. The metadata needs to
     * be updated with the mount points and mount overlay points in
     * order to create all the intermediate indices.
     *
     * @param storage        the storage to update for
     * @param update         the boolean update or remove flag
     *
     * @throws StorageException if the data couldn't be written
     */
    private void updateStorageMetadata(Storage storage, boolean update) throws StorageException {

        Path path = storage.path().child("storage", false);
        if (update) {
            metadata.store(path, storage);
        } else {
            metadata.remove(path);
        }
        Path overlay = storage.mountOverlayPath();
        if (overlay != null && !overlay.isRoot()) {
            overlay = overlay.child(".", false);
            if (update) {
                metadata.store(overlay, ObjectUtils.NULL);
            } else {
                metadata.remove(overlay);
            }
        }
    }

    /**
     * Creates or removes a cache memory storage for the specified
     * path.
     *
     * @param path           the storage mount path
     * @param overlay        the root overlay path
     *
     * @throws StorageException if the cache couldn't be created or
     *             removed
     */
    private void updateStorageCache(Path path, Path overlay) throws StorageException {

        Path cachePath = Storage.PATH_STORAGE_CACHE.descendant(path.subPath(1));
        MemoryStorage cache = (MemoryStorage) cacheStorages.get(path);
        if (overlay != null && cache == null) {
            cache = new MemoryStorage(true, true);
            cache.setMountInfo(cachePath, true, overlay, 0);
            updateStorageMetadata(cache, true);
            cacheStorages.put(path, cache);
        } else if (overlay == null && cache != null) {
            updateStorageMetadata(cache, false);
            cacheStorages.remove(path);
            cacheRemove(cache, Path.ROOT, false, true);
            cache.destroy();
        }
    }

    /**
     * Mounts a storage to a unique path. The path may not collide
     * with a previously mounted storage, such that it would hide or
     * be hidden by the other storage. Overlapping parent indices
     * will be merged automatically. In addition to adding the
     * storage to the specified path, it's contents may also be
     * overlaid directly on the root path.
     *
     * @param storage        the storage to mount
     * @param path           the mount path
     * @param readWrite      the read write flag
     * @param overlay        the root overlay path
     * @param prio           the root overlay search priority (higher numbers
     *                       are searched before lower numbers)
     *
     * @throws StorageException if the storage couldn't be mounted
     */
    public void mount(Storage storage, Path path, boolean readWrite, Path overlay, int prio)
            throws StorageException {

        String msg;

        if (!path.isIndex()) {
            msg = "cannot mount storage to a non-index path: " + path;
            LOG.warning(msg);
            throw new StorageException(msg);
        } else if (!path.startsWith(PATH_STORAGE)) {
            msg = "cannot mount storage to a non-storage path: " + path;
            LOG.warning(msg);
            throw new StorageException(msg);
        } else if (metadata.lookup(path) != null) {
            msg = "storage mount path conflicts with another mount: " + path;
            LOG.warning(msg);
            throw new StorageException(msg);
        }
        LOG.fine("mounting " + storage);
        storage.setMountInfo(path, readWrite, overlay, prio);
        updateStorageMetadata(storage, true);
        mountedStorages.add(storage);
        mountedStorages.sort();
        updateStorageCache(path, overlay);
    }

    /**
     * Remounts a storage for a unique path. The path or the storage
     * are not modified, but only the mounting options.
     *
     * @param path           the mount path
     * @param readWrite      the read write flag
     * @param overlay        the root overlay path
     * @param prio           the root overlay search priority (higher numbers
     *                       are searched before lower numbers)
     *
     * @throws StorageException if the storage couldn't be remounted
     */
    public void remount(Path path, boolean readWrite, Path overlay, int prio) throws StorageException {

        Storage storage = getMountedStorage(path, true);
        if (storage == null) {
            String msg = "no mounted storage found matching path: " + path;
            LOG.warning(msg);
            throw new StorageException(msg);
        }
        LOG.fine("remounting " + storage);
        updateStorageMetadata(storage, false);
        storage.setMountInfo(storage.path(), readWrite, overlay, prio);
        updateStorageMetadata(storage, true);
        mountedStorages.sort();
        updateStorageCache(path, overlay);
    }

    /**
     * Unmounts a storage from the specified path. The path must have
     * previously been used to mount a storage, which will also be
     * destroyed by this operation.
     *
     * @param path           the mount path
     *
     * @throws StorageException if the storage couldn't be unmounted
     */
    public void unmount(Path path) throws StorageException {
        Storage storage = getMountedStorage(path, true);
        String msg;

        if (storage == null) {
            msg = "no mounted storage found matching path: " + path;
            LOG.warning(msg);
            throw new StorageException(msg);
        }
        LOG.fine("unmounting " + storage);
        updateStorageCache(path, null);
        mountedStorages.remove(mountedStorages.indexOf(storage));
        updateStorageMetadata(storage, false);
        storage.destroy();
    }

    /**
     * Unmounts and destroys all mounted storages.
     */
    public void unmountAll() {
        Storage storage;
        String msg;

        while (mountedStorages.size() > 0) {
            storage = (Storage) mountedStorages.get(mountedStorages.size() - 1);
            try {
                unmount(storage.path());
            } catch (Exception e) {
                msg = "failed to unmount storage at " + storage.path();
                LOG.log(Level.WARNING, msg, e);
            }
        }
    }

    /**
     * Searches for an object at the specified location and returns
     * metadata about the object if found. The path may locate either
     * an index or a specific object.
     *
     * @param path           the storage location
     *
     * @return the metadata for the object, or
     *         null if not found
     */
    public Metadata lookup(Path path) {
        Storage storage = getMountedStorage(path, false);
        Metadata meta = null;

        if (storage != null) {
            meta = storage.lookup(storage.localPath(path));
            return (meta == null) ? null : new Metadata(path, meta);
        } else {
            meta = metadata.lookup(path);
            for (int i = 0; i < mountedStorages.size(); i++) {
                storage = (Storage) mountedStorages.get(i);
                Path overlay = storage.mountOverlayPath();
                if (overlay != null && path.startsWith(overlay)) {
                    Path subpath = path.subPath(overlay.depth());
                    meta = Metadata.merge(meta, lookupObject(storage, subpath));
                }
            }
            return meta;
        }
    }

    /**
     * Searches for an object in a specified storage. The object will
     * be looked up primarily in the cache and thereafter in the
     * actual storage.
     *
     * @param storage        the storage to search in
     * @param path           the storage location
     *
     * @return the metadata for the object, or
     *         null if not found
     */
    private Metadata lookupObject(Storage storage, Path path) {
        MemoryStorage cache;
        Metadata meta = null;

        cache = (MemoryStorage) cacheStorages.get(storage.path());
        if (cache != null) {
            meta = cache.lookup(path);
        }
        return Metadata.merge(meta, storage.lookup(path));
    }

    /**
     * Loads an object from the specified location. The path may
     * locate either an index or a specific object. In case of an
     * index, the data returned is an index dictionary listing of
     * all objects in it.
     *
     * @param path           the storage location
     *
     * @return the data read, or
     *         null if not found
     */
    public Object load(Path path) {
        Storage storage = getMountedStorage(path, false);
        Object res;
        Index idx = null;

        if (storage != null) {
            res = storage.load(storage.localPath(path));
            if (res instanceof Index) {
                idx = (Index) res;
                return new Index(path, idx.indices(), idx.objects());
            } else {
                return res;
            }
        } else {
            res = metadata.load(path);
            if (res instanceof Index) {
                idx = (Index) res;
            } else if (res != null && res != ObjectUtils.NULL) {
                return res;
            }
            for (int i = 0; i < mountedStorages.size(); i++) {
                storage = (Storage) mountedStorages.get(i);
                Path overlay = storage.mountOverlayPath();
                if (overlay != null && path.startsWith(overlay)) {
                    Path subpath = path.subPath(overlay.depth());
                    res = loadObject(storage, subpath);
                    if (res instanceof Index) {
                        idx = Index.merge(idx, (Index) res);
                    } else if (res != null) {
                        return res;
                    }
                }
            }
        }
        return idx;
    }

    /**
     * Loads an object from the specified storage. The storage cache
     * will be used primarily, if it exists. If an object is found in
     * the storage that can be cached, it will be initialized and
     * cached by this method.
     *
     * @param storage        the storage to load from
     * @param path           the storage location
     *
     * @return the data read, or
     *         null if not found
     */
    private Object loadObject(Storage storage, Path path) {
        boolean isCached = cacheStorages.containsKey(storage.path());
        Index idx = null;
        Object res = null;
        String id;

        res = cacheGet(storage.path(), path);
        if (res instanceof Index) {
            idx = (Index) res;
        } else if (res instanceof StorableObject) {
            LOG.fine("loaded cached object " + path + " from " + storage.path());
            return res;
        }
        res = storage.load(path);
        if (isCached && res instanceof Dict) {
            id = path.toIdent(1);
            res = initObject(id, (Dict) res);
            cacheAdd(storage.path(), path, res);
        }
        if (res != null) {
            LOG.fine("loaded " + path + " from " + storage.path() + ": " + res);
        }
        if (idx != null && (res == null || res instanceof Index)) {
            return Index.merge(idx, (Index) res);
        } else {
            return res;
        }
    }

    /**
     * Initializes an object with the corresponding object type (if
     * found).
     *
     * @param id             the object id
     * @param dict           the dictionary data
     *
     * @return the StorableObject instance created, or
     *         the input dictionary if no type matched
     */
    private Object initObject(String id, Dict dict) {
        Constructor constr = Type.constructor(this, dict);
        if (constr != null) {
            String typeId = dict.getString(KEY_TYPE, null);
            Object[] args = new Object[] { id, typeId, dict };
            try {
                StorableObject obj = (StorableObject) constr.newInstance(args);
                obj.init();
                obj.activate();
                return obj;
            } catch (Exception e) {
                String msg = "failed to create instance of " + constr.getClass().getName() + " for object " + id
                        + " of type " + typeId;
                LOG.log(Level.WARNING, msg, e);
                dict.add("_error", msg);
                return dict;
            }
        }
        return dict;
    }

    /**
     * Stores an object at the specified location. The path must
     * locate a particular object or file, since direct manipulation
     * of indices is not supported. Any previous data at the
     * specified path will be overwritten or removed.
     *
     * @param path           the storage location
     * @param data           the data to store
     *
     * @throws StorageException if the data couldn't be written
     */
    public void store(Path path, Object data) throws StorageException {
        Storage storage = getMountedStorage(path, false);
        if (storage != null && path.startsWith(PATH_STORAGE_CACHE)) {
            Path storageId = storage.path().subPath(PATH_STORAGE_CACHE.depth());
            Path storagePath = PATH_STORAGE.descendant(storageId);
            cacheAdd(storagePath, storage.localPath(path), data);
        } else {
            store(storage, path, data, true);
        }
        LOG.fine("stored " + path);
    }

    /**
     * Stores an object at the specified location. The path must
     * locate a particular object or file, since direct manipulation
     * of indices is not supported. Any previous data at the
     * specified path will be overwritten or removed. If the caching
     * flag is not set, no updates will be made to the storage cache.
     *
     * @param storage        the storage to write to (or null)
     * @param path           the storage location
     * @param data           the data to store
     * @param caching        the caching update flag
     *
     * @throws StorageException if the data couldn't be written
     */
    private void store(Storage storage, Path path, Object data, boolean caching) throws StorageException {

        if (storage != null) {
            if (caching) {
                cacheAdd(storage.path(), storage.localPath(path), data);
            }
            storage.store(storage.localPath(path), data);
        } else {
            boolean stored = false;
            for (int i = 0; i < mountedStorages.size(); i++) {
                storage = (Storage) mountedStorages.get(i);
                Path overlay = storage.mountOverlayPath();
                if (overlay != null && path.startsWith(overlay)) {
                    Path subpath = path.subPath(overlay.depth());
                    if (!stored && storage.isReadWrite()) {
                        if (caching) {
                            cacheAdd(storage.path(), subpath, data);
                        }
                        storage.store(subpath, data);
                        stored = true;
                    } else if (caching) {
                        cacheRemove(storage.path(), subpath);
                    }
                }
            }
            if (!stored) {
                throw new StorageException("no writable storage found for " + path);
            }
        }
    }

    /**
     * Removes an object or an index at the specified location. If
     * the path refers to an index, all contained objects and indices
     * will be removed recursively.
     *
     * @param path           the storage location
     *
     * @throws StorageException if the data couldn't be removed
     */
    public void remove(Path path) throws StorageException {
        Storage storage = getMountedStorage(path, false);
        if (storage != null && path.startsWith(PATH_STORAGE_CACHE)) {
            Path storageId = storage.path().subPath(PATH_STORAGE_CACHE.depth());
            Path storagePath = PATH_STORAGE.descendant(storageId);
            cacheRemove(storagePath, storage.localPath(path));
        } else {
            remove(storage, path, true);
        }
        LOG.fine("removed " + path);
    }

    /**
     * Removes an object or an index at the specified location. If
     * the path refers to an index, all contained objects and indices
     * will be removed recursively. If the caching flag is not set,
     * no updates will be made to the storage cache.
     *
     * @param storage        the storage to write to (or null)
     * @param path           the storage location
     * @param caching        the caching update flag
     *
     * @throws StorageException if the data couldn't be removed
     */
    private void remove(Storage storage, Path path, boolean caching) throws StorageException {

        if (storage != null) {
            if (caching) {
                cacheRemove(storage.path(), storage.localPath(path));
            }
            storage.remove(storage.localPath(path));
        } else {
            if (caching) {
                cacheRemove(null, path);
            }
            for (int i = 0; i < mountedStorages.size(); i++) {
                storage = (Storage) mountedStorages.get(i);
                Path overlay = storage.mountOverlayPath();
                boolean isMatch = (overlay != null) && path.startsWith(overlay);
                if (storage.isReadWrite() && isMatch) {
                    Path subpath = path.subPath(overlay.depth());
                    storage.remove(subpath);
                }
            }
        }
    }

    /**
     * Retrieves an object from the cache. If the object is a storable
     * object, it will also be activated.
     *
     * @param storagePath    the storage path being cached
     * @param path           the object location
     *
     * @return the object loaded from the cache, or
     *         null if not found
     */
    private Object cacheGet(Path storagePath, Path path) {
        MemoryStorage cache = (MemoryStorage) cacheStorages.get(storagePath);
        Object res = cache.load(path);
        if (res != null) {
            LOG.fine("cache " + cache.path() + ": loaded " + path);
        }
        if (res instanceof StorableObject) {
            LOG.fine("cache " + cache.path() + ": activating object " + path);
            ((StorableObject) res).activate();
        }
        return res;
    }

    /**
     * Adds an object to a storage cache (if possible). The object
     * will only be added if it is an instance of  StorableObject and
     * a memory cache exists for the specified storage path. Any old
     * object will be removed and destroyed.
     *
     * @param storagePath    the storage path to cache for
     * @param path           the object location
     * @param data           the object to store
     */
    private void cacheAdd(Path storagePath, Path path, Object data) {
        MemoryStorage cache = (MemoryStorage) cacheStorages.get(storagePath);
        if (cache != null) {
            cacheReplace(cache, path, data, cache.load(path));
        }
    }

    /**
     * Removes one or more objects from the storage cache. All the
     * objects removed will be destroyed, but not persisted if
     * modified.
     *
     * @param storagePath    the storage path or null for all
     * @param path           the path to remove
     */
    private void cacheRemove(Path storagePath, Path path) {
        if (storagePath == null) {
            LOG.fine("removing " + path + " from all caches");
            Iterator iter = cacheStorages.keySet().iterator();
            while (iter.hasNext()) {
                cacheRemove((Path) iter.next(), path);
            }
        } else {
            MemoryStorage cache = (MemoryStorage) cacheStorages.get(storagePath);
            if (cache != null) {
                LOG.fine("removing " + path + " from cache " + cache.path());
                cacheRemove(cache, path, false, true);
            }
        }
    }

    /**
     * Removes one or more objects from a storage cache. All objects
     * removed will be either persisted (if modified) and/or removed
     * depending on the store and force removal flags.
     *
     * @param cache          the storage cache to modify
     * @param basePath       the object path to remove
     * @param store          the persist modified objects flag
     * @param force          the forced removal flag
     */
    private void cacheRemove(MemoryStorage cache, Path basePath, boolean store, boolean force) {

        String debugPrefix = "cache " + cache.path() + ": ";
        Metadata[] metas = cache.lookupAll(basePath);
        for (int i = 0; i < metas.length; i++) {
            boolean keepObject = false;
            Path path = metas[i].path();
            Object obj = cache.load(path);
            if (obj instanceof StorableObject) {
                StorableObject storable = (StorableObject) obj;
                if (store && storable.isModified()) {
                    try {
                        LOG.fine(debugPrefix + "persisting modified object " + path);
                        store(null, path, storable, false);
                    } catch (StorageException e) {
                        LOG.log(Level.WARNING, "failed to persist cached object", e);
                    }
                }
                keepObject = !force && storable.isActive();
            }
            if (keepObject) {
                cacheReplace(cache, path, obj, obj);
            } else {
                cacheReplace(cache, path, null, obj);
            }
        }
    }

    /**
     * Replaces a single object in a storage cache. This operation is
     * atomic, meaning that old object are destroyed only after the
     * new object has been inserted into the cache.
     *
     * @param cache          the storage cache to modify
     * @param path           the object path to write
     * @param newData        the new data to insert, or null for none
     * @param oldData        the old data to remove, or null for none
     */
    private void cacheReplace(MemoryStorage cache, Path path, Object newData, Object oldData) {

        String debugPrefix = "cache " + cache.path() + ": ";
        if (newData instanceof StorableObject) {
            StorableObject storable = (StorableObject) newData;
            LOG.fine(debugPrefix + "passivating object " + path);
            storable.passivate();
            if (newData != oldData) {
                try {
                    LOG.fine(debugPrefix + "adding object " + path);
                    cache.store(path, newData);
                } catch (StorageException e) {
                    LOG.log(Level.WARNING, "failed to cache object", e);
                }
            }
        } else if (oldData != null) {
            try {
                LOG.fine(debugPrefix + "removing object " + path);
                cache.remove(path);
            } catch (StorageException e) {
                LOG.log(Level.WARNING, "failed to remove cached data", e);
            }
        }
        if (oldData != newData && oldData instanceof StorableObject) {
            StorableObject storable = (StorableObject) oldData;
            LOG.fine(debugPrefix + "passivating removed object " + path);
            storable.passivate();
            try {
                LOG.fine(debugPrefix + "destroying removed object " + path);
                storable.destroy();
            } catch (StorageException e) {
                LOG.log(Level.WARNING, "failed to destroy object", e);
            }
        }
    }

    /**
     * Destroys all cached objects. The objects will first be
     * passivated and thereafter queried for their status. All
     * modified objects will be stored persistently if possible, but
     * errors will only be logged. If the force clean flag is set,
     * all objects in the cache will be destroyed. Otherwise only
     * inactive objects.<p>
     *
     * This method is called regularly from a background job in
     * order to destroy inactive objects.
     *
     * @param force          the forced clean flag
     */
    public void cacheClean(boolean force) {
        Iterator iter = cacheStorages.values().iterator();
        while (iter.hasNext()) {
            cacheRemove((MemoryStorage) iter.next(), Path.ROOT, true, force);
        }
    }
}