org.apache.sling.mongodb.impl.MongoDBResourceProvider.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.sling.mongodb.impl.MongoDBResourceProvider.java

Source

/*
 * 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.QueriableResourceProvider;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceProvider;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ValueMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.mongodb.BasicDBObject;
import com.mongodb.BasicDBObjectBuilder;
import com.mongodb.CommandResult;
import com.mongodb.DBCollection;
import com.mongodb.DBCursor;
import com.mongodb.DBObject;
import com.mongodb.QueryBuilder;
import com.mongodb.util.JSON;

/**
 * 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, QueriableResourceProvider {

    /** The special path property containing the (relative) path of the resource in the tree. */
    private static final String PROP_PATH = "_path";

    /** The id property. */
    private 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(getPROP_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);
    }

    /**
     * TODO - we should handle delete different and not put all child resources into the
     * deleted set.
     * Instead when getting resources, the parents of the resource should be checked
     * first.
     * This minimizes concurrency issues.
     * @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(getPROP_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(getPROP_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(ResourceResolver)
     */
    public void revert(final ResourceResolver resolver) {
        this.changedResources.clear();
        this.deletedResources.clear();
    }

    /**
     * @see org.apache.sling.api.resource.ModifyingResourceProvider#commit(ResourceResolver)
     */
    public void commit(final ResourceResolver resolver) 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) {
                    if (col.findAndRemove(QueryBuilder.start(getPROP_PATH()).is(info[1]).get()) != null) {
                        this.context.notifyRemoved(info);
                    }
                }
            }
            for (final MongoDBResource changed : this.changedResources.values()) {

                final DBCollection col = this.context.getDatabase().getCollection(changed.getCollection());
                if (col != null) {
                    final String[] info = new String[] { changed.getCollection(),
                            changed.getProperties().get(getPROP_PATH()).toString() };
                    // create or update?
                    if (changed.getProperties().get(PROP_ID) != null) {
                        col.update(QueryBuilder.start(getPROP_PATH())
                                .is(changed.getProperties().get(getPROP_PATH())).get(), changed.getProperties());
                        this.context.notifyUpdated(info);
                    } else {
                        // create
                        col.save(changed.getProperties());
                        this.context.notifyUpdated(info);
                    }
                } else {
                    throw new PersistenceException("Unable to create collection " + changed.getCollection(), null,
                            changed.getPath(), null);
                }
            }
        } finally {
            this.revert(resolver);
        }
    }

    /**
     * @see org.apache.sling.api.resource.ModifyingResourceProvider#hasChanges(ResourceResolver)
     */
    public boolean hasChanges(final ResourceResolver resolver) {
        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 new MongoDBResource(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);
    }

    /**
     * TODO - we have to check for deleted and added resources
     * @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(getPROP_PATH()).regex(Pattern.compile(pattern)).get();
                final DBCursor cur = col.find(query).sort(BasicDBObjectBuilder.start(getPROP_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(getPROP_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
     */
    protected String[] extractResourceInfo(final String path) {
        if (path.startsWith(this.context.getRootWithSlash())) {
            if (path.length() == this.context.getRootWithSlash().length()) {
                // special resource - show all collections
                return new String[0];
            }
            final String info = path.substring(this.context.getRootWithSlash().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 };
        }

        if (path.equals(this.context.getRoot())) {
            // special resource - show all collections
            return new String[0];
        }

        return null;
    }

    /**
     * Check if a collection with a given name exists
     */
    protected boolean hasCollection(final String name) {
        logger.info("Mongo: Getting collection names");
        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
     */
    protected DBCollection getCollection(final String name) {
        if (this.hasCollection(name)) {
            return this.context.getDatabase().getCollection(name);
        }
        return null;
    }

    /**
     * Get a resource
     */
    protected 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(getPROP_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;
    }

    protected Set<String> getDeletedResources() {
        return this.deletedResources;
    }

    protected Map<String, MongoDBResource> getChangedResources() {
        return this.changedResources;
    }

    protected MongoDBContext getContext() {
        return this.context;
    }

    protected String getPROP_PATH() {
        return PROP_PATH;
    }

    public Iterator<Resource> findResources(final ResourceResolver resolver, String query, String language) {
        if (!language.equals("mongodb") || query == null || query.length() == 0 || query.indexOf(".find(") <= 0) {
            return null;
        }
        Iterator<Resource> returnValue = null;
        final String collectionName = query.substring(0, query.indexOf(".find("));
        DBCollection col = this.getCollection(collectionName);
        if (col != null) {
            String criteria = query.trim().substring(query.indexOf(".find(") + 6, query.length() - 1);
            DBObject dbObject = (DBObject) JSON.parse(criteria);
            final DBCursor cur = col.find(dbObject);
            final String rootPath = context.getRootWithSlash();

            return new Iterator<Resource>() {

                public boolean hasNext() {
                    return cur.hasNext();
                }

                public Resource next() {
                    final DBObject obj = cur.next();
                    final String objPath = obj.get(getPROP_PATH()).toString();
                    final int lastSlash = objPath.lastIndexOf('/');
                    final String name;
                    if (lastSlash == -1) {
                        name = objPath;
                    } else {
                        name = objPath.substring(lastSlash + 1);
                    }
                    return new MongoDBResource(resolver, rootPath + collectionName + "/" + name, collectionName,
                            obj, MongoDBResourceProvider.this);
                }

                public void remove() {
                    throw new UnsupportedOperationException("remove");
                }

            };
        }

        return returnValue;
    }

    public Iterator<ValueMap> queryResources(ResourceResolver resolver, String query, String language) {
        return null;
    }
}