com.ibm.ws.lars.rest.PersistenceBean.java Source code

Java tutorial

Introduction

Here is the source code for com.ibm.ws.lars.rest.PersistenceBean.java

Source

/*******************************************************************************
 * Copyright (c) 2015 IBM Corp.
 *
 * Licensed 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 com.ibm.ws.lars.rest;

import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.inject.Singleton;

import org.bson.types.ObjectId;

import com.ibm.ws.lars.rest.SortOptions.SortOrder;
import com.ibm.ws.lars.rest.exceptions.InvalidJsonAssetException;
import com.ibm.ws.lars.rest.exceptions.NonExistentArtefactException;
import com.ibm.ws.lars.rest.exceptions.RepositoryException;
import com.ibm.ws.lars.rest.model.Asset;
import com.ibm.ws.lars.rest.model.AssetList;
import com.ibm.ws.lars.rest.model.Attachment;
import com.ibm.ws.lars.rest.model.AttachmentContentMetadata;
import com.ibm.ws.lars.rest.model.AttachmentContentResponse;
import com.ibm.ws.lars.rest.model.AttachmentList;
import com.mongodb.BasicDBList;
import com.mongodb.BasicDBObject;
import com.mongodb.BasicDBObjectBuilder;
import com.mongodb.DBCollection;
import com.mongodb.DBCursor;
import com.mongodb.DBObject;
import com.mongodb.gridfs.GridFS;
import com.mongodb.gridfs.GridFSDBFile;
import com.mongodb.gridfs.GridFSInputFile;

/**
 * Bean through which supports CRUD operations. All accesses to the database should go through this
 * class.
 *
 * Current limitations:<br>
 * - No logging<br>
 * - Little or no error handling<br>
 * - Currently only supports JSON Strings.<br>
 *
 * Perhaps we want to be communicating using a facade over the top of DBObjects to make the
 * interface a bit nicer.
 *
 */
@Singleton
public class PersistenceBean implements Persistor {

    private static final Logger logger = Logger.getLogger(PersistenceBean.class.getCanonicalName());

    private static final String ASSETS_COLLECTION = "assets";

    private static final String ATTACHMENTS_COLLECTION = "attachments";

    private static final List<String> searchIndexFields = Arrays
            .asList(new String[] { "name", "description", "shortDescription", "tags" });

    /** The _id field of a MongoDB object */
    private static String ID = "_id";

    private static final String DB_NAME = "mongo/larsDB";

    @Resource(lookup = DB_NAME)
    private com.mongodb.DB db;

    private GridFS gridFS;

    @PostConstruct
    public void createGridFS() {
        gridFS = new GridFS(db);
    }

    private DBCollection getAssetCollection() {
        return db.getCollection(ASSETS_COLLECTION);
    }

    private DBCollection getAttachmentCollection() {
        return db.getCollection(ATTACHMENTS_COLLECTION);
    }

    private DBObject makeQueryById(ObjectId id) {
        return new BasicDBObject(ID, id);
    }

    private static void convertObjectIdToHexString(DBObject obj) {
        Object objectIdObject = obj.get(ID);
        if ((objectIdObject != null) && (objectIdObject instanceof ObjectId)) {
            ObjectId objId = (ObjectId) objectIdObject;
            String hex = objId.toStringMongod();
            obj.put(ID, hex);
        }
    }

    private static void convertHexIdToObjectId(DBObject obj) {
        Object idObject = obj.get(ID);
        if ((idObject != null) && (idObject instanceof String)) {
            String hex = (String) idObject;
            obj.put(ID, new ObjectId(hex));
        }
    }

    @Override
    public AssetList retrieveAllAssets() {
        List<Map<String, Object>> mapList = new ArrayList<>();

        try (DBCursor cursor = getAssetCollection().find()) {
            if (logger.isLoggable(Level.FINE)) {
                logger.fine("retrieveAllAssets: found " + cursor.count() + " assets.");
            }
            for (DBObject obj : cursor) {
                convertObjectIdToHexString(obj);
                // BSON spec says that all keys have to be strings
                // so this should be safe.
                @SuppressWarnings("unchecked")
                Map<String, Object> assetMap = obj.toMap();
                mapList.add(assetMap);
            }
        }

        return AssetList.createAssetListFromMaps(mapList);
    }

