ch.entwine.weblounge.contentrepository.impl.index.ContentRepositoryIndex.java Source code

Java tutorial

Introduction

Here is the source code for ch.entwine.weblounge.contentrepository.impl.index.ContentRepositoryIndex.java

Source

/*
 *  Weblounge: Web Content Management System
 *  Copyright (c) 2003 - 2011 The Weblounge Team
 *  http://entwinemedia.com/weblounge
 *
 *  This program is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public License
 *  as published by the Free Software Foundation; either version 2
 *  of the License, or (at your option) any later version.
 *
 *  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
 *  GNU Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public License
 *  along with this program; if not, write to the Free Software Foundation
 *  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */

package ch.entwine.weblounge.contentrepository.impl.index;

import static ch.entwine.weblounge.contentrepository.impl.index.IndexSchema.PATH;
import static ch.entwine.weblounge.contentrepository.impl.index.IndexSchema.RESOURCE_ID;
import static ch.entwine.weblounge.contentrepository.impl.index.IndexSchema.TYPE;
import static ch.entwine.weblounge.contentrepository.impl.index.IndexSchema.VERSION;

import ch.entwine.weblounge.common.content.Resource;
import ch.entwine.weblounge.common.content.ResourceSearchResultItem;
import ch.entwine.weblounge.common.content.ResourceURI;
import ch.entwine.weblounge.common.content.SearchQuery;
import ch.entwine.weblounge.common.content.SearchResult;
import ch.entwine.weblounge.common.content.SearchResultItem;
import ch.entwine.weblounge.common.impl.content.ResourceURIImpl;
import ch.entwine.weblounge.common.impl.content.SearchQueryImpl;
import ch.entwine.weblounge.common.repository.ContentRepositoryException;
import ch.entwine.weblounge.common.repository.ResourceSerializerService;
import ch.entwine.weblounge.common.site.Site;

import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.util.Iterator;
import java.util.List;
import java.util.UUID;

/**
 * This index into the repository is used to map resource and resource urls into
 * repository indices and vice versa. In addition, it will facilitate listing
 * url hierarchies.
 */
public class ContentRepositoryIndex {

    /** Logging facility */
    private static final Logger logger = LoggerFactory.getLogger(ContentRepositoryIndex.class);

    /** The search index */
    protected SearchIndex searchIdx = null;

    /** The site */
    protected Site site = null;

    /**
     * Creates a new index that is located in the indicated folder.
     * 
     * @param site
     *          the site
     * @param rootDir
     *          the root directory
     * @param readOnly
     *          <code>true</code> if the index should be read only
     * @param serializer
     *          the resource serializer
     * @throws IOException
     *           if creating the indices fails
     */
    public ContentRepositoryIndex(Site site, ResourceSerializerService serializer, boolean readOnly)
            throws IOException {
        this.site = site;
        this.searchIdx = new SearchIndex(site, serializer, readOnly);
    }

    /**
     * Returns the index version or <code>-1</code> if the versions differ.
     * 
     * @return the index version
     */
    public int getIndexVersion() {
        int version = searchIdx.getIndexVersion();
        return version;
    }

    /**
     * Sets the search index.
     * 
     * @param searchIndex
     *          the search index
     */
    protected void setSearchIndex(SearchIndex searchIndex) {
        this.searchIdx = searchIndex;
    }

    /**
     * Closes the index files. No more read and write operations are allowed.
     * 
     * @throws IOException
     *           if closing the index fails
     */
    public void close() throws IOException {
        if (searchIdx != null)
            searchIdx.close();
    }

    /**
     * Returns the number of resources in this index.
     * 
     * @return the number of resources
     * @throws ContentRepositoryException
     *           if querying for the resource count fails
     */
    public long getResourceCount() throws ContentRepositoryException {
        SearchQuery q = new SearchQueryImpl(site).withPreferredVersion(Resource.LIVE).withField(RESOURCE_ID);
        return searchIdx.getByQuery(q).getHitCount();
    }

    /**
     * Returns the number of revisions in this index.
     * 
     * @return the number of revisions
     * @throws ContentRepositoryException
     *           if querying for the resource count fails
     */
    public long getRevisionCount() throws ContentRepositoryException {
        SearchQuery q = new SearchQueryImpl(site).withField(RESOURCE_ID);
        return searchIdx.getByQuery(q).getHitCount();
    }

