org.forgerock.openidm.repo.mongodb.impl.MongoDBRepoService.java Source code

Java tutorial

Introduction

Here is the source code for org.forgerock.openidm.repo.mongodb.impl.MongoDBRepoService.java

Source

/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright  2011 ForgeRock AS.
 * Portions Copyrighted 2013 Takao Sekiguchi.
 * All rights reserved.
 *
 * The contents of this file are subject to the terms
 * of the Common Development and Distribution License
 * (the License). You may not use this file except in
 * compliance with the License.
 *
 * You can obtain a copy of the License at
 * http://forgerock.org/license/CDDLv1.0.html
 * See the License for the specific language governing
 * permission and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL
 * Header Notice in each file and include the License file
 * at http://forgerock.org/license/CDDLv1.0.html
 * If applicable, add the following below the CDDL Header,
 * with the fields enclosed by brackets [] replaced by
 * your own identifying information:
 * "Portions Copyrighted [year] [name of copyright owner]"
 */
package org.forgerock.openidm.repo.mongodb.impl;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.felix.scr.annotations.Activate;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.ConfigurationPolicy;
import org.apache.felix.scr.annotations.Deactivate;
import org.apache.felix.scr.annotations.Modified;
import org.apache.felix.scr.annotations.Properties;
import org.apache.felix.scr.annotations.Property;
import org.apache.felix.scr.annotations.Service;
import org.forgerock.json.fluent.JsonValue;
// JSON Resource
import org.forgerock.json.resource.JsonResource;
import org.forgerock.openidm.config.EnhancedConfig;
import org.forgerock.openidm.config.JSONEnhancedConfig;
// Deprecated
import org.forgerock.openidm.objset.BadRequestException;
import org.forgerock.openidm.objset.ConflictException;
import org.forgerock.openidm.objset.ForbiddenException;
import org.forgerock.openidm.objset.NotFoundException;
import org.forgerock.openidm.objset.ObjectSetException;
import org.forgerock.openidm.objset.ObjectSetJsonResource;
import org.forgerock.openidm.objset.Patch;
import org.forgerock.openidm.objset.PreconditionFailedException;
import org.forgerock.openidm.repo.QueryConstants;
import org.forgerock.openidm.repo.RepoBootService;
import org.forgerock.openidm.repo.RepositoryService;
import org.forgerock.openidm.repo.mongodb.impl.query.PredefinedQueries;
import org.forgerock.openidm.repo.mongodb.impl.query.Queries;
import org.osgi.service.component.ComponentContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.mongodb.BasicDBObject;
import com.mongodb.BasicDBObjectBuilder;
import com.mongodb.DB;
import com.mongodb.DBCollection;
import com.mongodb.DBObject;
import com.mongodb.WriteResult;

/**
 * Repository service implementation using MongoDB
 * @author takao-s
 */
@Component(name = MongoDBRepoService.PID, immediate = true, policy = ConfigurationPolicy.REQUIRE, enabled = true)
@Service(value = { RepositoryService.class, JsonResource.class }) // Omit the RepoBootService interface from the managed service
@Properties({ @Property(name = "service.description", value = "Repository Service using MongoDB"),
        @Property(name = "service.vendor", value = "ForgeRock AS"),
        @Property(name = "openidm.router.prefix", value = "repo"), @Property(name = "db.type", value = "MongoDB") })
public class MongoDBRepoService extends ObjectSetJsonResource implements RepositoryService, RepoBootService {
    final static Logger logger = LoggerFactory.getLogger(MongoDBRepoService.class);

    public static final String PID = "org.forgerock.openidm.repo.mongodb";

    // Keys in the JSON configuration
    public static final String CONFIG_REPLICASET = "replicaSet";
    public static final String CONFIG_HOST = "host";
    public static final String CONFIG_PORT = "port";
    public static final String CONFIG_DBNAME = "dbName";
    public static final String CONFIG_CONN_PER_HOST = "connectionPerHost";
    public static final String CONFIG_CONN_MULTIPLIER = "connectionMultiple";
    public static final String CONFIG_WRITE_CONCERN = "writeconcern";

    public static final String CONFIG_QUERIES = "queries";
    public static final String CONFIG_QUERY_FIELDS = "fields";
    public static final String CONFIG_QUERY_SORT = "sort";
    public static final String CONFIG_QUERY_AGGREGATE = "aggregate";

    public static final String CONFIG_USER = "user";
    public static final String CONFIG_PASSWORD = "password";

    public static final String CONFIG_DB_COLLECTIONS = "collections";
    public static final String CONFIG_INDEX = "index";
    public static final String CONFIG_INDEX_UNIQUE = "unique";

