blob: c35a8f1cc830355ff3da6d08caad840de0dca390 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.sling.mongodb.impl;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import javax.servlet.http.HttpServletRequest;
import org.apache.sling.api.resource.ModifyingResourceProvider;
import org.apache.sling.api.resource.PersistenceException;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceProvider;
import org.apache.sling.api.resource.ResourceResolver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.mongodb.BasicDBObject;
import com.mongodb.BasicDBObjectBuilder;
import com.mongodb.DBCollection;
import com.mongodb.DBCursor;
import com.mongodb.DBObject;
import com.mongodb.QueryBuilder;
/**
* The MongoDB resource provider creates resources based on MongoDB entries.
* The resources contain all properties stored in the MongoDB except those starting with a "_".
*/
public class MongoDBResourceProvider implements ResourceProvider, ModifyingResourceProvider {
/** The special path property containing the (relative) path of the resource in the tree. */
public static final String PROP_PATH = "_path";
/** The id property. */
public static final String PROP_ID = "_id";
/** Logger. */
private final Logger logger = LoggerFactory.getLogger(this.getClass());
/** The global context .*/
private final MongoDBContext context;
private final Map<String, MongoDBResource> changedResources = new HashMap<String, MongoDBResource>();
private final Set<String> deletedResources = new HashSet<String>();
public MongoDBResourceProvider(final MongoDBContext context) {
this.context = context;
}
public static String propNameToKey(final String name) {
if ( name.startsWith("_") ) {
return "_" + name;
}
return name;
}
public static String keyToPropName(final String key) {
if ( key.startsWith("__") ) {
return key.substring(1);
} else if ( key.startsWith("_") ) {
return null;
}
return key;
}
/**
* @see org.apache.sling.api.resource.ModifyingResourceProvider#create(org.apache.sling.api.resource.ResourceResolver, java.lang.String, java.util.Map)
*/
public Resource create(final ResourceResolver resolver, final String path, final Map<String, Object> properties)
throws PersistenceException {
final String[] info = this.extractResourceInfo(path);
if ( info != null && info.length == 2) {
final boolean deleted = this.deletedResources.remove(path);
final MongoDBResource oldResource = (MongoDBResource)this.getResource(resolver, path, info);
if ( !deleted && oldResource != null ) {
throw new PersistenceException("Resource already exists at " + path, null, path, null);
}
final DBObject dbObj = new BasicDBObject();
dbObj.put(PROP_PATH, info[1]);
if ( properties != null ) {
for(Map.Entry<String, Object> entry : properties.entrySet()) {
final String key = propNameToKey(entry.getKey());
dbObj.put(key, entry.getValue());
}
}
if ( deleted && oldResource != null ) {
dbObj.put(PROP_ID, oldResource.getProperties().get(PROP_ID));
}
final MongoDBResource rsrc = new MongoDBResource(resolver, path, info[0], dbObj, this);
this.changedResources.put(path, rsrc);
return rsrc;
}
throw new PersistenceException("Illegal path - unable to create resource at " + path, null, path, null);
}
/**
* @see org.apache.sling.api.resource.ModifyingResourceProvider#delete(org.apache.sling.api.resource.ResourceResolver, java.lang.String)
*/
public void delete(final ResourceResolver resolver, final String path)
throws PersistenceException {
final String[] info = this.extractResourceInfo(path);
if ( info != null ) {
boolean deletedResource = false;
if ( !deletedResources.contains(path) ) {
final Resource rsrc = this.getResource(resolver, path, info);
if ( rsrc instanceof MongoDBResource ) {
this.deletedResources.add(path);
this.changedResources.remove(path);
final DBCollection col = this.getCollection(info[0]);
final String pattern = "^" + Pattern.quote(info[1]) + "/";
final DBObject query = QueryBuilder.start(PROP_PATH).regex(Pattern.compile(pattern)).get();
final DBCursor cur = col.find(query);
while ( cur.hasNext() ) {
final DBObject dbObj = cur.next();
final String childPath = info[0] + '/' + dbObj.get(PROP_PATH);
this.deletedResources.add(childPath);
this.changedResources.remove(childPath);
}
deletedResource = true;
}
} else {
deletedResource = true;
}
if ( deletedResource ) {
final String prefix = path + "/";
final Iterator<Map.Entry<String, MongoDBResource>> i = this.changedResources.entrySet().iterator();
while ( i.hasNext() ) {
final Map.Entry<String, MongoDBResource> entry = i.next();
if ( entry.getKey().startsWith(prefix) ) {
i.remove();
}
}
return;
}
}
throw new PersistenceException("Unable to delete resource at {}" + path, null, path, null);
}
/**
* @see org.apache.sling.api.resource.ModifyingResourceProvider#revert()
*/
public void revert() {
this.changedResources.clear();
this.deletedResources.clear();
}
/**
* @see org.apache.sling.api.resource.ModifyingResourceProvider#commit()
*/
public void commit() throws PersistenceException {
try {
for(final String deleted : this.deletedResources) {
final String[] info = this.extractResourceInfo(deleted);
// check if the collection still exists
final DBCollection col = this.getCollection(info[0]);
if ( col != null ) {
col.findAndRemove(QueryBuilder.start(PROP_PATH).is(info[1]).get());
}
}
for(final MongoDBResource changed : this.changedResources.values()) {
final DBCollection col = this.context.getDatabase().getCollection(changed.getCollection());
if ( col != null ) {
// create or update?
if ( changed.getProperties().get(PROP_ID) != null ) {
col.update(QueryBuilder.start(PROP_PATH).is(changed.getProperties().get(PROP_PATH)).get(),
changed.getProperties());
} else {
// create
col.save(changed.getProperties());
}
} else {
throw new PersistenceException("Unable to create collection " + changed.getCollection(), null, changed.getPath(), null);
}
}
} finally {
this.revert();
}
}
/**
* @see org.apache.sling.api.resource.ModifyingResourceProvider#hasChanges()
*/
public boolean hasChanges() {
return this.changedResources.size() > 0 || this.deletedResources.size() > 0;
}
/**
* @see org.apache.sling.api.resource.ResourceProvider#getResource(org.apache.sling.api.resource.ResourceResolver, java.lang.String)
*/
public Resource getResource(final ResourceResolver resourceResolver, final String path) {
if ( this.deletedResources.contains(path) ) {
return null;
}
if ( this.changedResources.containsKey(path) ) {
return this.changedResources.get(path);
}
final String[] info = this.extractResourceInfo(path);
if ( info != null ) {
return this.getResource(resourceResolver, path, info);
}
return null;
}
/**
* Inform about changes of a resource.
*/
public void changed(final MongoDBResource resource) {
this.deletedResources.remove(resource.getPath());
this.changedResources.put(resource.getPath(), resource);
}
/**
* @see org.apache.sling.api.resource.ResourceProvider#listChildren(org.apache.sling.api.resource.Resource)
*/
public Iterator<Resource> listChildren(final Resource parent) {
final String[] info = this.extractResourceInfo(parent.getPath());
if ( info != null ) {
if ( info.length == 0 ) {
// all collections
final Set<String> names = new HashSet<String>(context.getDatabase().getCollectionNames());
names.removeAll(this.context.getFilterCollectionNames());
final Iterator<String> i = names.iterator();
return new Iterator<Resource>() {
public boolean hasNext() {
return i.hasNext();
}
public Resource next() {
final String name = i.next();
return new MongoDBCollectionResource(parent.getResourceResolver(), parent.getPath() + '/' + name);
}
public void remove() {
throw new UnsupportedOperationException("remove");
}
};
}
final DBCollection col = this.getCollection(info[0]);
if ( col != null ) {
final String pattern;
if ( info.length == 1 ) {
pattern = "^([^/])*$";
} else {
pattern = "^" + Pattern.quote(info[1]) + "/([^/])*$";
}
final DBObject query = QueryBuilder.start(PROP_PATH).regex(Pattern.compile(pattern)).get();
final DBCursor cur = col.find(query).
sort(BasicDBObjectBuilder.start(PROP_PATH, 1).get());
return new Iterator<Resource>() {
public boolean hasNext() {
return cur.hasNext();
}
public Resource next() {
final DBObject obj = cur.next();
final String objPath = obj.get(PROP_PATH).toString();
final int lastSlash = objPath.lastIndexOf('/');
final String name;
if (lastSlash == -1) {
name = objPath;
} else {
name = objPath.substring(lastSlash + 1);
}
return new MongoDBResource(parent.getResourceResolver(),
parent.getPath() + '/' + name,
info[0],
obj,
MongoDBResourceProvider.this);
}
public void remove() {
throw new UnsupportedOperationException("remove");
}
};
}
}
return null;
}
/**
* @see org.apache.sling.api.resource.ResourceProvider#getResource(org.apache.sling.api.resource.ResourceResolver, javax.servlet.http.HttpServletRequest, java.lang.String)
*/
@SuppressWarnings("javadoc")
public Resource getResource(final ResourceResolver resourceResolver,
final HttpServletRequest request,
final String path) {
return this.getResource(resourceResolver, path);
}
/**
* Extract info about collection and path
*/
private String[] extractResourceInfo(final String path) {
for(final String root : this.context.getRootsWithSlash()) {
if ( path.startsWith(root) ) {
if ( path.length() == root.length() ) {
// special resource - show all collections
return new String[0];
}
final String info = path.substring(root.length());
final int slashPos = info.indexOf('/');
if ( slashPos != -1 ) {
return new String[] {info.substring(0, slashPos), info.substring(slashPos + 1)};
}
// special resource - collection
return new String[] {info};
}
}
for(final String root : this.context.getRoots()) {
if ( path.equals(root) ) {
// special resource - show all collections
return new String[0];
}
}
return null;
}
/**
* Check if a collection with a given name exists
*/
private boolean hasCollection(final String name) {
final Set<String> names = this.context.getDatabase().getCollectionNames();
return names.contains(name) && !this.context.isFilterCollectionName(name);
}
/**
* Check if a collection with a given name exists and return it
*/
private DBCollection getCollection(final String name) {
if ( this.hasCollection(name) ) {
return this.context.getDatabase().getCollection(name);
}
return null;
}
/**
* Get a resource
*/
private Resource getResource(final ResourceResolver resourceResolver, final String path, final String[] info) {
if ( info.length == 0 ) {
// special resource : all collections
return new MongoDBCollectionResource(resourceResolver, path);
} else if ( info.length == 1 ) {
// special resource : collection
if ( this.hasCollection(info[0]) ) {
return new MongoDBCollectionResource(resourceResolver, path);
}
return null;
}
logger.debug("Searching {} in {}", info[1], info[0]);
final DBCollection col = this.getCollection(info[0]);
if ( col != null ) {
final DBObject obj = col.findOne(QueryBuilder.start(PROP_PATH).is(info[1]).get());
logger.debug("Found {}", obj);
if ( obj != null ) {
return new MongoDBResource(resourceResolver,
path,
info[0],
obj,
this);
}
}
return null;
}
/**
* Check if there is a newer db object for that path.
*/
public DBObject getUpdatedDBObject(final String path, final DBObject dbObj) {
final MongoDBResource stored = this.changedResources.get(path);
if ( stored != null ) {
return stored.getProperties();
}
return dbObj;
}
}