    /** {@inheritDoc} */
    @Override
    public AssetList retrieveAllAssets(Map<String, List<Condition>> filters, String searchTerm,
            PaginationOptions pagination, SortOptions sortOptions) {

        if (filters.size() == 0 && searchTerm == null && pagination == null && sortOptions == null) {
            return retrieveAllAssets();
        }

        BasicDBObject filterObject = createFilterObject(filters, searchTerm);

        DBObject sortObject = null;
        DBObject projectionObject = null;
        boolean textScoreAdded = false;

        if (sortOptions != null) {
            // If sort options are provided, use them to sort the results
            int sortOrder = getMongoSortOrder(sortOptions.getSortOrder());
            sortObject = new BasicDBObject(sortOptions.getField(), sortOrder);
        } else {
            // If no sort options are provided but there is a search term, sort on relevance to the search term
            if (searchTerm != null) {
                sortObject = new BasicDBObject("score", new BasicDBObject("$meta", "textScore"));
                projectionObject = sortObject;
                textScoreAdded = true;
            }
        }

        List<DBObject> results = query(filterObject, sortObject, projectionObject, pagination);
        List<Map<String, Object>> assets = new ArrayList<Map<String, Object>>();
        for (DBObject result : results) {
            // BSON spec says that all keys have to be strings
            // so this should be safe.
            @SuppressWarnings("unchecked")
            Map<String, Object> resultMap = result.toMap();
            if (textScoreAdded) {
                resultMap.remove("score");
            }
            assets.add(resultMap);
        }
        return AssetList.createAssetListFromMaps(assets);
    }

    /** {@inheritDoc} */
    @Override
    public int countAllAssets(Map<String, List<Condition>> filters, String searchTerm) {
        BasicDBObject filterObject = createFilterObject(filters, searchTerm);
        return queryCount(filterObject);
    }

    /** {@inheritDoc} */
    @SuppressWarnings("unchecked")
    @Override
    public List<Object> getDistinctValues(String field, Map<String, List<Condition>> filters, String searchTerm) {
        return getAssetCollection().distinct(field, createFilterObject(filters, searchTerm));
    }

    /**
     * Create a filter object for a mongodb query from a filtermap and search term
     *
     * @param filters the filter map
     * @param searchTerm the search term
     * @return a filter object which can be passed as a query to mongodb find()
     */
    private BasicDBObject createFilterObject(Map<String, List<Condition>> filters, String searchTerm) {

        // Must return an empty object if there are no filters or search term
        if ((filters == null || filters.isEmpty()) && searchTerm == null) {
            return new BasicDBObject();
        }

        // Need to use a filterList and an $and operator because we may add multiple $or sections
        // which would overwrite each other if we just appended them to the filterObject
        BasicDBList filterList = new BasicDBList();
        BasicDBObject filterObject = new BasicDBObject("$and", filterList);

        for (Entry<String, List<Condition>> filter : filters.entrySet()) {
            List<Condition> conditions = filter.getValue();
            if (conditions.size() == 1) {
                filterList.add(createFilterObject(filter.getKey(), conditions.get(0)));
            } else {
                BasicDBList list = new BasicDBList();
                for (Condition condition : conditions) {
                    list.add(createFilterObject(filter.getKey(), condition));
                }
                filterList.add(new BasicDBObject("$or", list));
            }
        }

        if (searchTerm != null) {
            BasicDBObject value = new BasicDBObject("$search", searchTerm);
            BasicDBObject searchObject = new BasicDBObject("$text", value);
            filterList.add(searchObject);
        }

        return filterObject;
    }

    private BasicDBObject createFilterObject(String field, Condition condition) {
        Object value = null;
        switch (condition.getOperation()) {
        case EQUALS:
            value = condition.getValue();
            break;
        case NOT_EQUALS:
            value = new BasicDBObject("$ne", condition.getValue());
            break;
        }

        return new BasicDBObject(field, value);
    }

    private List<DBObject> query(DBObject filterObject, DBObject sortObject, DBObject projectionObject,
            PaginationOptions pagination) {

        if (logger.isLoggable(Level.FINE)) {
            logger.fine("query: Querying database with query object " + filterObject);
            logger.fine("query: sort object " + sortObject);
            logger.fine("query: projection object " + projectionObject);
            logger.fine("query: pagination object " + pagination);
        }

        List<DBObject> results = new ArrayList<DBObject>();
        try (DBCursor cursor = getAssetCollection().find(filterObject, projectionObject)) {
            if (logger.isLoggable(Level.FINE)) {
                logger.fine("query: found " + cursor.count() + " assets.");
            }

            if (pagination != null) {
                cursor.skip(pagination.getOffset());
                cursor.limit(pagination.getLimit());
            }

            if (sortObject != null) {
                cursor.sort(sortObject);
            }

            for (DBObject obj : cursor) {
                convertObjectIdToHexString(obj);
                results.add(obj);
            }
        }
        return results;
    }