    DB db;

    // Current configuration
    JsonValue existingConfig;

    PredefinedQueries predefinedQueries = new PredefinedQueries();
    Queries queries = new Queries();
    EnhancedConfig enhancedConfig = new JSONEnhancedConfig();

    /**
     * Gets an object from the repository by identifier. The returned object is not validated 
     * against the current schema and may need processing to conform to an updated schema.
     * <p>
     *
     * @param fullId the identifier of the object to retrieve from the object set.
     * @throws NotFoundException if the specified object could not be found. 
     * @throws ForbiddenException if access to the object is forbidden.
     * @throws BadRequestException if the passed identifier is invalid
     * @return the requested object.
     */
    @Override
    public Map<String, Object> read(String fullId) throws ObjectSetException {
        String localId = getLocalId(fullId);
        String type = getObjectType(fullId, false);

        if (fullId == null || localId == null) {
            throw new NotFoundException("The repository requires clients to supply an identifier "
                    + "for the object to create. Full identifier: " + fullId + " local identifier: " + localId);
        } else if (type == null) {
            throw new NotFoundException("The object identifier did not include "
                    + "sufficient information to determine the object type: " + fullId);
        }

        Map<String, Object> result = null;
        DBCollection collection = getCollection(type);
        DBObject doc = predefinedQueries.getByID(localId, collection);
        if (doc == null) {
            throw new NotFoundException("Object " + fullId + " not found in " + type);
        }
        doc = DocumentUtil.normalizeForRead(doc);
        result = doc.toMap();
        logger.trace("Completed get for id: {} result: {}", fullId, result);
        return result;
    }

    /**
     * Creates a new object in the object set.
     * <p>
     * This method sets the {@code _id} property to the assigned identifier for the object,
     * and the {@code _rev} property to the revised object version (For optimistic concurrency)
     *
     * @param fullId the client-generated identifier to use, or {@code null} if server-generated identifier is requested.
     * @param obj the contents of the object to create in the object set.
     * @throws NotFoundException if the specified id could not be resolved. 
     * @throws ForbiddenException if access to the object or object set is forbidden.
     * @throws PreconditionFailedException if an object with the same ID already exists.
     */
    @Override
    public void create(String fullId, Map<String, Object> obj) throws ObjectSetException {
        String localId = getLocalId(fullId);
        String type = getObjectType(fullId, false);

        if (fullId == null || localId == null) {
            throw new NotFoundException(
                    "The repository requires clients to supply an identifier for the object to create. Full identifier: "
                            + fullId + " local identifier: " + localId);
        } else if (type == null) {
            throw new NotFoundException(
                    "The object identifier did not include sufficient information to determine the object type: "
                            + fullId);
        }

        obj.put(DocumentUtil.TAG_ID, localId);
        obj.put(DocumentUtil.MONGODB_PRIMARY_KEY, localId);
        obj.put(DocumentUtil.TAG_REV, "0");
        BasicDBObjectBuilder builder = BasicDBObjectBuilder.start(obj);
        DBObject jo = builder.get();
        jo = DocumentUtil.normalizeForWrite(jo);

        DBCollection collection = getCollection(type);
        collection.insert(jo);
        logger.debug("Completed create for id: {} revision: {}", fullId, jo.get(DocumentUtil.TAG_REV));
        logger.trace("Create payload for id: {} doc: {}", fullId, jo);
    }

    /**
     * Updates the specified object in the object set. 
     * <p>
     * This implementation requires MVCC and hence enforces that clients state what revision they expect 
     * to be updating
     * 
     * If successful, this method updates metadata properties within the passed object,
     * including: a new {@code _rev} value for the revised object's version
     *
     * @param fullId the identifier of the object to be put, or {@code null} to request a generated identifier.
     * @param rev the version of the object to update; or {@code null} if not provided.
     * @param obj the contents of the object to put in the object set.
     * @throws ConflictException if version is required but is {@code null}.
     * @throws ForbiddenException if access to the object is forbidden.
     * @throws NotFoundException if the specified object could not be found. 
     * @throws PreconditionFailedException if version did not match the existing object in the set.
     * @throws BadRequestException if the passed identifier is invalid
     */
    @Override
    public void update(String fullId, String rev, Map<String, Object> obj) throws ObjectSetException {

        String localId = getLocalId(fullId);
        String type = getObjectType(fullId, false);

        if (rev == null) {
            throw new ConflictException("Object passed into update does not have revision it expects set.");
        } else {
            DocumentUtil.parseVersion(rev);
            obj.put(DocumentUtil.TAG_REV, rev);
        }

        DBCollection collection = getCollection(type);
        DBObject existingDoc = predefinedQueries.getByID(localId, collection);
        if (existingDoc == null) {
            throw new NotFoundException("Update on object " + fullId + " could not find existing object.");
        }

        obj.remove(DocumentUtil.TAG_ID);
        obj.put(DocumentUtil.MONGODB_PRIMARY_KEY, localId);
        BasicDBObjectBuilder builder = BasicDBObjectBuilder.start(obj);
        DBObject jo = builder.get();
        jo = DocumentUtil.normalizeForWrite(jo);
        WriteResult res = collection.update(new BasicDBObject(DocumentUtil.TAG_ID, localId), jo);
        logger.trace("Updated doc for id {} to save {}", fullId, jo);
    }

