/*
* 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.search.impl.IndexSchema.PATH;
import static ch.entwine.weblounge.search.impl.IndexSchema.RESOURCE_ID;
import static ch.entwine.weblounge.search.impl.IndexSchema.TYPE;
import static ch.entwine.weblounge.search.impl.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.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.search.SearchIndex;
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.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 searchIndex
* the search index
* @throws IOException
* if creating the indices fails
* @throws IllegalArgumentException
* if one of the parameters is null
*/
public ContentRepositoryIndex(Site site, SearchIndex searchIndex)
throws IOException, IllegalArgumentException {
if (site == null)
throw new IllegalArgumentException("Site must not be null");
if (searchIndex == null)
throw new IllegalArgumentException("Search index must not be null");
this.site = site;
this.searchIdx = searchIndex;
}
/**
* 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;
}
/**
* 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 = null;
}
/**
* 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");
}
}