    /**
     * Adds all relevant entries for the given resource uri to the index and
     * returns it, probably providing a newly created uri identifier.
     * 
     * @param url
     *          the resource uri
     * @return the uri
     * @throws IOException
     *           if writing to the index fails
     * @throws ContentRepositoryException
     *           if adding to the index fails
     */
    public synchronized ResourceURI add(Resource<?> resource) throws IOException, ContentRepositoryException {

        ResourceURI uri = resource.getURI();
        String id = uri.getIdentifier();
        String path = StringUtils.trimToNull(uri.getPath());
        long version = uri.getVersion();

        // Make sure we are not asked to add a resource to the index that has the
        // same id as an existing one
        if (id != null) {
            SearchQuery q = new SearchQueryImpl(site).withIdentifier(id).withPreferredVersion(version).withLimit(1)
                    .withField(PATH);
            SearchResultItem[] items = searchIdx.getByQuery(q).getItems();
            if (items.length > 0) {
                long versionInIndex = (Long) ((ResourceSearchResultItem) items[0]).getMetadataByKey(VERSION)
                        .getValue();
                if (items.length == 1 && versionInIndex == version)
                    throw new ContentRepositoryException(
                            "Resource '" + id + "' already exists in version " + version);
                if (path == null) {
                    path = (String) ((ResourceSearchResultItem) items[0]).getMetadataByKey(PATH).getValue();
                    resource.getURI().setPath(path);
                }
            }
        }

        // Make sure we are not asked to add a resource to the index that has the
        // same path as an existing one
        if (path != null) {
            SearchQuery q = new SearchQueryImpl(site).withPath(path).withPreferredVersion(version).withLimit(1)
                    .withField(RESOURCE_ID);
            SearchResultItem[] items = searchIdx.getByQuery(q).getItems();
            if (items.length > 0) {
                long versionInIndex = (Long) ((ResourceSearchResultItem) items[0]).getMetadataByKey(VERSION)
                        .getValue();
                if (items.length == 1 && versionInIndex == version)
                    throw new ContentRepositoryException(
                            "Resource '" + id + "' already exists in version " + version);
                if (id == null) {
                    id = (String) ((ResourceSearchResultItem) items[0]).getMetadataByKey(RESOURCE_ID).getValue();
                    resource.getURI().setIdentifier(id);
                }
            }
        }

        // Create an id if necessary. A missing id indicates that the resource
        // has never been added to the index before
        if (id == null) {
            id = UUID.randomUUID().toString();
            resource.setIdentifier(id);
            uri.setIdentifier(id);
        }

        try {
            searchIdx.add(resource);
        } catch (ContentRepositoryException e) {
            throw e;
        } catch (Throwable t) {
            throw new ContentRepositoryException("Error adding " + resource + " to index", t);
        }

        return uri;
    }

    /**
     * Removes all entries for the given resource uri from the index and returns
     * <code>true</code>. If the resource is not part of the index,
     * <code>false</code> is returned.
     * 
     * @param uri
     *          the resource uri
     * @return <code>true</code> if the resource was deleted
     * @throws IOException
     *           if updating the index fails
     * @throws ContentRepositoryException
     *           if deleting the resource fails
     */
    public synchronized boolean delete(ResourceURI uri)
            throws IOException, ContentRepositoryException, IllegalArgumentException {
        getIdentifier(uri);
        StringUtils.trimToNull(uri.getPath());
        uri.getVersion();

        // Finally, delete the entry
        return searchIdx.delete(uri);
    }

    /**
     * Returns all revisions for the specified resource or an empty array if the
     * resource doesn't exist.
     * 
     * @param uri
     *          the resource uri
     * @return the revisions
     */
    public long[] getRevisions(ResourceURI uri) throws ContentRepositoryException {
        String id = getIdentifier(uri);
        if (id == null)
            return new long[] {};
        SearchQuery q = new SearchQueryImpl(site).withIdentifier(id).withField(VERSION);
        SearchResultItem[] items = searchIdx.getByQuery(q).getItems();
        long[] versions = new long[items.length];
        for (int i = 0; i < items.length; i++) {
            versions[i] = (Long) ((ResourceSearchResultItem) items[i]).getMetadataByKey(VERSION).getValue();
        }
        return versions;
    }