    /**
     * Deletes the specified object from the object set.
     *
     * @param fullId the identifier of the object to be deleted.
     * @param rev the version of the object to delete or {@code null} if not provided.
     * @throws NotFoundException if the specified object could not be found. 
     * @throws ForbiddenException if access to the object is forbidden.
     * @throws ConflictException if version is required but is {@code null}.
     * @throws PreconditionFailedException if version did not match the existing object in the set.
     */
    @Override
    public void delete(String fullId, String rev) throws ObjectSetException {
        String localId = getLocalId(fullId);
        String type = getObjectType(fullId, false);

        if (rev == null) {
            throw new ConflictException("Object passed into delete does not have revision it expects set.");
        }

        int ver = Integer.valueOf(DocumentUtil.parseVersion(rev)); // This throws ConflictException if parse fails

        DBCollection collection = getCollection(type);
        DBObject existingDoc = predefinedQueries.getByID(localId, collection);
        if (existingDoc == null) {
            throw new NotFoundException("Object does not exist for delete on: " + fullId);
        }

        WriteResult res = collection.remove(new BasicDBObject(DocumentUtil.TAG_ID, localId));
    }

    /**
     * Currently not supported by this implementation.
     * 
     * Applies a patch (partial change) to the specified object in the object set.
     *
     * @param id the identifier of the object to be patched.
     * @param rev the version of the object to patch or {@code null} if not provided.
     * @param patch the partial change to apply to the object.
     * @throws ConflictException if patch could not be applied object state or if version is required.
     * @throws ForbiddenException if access to the object is forbidden.
     * @throws NotFoundException if the specified object could not be found. 
     * @throws PreconditionFailedException if version did not match the existing object in the set.
     */
    @Override
    public void patch(String id, String rev, Patch patch) throws ObjectSetException {
        throw new UnsupportedOperationException();
    }

    /**
     * Performs the query on the specified object and returns the associated results.
     * <p>
     * Queries are parametric; a set of named parameters is provided as the query criteria.
     * The query result is a JSON object structure composed of basic Java types. 
     * 
     * The returned map is structured as follow: 
     * - The top level map contains meta-data about the query, plus an entry with the actual result records.
     * - The <code>QueryConstants</code> defines the map keys, including the result records (QUERY_RESULT)
     *
     * @param fullId identifies the object to query.
     * @param params the parameters of the query to perform.
     * @return the query results, which includes meta-data and the result records in JSON object structure format.
     * @throws NotFoundException if the specified object could not be found. 
     * @throws BadRequestException if the specified params contain invalid arguments, e.g. a query id that is not
     * configured, a query expression that is invalid, or missing query substitution tokens.
     * @throws ForbiddenException if access to the object or specified query is forbidden.
     */
    @Override
    public Map<String, Object> query(String fullId, Map<String, Object> params) throws ObjectSetException {
        String type = getObjectType(fullId, true);
        logger.trace("Full id: {} Extracted type: {}", fullId, type);

        Map<String, Object> result = new HashMap<String, Object>();
        DBCollection collection = getCollection(type);

        List<Map<String, Object>> docs = new ArrayList<Map<String, Object>>();
        result.put(QueryConstants.QUERY_RESULT, docs);
        long start = System.currentTimeMillis();
        List<DBObject> queryResult = queries.query(params, collection);
        long end = System.currentTimeMillis();
        if (queryResult != null) {
            long convStart = System.currentTimeMillis();
            for (DBObject entry : queryResult) {
                entry = DocumentUtil.normalizeForRead(entry);
                Map<String, Object> convertedEntry = entry.toMap();
                docs.add(convertedEntry);
            }
            long convEnd = System.currentTimeMillis();
            result.put(QueryConstants.STATISTICS_CONVERSION_TIME, Long.valueOf(convEnd - convStart));
        }
        result.put(QueryConstants.STATISTICS_QUERY_TIME, Long.valueOf(end - start));

        if (logger.isDebugEnabled()) {
            logger.debug("Query result contains {} records, took {} ms and took {} ms to convert result.",
                    new Object[] { ((List) result.get(QueryConstants.QUERY_RESULT)).size(),
                            result.get(QueryConstants.STATISTICS_QUERY_TIME),
                            result.get(QueryConstants.STATISTICS_CONVERSION_TIME) });
            logger.debug("Query result: {}", result);
        }
        return result;
    }