    private int queryCount(DBObject filterObject) {
        if (logger.isLoggable(Level.FINE)) {
            logger.fine("queryCount: Querying database with query object " + filterObject);
        }

        DBCursor cursor = getAssetCollection().find(filterObject);
        int count = cursor.count();

        if (logger.isLoggable(Level.FINE)) {
            logger.fine("queryCount: found " + count + " assets.");
        }

        return count;
    }

    private int getMongoSortOrder(SortOrder sortOrder) {
        switch (sortOrder) {
        case ASCENDING:
            return 1;
        case DESCENDING:
            return -1;
        default:
            throw new RepositoryException("Invalid sort order: " + sortOrder);
        }
    }

    @Override
    public Asset retrieveAsset(String assetId) throws NonExistentArtefactException {
        return retrieveAsset(new ObjectId(assetId));
    }

    /**
     * Retrieve a single asset by its id.
     *
     * @return The requested asset
     * @throws NonExistentArtefactException if the asset doesn't exist
     */
    private Asset retrieveAsset(ObjectId assetId) throws NonExistentArtefactException {
        BasicDBObject query = new BasicDBObject(ID, assetId);
        DBObject resultObj = getAssetCollection().findOne(query);
        if (resultObj == null) {
            throw new NonExistentArtefactException(assetId.toString(), "asset");
        }
        convertObjectIdToHexString(resultObj);
        // All entries in a Mongo document have string keys, this is part of
        // the BSON spec, so this should be safe. Not very nice though.
        @SuppressWarnings("unchecked")
        Map<String, Object> asset = resultObj.toMap();
        return Asset.createAssetFromMap(asset);
    }

    @Override
    public Asset createAsset(Asset newAsset) throws InvalidJsonAssetException {

        DBObject obj = new BasicDBObject(newAsset.getProperties());
        convertHexIdToObjectId(obj);

        DBCollection coll = getAssetCollection();

        if (logger.isLoggable(Level.FINE)) {
            logger.fine("createAsset: inserting object into the database: " + obj);
        }

        coll.insert(obj);

        Asset createdAsset = null;
        try {
            createdAsset = retrieveAsset((ObjectId) obj.get(ID));
        } catch (NonExistentArtefactException e) {
            // This should not happen. If it does it is a repository bug
            throw new RepositoryException("Created asset could not be retrieved from the database.", e);
        }

        return createdAsset;
    }

    @Override
    public Asset updateAsset(String assetId, Asset asset)
            throws InvalidJsonAssetException, NonExistentArtefactException {
        if (!Objects.equals(assetId, asset.get_id())) {
            throw new InvalidJsonAssetException("The specified asset id does not match the specified asset.");
        }

        DBCollection coll = getAssetCollection();

        ObjectId objId = new ObjectId(assetId);
        DBObject query = makeQueryById(objId);

        DBObject obj = new BasicDBObject(asset.getProperties());
        convertHexIdToObjectId(obj);

        if (logger.isLoggable(Level.FINE)) {
            String msg = "updateAsset: query object: " + query + "\nupdated asset:" + obj;
            logger.fine(msg);
        }

        coll.update(query, obj);

        return retrieveAsset(objId);
    }

    /**
     * Delete the asset with the specified id.
     */
    @Override
    public void deleteAsset(String assetId) {
        DBCollection coll = getAssetCollection();
        DBObject query = new BasicDBObject(ID, new ObjectId(assetId));
        coll.remove(query);
    }

    /**
     * @param attachmentContentStream
     * @return
     */
    @Override
    public AttachmentContentMetadata createAttachmentContent(String name, String contentType,
            InputStream attachmentContentStream) {
        // Do not specify a bucket (so the data will be stored in fs.files and fs.chunks)
        GridFSInputFile gfsFile = gridFS.createFile(attachmentContentStream);
        ObjectId id = new ObjectId();
        gfsFile.setContentType(contentType);
        gfsFile.setId(id);
        String filename = id.toString();
        gfsFile.setFilename(filename);
        gfsFile.save();

        return new AttachmentContentMetadata(gfsFile.getFilename(), gfsFile.getLength());
    }