    /**
     * Returns the identifier of the resource with uri <code>uri</code> or
     * <code>null</code> if the uri is not part of the index.
     * 
     * @param uri
     *          the uri
     * @return the id
     * @throws IllegalArgumentException
     *           if the uri does not contain a path
     * @throws ContentRepositoryException
     *           if accessing the index fails
     */
    public String getIdentifier(ResourceURI uri) throws ContentRepositoryException, IllegalArgumentException {

        if (uri.getIdentifier() != null)
            return uri.getIdentifier();

        String path = StringUtils.trimToNull(uri.getPath());
        if (path == null)
            throw new IllegalArgumentException("ResourceURI must contain a path");

        // Load the identifier from the index
        SearchQuery q = new SearchQueryImpl(site).withPath(path);
        if (uri.getType() != null)
            q.withTypes(uri.getType());
        SearchResultItem[] items = searchIdx.getByQuery(q).getItems();
        if (items.length == 0) {
            logger.debug("Attempt to locate id for non-existing path {}", path);
            return null;
        }

        String id = (String) ((ResourceSearchResultItem) items[0]).getMetadataByKey(RESOURCE_ID).getValue();
        uri.setIdentifier(id);
        return id;
    }

    /**
     * Returns the path of the resource with uri <code>uri</code> by looking it up
     * using the uri's identifier or <code>null</code> if the uri is not part of
     * the index.
     * 
     * @param uri
     *          the uri
     * @return the path
     * @throws IllegalArgumentException
     *           if the uri does not contain an identifier
     * @throws ContentRepositoryException
     *           if accessing the index fails
     */
    public String getPath(ResourceURI uri) throws ContentRepositoryException, IllegalArgumentException {
        if (uri.getPath() != null)
            return uri.getPath();

        String id = uri.getIdentifier();
        if (id == null)
            throw new IllegalArgumentException("ResourceURI must contain an identifier");

        // Load the path from the index
        SearchQuery q = new SearchQueryImpl(site).withIdentifier(id);
        if (uri.getType() != null)
            q.withTypes(uri.getType());
        SearchResultItem[] items = searchIdx.getByQuery(q).getItems();
        if (items.length == 0) {
            logger.debug("Attempt to locate path for non existing resource '{}'", id);
            return null;
        }

        String path = (String) ((ResourceSearchResultItem) items[0]).getMetadataByKey(RESOURCE_ID).getValue();
        uri.setPath(path);
        return path;
    }

    /**
     * Returns the resource type or <code>null</code> the resource could not be
     * found.
     * 
     * @param uri
     *          the resource uri
     * @return the resource type
     * @throws ContentRepositoryException
     *           if loading the type from the index fails
     */
    public String getType(ResourceURI uri) throws ContentRepositoryException {
        if (uri.getType() != null)
            return uri.getType();

        String id = uri.getIdentifier();
        String path = uri.getPath();
        SearchQuery q = null;

        if (id != null)
            q = new SearchQueryImpl(site).withIdentifier(id).withLimit(1);
        else if (path != null)
            q = new SearchQueryImpl(site).withPath(path).withLimit(1);
        else
            throw new IllegalArgumentException("URI must have either id or path");

        // Load the path from the index
        SearchResultItem[] items = searchIdx.getByQuery(q).getItems();
        if (items.length == 0) {
            logger.debug("Attempt to locate path for non existing resource '{}'", id);
            return null;
        }

        String type = (String) ((ResourceSearchResultItem) items[0]).getMetadataByKey(TYPE).getValue();
        uri.setType(type);
        return type;
    }

    /**
     * Updates the resource in the search index.
     * 
     * @param resource
     *          the resource to update
     * @throws IOException
     *           if writing to the index fails
     * @throws ContentRepositoryException
     *           if updating the index fails
     */
    public synchronized void update(Resource<?> resource) throws IOException, ContentRepositoryException {
        ResourceURI uri = resource.getURI();

        // Make sure the uri has an identifier
        if (uri.getIdentifier() == null) {
            uri.setIdentifier(getIdentifier(uri));
        }

        searchIdx.update(resource);
    }

    /**
     * Updates the path of the given resource.
     * 
     * @param uir
     *          the resource uri
     * @param path
     *          the new path
     * @throws IOException
     *           if writing to the index fails
     * @throws ContentRepositoryException
     *           if moving the resource fails
     * @throws IllegalStateException
     *           if the resource to be moved could not be found in the index
     */
    public synchronized void move(ResourceURI uri, String path)
            throws IOException, ContentRepositoryException, IllegalStateException {

        // Do it this way to make sure we have identical path trimming
        ResourceURI newURI = new ResourceURIImpl(uri.getType(), uri.getSite(), StringUtils.trimToNull(path),
                uri.getIdentifier(), uri.getVersion());
        path = newURI.getPath();

        searchIdx.move(uri, path);
    }