    @Override
    public Map<String, Object> action(String id, Map<String, Object> params) throws ObjectSetException {
        throw new UnsupportedOperationException();
    }

    /**
     * @param name
     * @return A MongoDB collection.
     */
    DBCollection getCollection(String name) {
        return db.getCollection(typeToCollectionName(name));
    }

    private static String getLocalId(String id) {
        String localId = null;
        int lastSlashPos = id.lastIndexOf("/");
        if (lastSlashPos > -1) {
            localId = id.substring(id.lastIndexOf("/") + 1);
        }
        logger.trace("Full id: {} Extracted local id: {}", id, localId);
        return localId;
    }

    private static String getObjectType(String id, boolean query) {
        String type = null;

        if (query) {
            type = id;
            if (id != null && id.startsWith("/")) {
                type = id.substring(1);
            }
        } else {
            int lastSlashPos = id.lastIndexOf("/");
            if (lastSlashPos > -1) {
                int startPos = 0;
                // This should not be necessary as relative URI should not start with slash
                if (id.startsWith("/")) {
                    startPos = 1;
                }
                type = id.substring(startPos, lastSlashPos);
            }
        }
        logger.trace("Full id: {} Extracted type: {}", id, type);
        return type;
    }

    private static String typeToCollectionName(String type) {
        return type.replace("/", "_");
    }

    /**
     * Populate and return a repository service that knows how to query and manipulate configuration.
     *
     * @param repoConfig the bootstrap configuration
     * @return the boot repository service. This instance is not managed by SCR and needs to be manually registered.
     */
    static MongoDBRepoService getRepoBootService(JsonValue repoConfig) {
        MongoDBRepoService bootRepo = new MongoDBRepoService();
        bootRepo.init(repoConfig);
        return bootRepo;
    }

    /**
     * Initialize the instnace with the given configuration.
     * 
     * This can configure managed (DS/SCR) instances, as well as explicitly instantiated
     * (bootstrap) instances.
     * 
     * @param config the configuration
     */
    void init(JsonValue config) {
        db = DBHelper.getDB(config, true);

        queries.setQueriesConfig(config.get(CONFIG_QUERIES).toString());
        queries.setFieldsConfig(config.get(CONFIG_QUERY_FIELDS).toString());
        queries.setSortConfig(config.get(CONFIG_QUERY_SORT).toString());
        queries.setAggregationConfig(config.get(CONFIG_QUERY_AGGREGATE).toString());
    }

    /**
     * Activates the MongoDB Repository Service
     * 
     * @param compContext   The component context
     */
    @Activate
    void activate(ComponentContext compContext) throws Exception {
        logger.debug("Activating Service with configurati" + "on {}", compContext.getProperties());
        try {
            existingConfig = enhancedConfig.getConfigurationAsJson(compContext);
        } catch (RuntimeException ex) {
            logger.warn("Configuration invalid and could not be parsed, can not start MongoDB repository: "
                    + ex.getMessage(), ex);
            throw ex;
        }

        init(existingConfig);

        logger.info("Repository started.");
    }

    /**
     * Handle an existing activated service getting changed; 
     * e.g. configuration changes or dependency changes
     * 
     * @param compContext THe OSGI component context
     * @throws Exception if handling the modified event failed
     */
    @Modified
    void modified(ComponentContext compContext) throws Exception {
        logger.debug("Handle repository service modified notification");
        JsonValue newConfig = null;
        try {
            newConfig = enhancedConfig.getConfigurationAsJson(compContext);
        } catch (RuntimeException ex) {
            logger.warn("Configuration invalid and could not be parsed, can not start MongoDB repository", ex);
            throw ex;
        }
        if (existingConfig != null) {
            logger.info("(Re-)initialize repository with latest configuration.");
            init(newConfig);
        } else {
            // If the connection settings changed do a more complete re-initialization
            logger.info(
                    "Re-initialize repository with latest configuration - including connection setting changes.");
            deactivate(compContext);
            activate(compContext);
        }

        existingConfig = newConfig;
        logger.debug("Repository service modified");
    }

    @Deactivate
    void deactivate(ComponentContext compContext) {
        DBHelper.close();
        logger.debug("Deactivating Service {}", compContext);
    }
}