    /**
     * @param attachment
     * @return
     */
    @Override
    public Attachment createAttachmentMetadata(Attachment attachment) {
        BasicDBObject state = new BasicDBObject(attachment.getProperties());
        convertHexIdToObjectId(state);

        DBCollection coll = getAttachmentCollection();
        if (logger.isLoggable(Level.FINE)) {
            logger.fine("createAttachmentMetadata: inserting new attachment " + state);
        }
        coll.insert(state);
        Object idObject = state.get(ID);
        String id;
        if (idObject instanceof String) {
            id = (String) idObject;
        } else if (idObject instanceof ObjectId) {
            id = ((ObjectId) idObject).toStringMongod();
        } else {
            throw new AssertionError("_id should be either String of ObjectId");
        }
        try {
            return retrieveAttachmentMetadata(id);
        } catch (NonExistentArtefactException e) {
            throw new RepositoryException("Created attachment could not be retrieved from the persistence store",
                    e);
        }
    }

    @Override
    public Attachment retrieveAttachmentMetadata(String attachmentId) throws NonExistentArtefactException {
        BasicDBObject query = new BasicDBObject(ID, new ObjectId(attachmentId));
        DBObject resultObj = getAttachmentCollection().findOne(query);
        if (resultObj == null) {
            throw new NonExistentArtefactException(attachmentId, "attachment");
        }
        convertObjectIdToHexString(resultObj);

        // All entries in a Mongo document have string keys, this is part of
        // the BSON spec, so this should be safe. Not very nice though.
        @SuppressWarnings("unchecked")
        Map<String, Object> map = resultObj.toMap();
        return Attachment.createAttachmentFromMap(map);
    }

    @Override
    public void deleteAttachmentContent(String attachmentId) {
        gridFS.remove(attachmentId);
    }

    @Override
    public void deleteAttachmentMetadata(String attachmentId) {
        DBObject query = new BasicDBObject(ID, new ObjectId(attachmentId));
        getAttachmentCollection().remove(query);
    }

    @Override
    public AttachmentList findAttachmentsForAsset(String assetId) {
        BasicDBObject query = new BasicDBObject("assetId", assetId);
        ArrayList<Map<String, Object>> results = new ArrayList<Map<String, Object>>();
        try (DBCursor cursor = getAttachmentCollection().find(query)) {
            if (logger.isLoggable(Level.FINE)) {
                logger.fine(
                        "findAttachmentsForAsset: found " + cursor.count() + " attachments for asset " + assetId);
            }

            for (DBObject attachment : cursor) {
                convertObjectIdToHexString(attachment);
                @SuppressWarnings("unchecked")
                Map<String, Object> oneResult = attachment.toMap();
                results.add(oneResult);
            }
        }

        return AttachmentList.createAttachmentListFromMaps(results);
    }

    /**
     * Returns an InputStream of the content of the attachment or null if the attachment does not
     * exist.
     *
     * @throws NonExistentArtefactException
     */
    @Override
    public AttachmentContentResponse retrieveAttachmentContent(String gridFSId)
            throws NonExistentArtefactException {
        GridFSDBFile file = gridFS.findOne(gridFSId);

        if (file != null) {
            InputStream contentStream = file.getInputStream();
            String contentType = file.getContentType();
            return new AttachmentContentResponse(contentStream, contentType);
        } else {
            throw new NonExistentArtefactException();
        }
    }

    /** {@inheritDoc} */
    @Override
    public String allocateNewId() {
        return new ObjectId().toStringMongod();
    }

    /** {@inheritDoc} */
    @Override
    public void initialize() {
        // Make sure the fields we want to query are indexed
        DBCollection assets = db.getCollection(ASSETS_COLLECTION);
        DBCollection attachments = db.getCollection(ATTACHMENTS_COLLECTION);

        // Add text index
        BasicDBObjectBuilder textIndex = BasicDBObjectBuilder.start();
        for (String indexField : searchIndexFields) {
            textIndex.add(indexField, "text");
        }
        assets.ensureIndex(textIndex.get());

        // Add Attachment(assetId) index
        attachments.ensureIndex(new BasicDBObject("assetId", 1));
    }
}