    /**
     * Removes all entries from the index.
     * 
     * @throws IOException
     *           if clearing the index fails
     */
    public synchronized void clear() throws IOException {
        searchIdx.clear();
    }

    /**
     * Returns <code>true</code> if the given uri exists in the given version.
     * 
     * @param uri
     *          the uri
     * @return <code>true</code> if the uri exists
     * @throws ContentRepositoryException
     *           if looking up the uri fails
     */
    public boolean exists(ResourceURI uri) throws ContentRepositoryException {
        String id = getIdentifier(uri);
        if (id == null)
            return false;
        SearchQuery q = new SearchQueryImpl(site).withIdentifier(id).withVersion(uri.getVersion())
                .withField(RESOURCE_ID);
        if (uri.getType() != null)
            q.withTypes(uri.getType());
        return searchIdx.getByQuery(q).getDocumentCount() > 0;
    }

    /**
     * Returns <code>true</code> if the given uri exists in any version.
     * 
     * @param uri
     *          the uri
     * @return <code>true</code> if the uri exists in any version
     * @throws ContentRepositoryException
     *           if looking up the uri fails
     */
    public boolean existsInAnyVersion(ResourceURI uri) throws ContentRepositoryException {
        String id = getIdentifier(uri);
        if (id == null)
            return false;
        SearchQuery q = new SearchQueryImpl(site).withIdentifier(id).withLimit(1).withField(RESOURCE_ID);
        if (uri.getType() != null)
            q.withTypes(uri.getType());
        return searchIdx.getByQuery(q).getDocumentCount() > 0;
    }

    /**
     * Returns all URIs that share the common root <code>uri</code>, are no more
     * than <code>level</code> levels deep nested and feature the indicated
     * version.
     * <p>
     * 
     * @param uri
     *          the root uri
     * @param level
     *          the maximum nesting, <code>0</code> to return direct children only
     * @return an iteration of the resulting uris
     */
    public Iterator<ResourceURI> list(ResourceURI uri, int level) {
        return list(uri, level, -1);
    }

    /**
     * Returns all uris that share the common root <code>uri</code>, are no more
     * than <code>level</code> levels deep nested and feature the indicated
     * version.
     * <p>
     * 
     * @param uri
     *          the root uri
     * @param level
     *          the maximum nesting, <code>0</code> to return direct children only
     * @param version
     *          the requested version, <code>-1</code> for any version
     * @return an iteration of the resulting uris
     */
    public Iterator<ResourceURI> list(ResourceURI uri, int level, long version) {
        throw new UnsupportedOperationException("Not yet implemented");
    }

    /**
     * Issues a search query and returns the matches in the result set.
     * 
     * @param query
     *          the search query
     * @return the search result
     * @throws IllegalArgumentException
     *           if the query is <code>null</code>
     * @throws ContentRepositoryException
     *           if processing the query fails
     */
    public SearchResult find(SearchQuery query) throws ContentRepositoryException, IllegalArgumentException {
        if (query == null)
            throw new IllegalArgumentException("Query cannot be null");
        return searchIdx.getByQuery(query);
    }

    /**
     * Returns the suggestions as returned from the selected dictionary based on
     * <code>seed</code>.
     * 
     * @param dictionary
     *          the dictionary
     * @param seed
     *          the seed used for suggestions
     * @param onlyMorePopular
     *          whether to return only more popular results
     * @param count
     *          the maximum number of suggestions
     * @param collate
     *          whether to provide a query collated with the first matching
     *          suggestion
     * @throws ContentRepositoryException
     *           if suggesting fails
     */
    public List<String> suggest(String dictionary, String seed, boolean onlyMorePopular, int count, boolean collate)
            throws ContentRepositoryException {
        if (StringUtils.isBlank(dictionary))
            throw new IllegalArgumentException("Dictionary cannot be null");
        if (StringUtils.isBlank(seed))
            throw new IllegalArgumentException("Seed cannot be null");
        return searchIdx.suggest(dictionary, seed, onlyMorePopular, count, collate);
    }

}