com.redhat.lightblue.savedsearch.SavedSearchCache.java Source code

Java tutorial

Introduction

Here is the source code for com.redhat.lightblue.savedsearch.SavedSearchCache.java

Source

/*
 Copyright 2013 Red Hat, Inc. and/or its affiliates.
    
 This file is part of lightblue.
    
 This program is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation, either version 3 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 General Public License for more details.
    
 You should have received a copy of the GNU General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.redhat.lightblue.savedsearch;

import java.util.Iterator;
import java.util.Objects;
import java.util.List;
import java.util.ArrayList;

import java.util.concurrent.TimeUnit;

import java.io.IOException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.TextNode;
import com.fasterxml.jackson.databind.node.NullNode;

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;

import com.redhat.lightblue.Request;
import com.redhat.lightblue.Response;
import com.redhat.lightblue.ClientIdentification;
import com.redhat.lightblue.EntityVersion;

import com.redhat.lightblue.config.SavedSearchConfiguration;

import com.redhat.lightblue.crud.FindRequest;
import com.redhat.lightblue.crud.CrudConstants;

import com.redhat.lightblue.mediator.Mediator;

import com.redhat.lightblue.metadata.EntityMetadata;

import com.redhat.lightblue.query.*;

import com.redhat.lightblue.util.Error;
import com.redhat.lightblue.util.Path;
import com.redhat.lightblue.util.JsonUtils;

/**
 * This class is the main access point to saved searches. It loads
 * saved searches from the db, and keeps them in a weak cache.
 *
 * We only provide the basics to implement saved search functionality
 * at this level. The functionality must be enabled at the higher
 * layer, REST or embedded application layers.
 *
 * The implementation should:
 * <ul>
 * <li>Instantiate a singleton instance of the SavedSearchCache. This 
 * should be shared among all threads.</li>
 * <li>Determine the name of the saved search, the entity, and its version.
 * The saved search name is meaningful only with its associated entity<li?.
 * <li>Retrieve the saved search document using SavedSearchCache.getSavedSearch</li>
 * <li>Collect the query parameters, and fill in defaults using FindRequestBuilder.fillDefaults</li>
 * <li>Prepare a FindRequest using FindRequestBuilder.buildRequest.</li>
 * <li>Call find()</li>
 * </ul>
 */
public class SavedSearchCache {
    private static final Logger LOGGER = LoggerFactory.getLogger(SavedSearchCache.class);

    public static final String ERR_SAVED_SEARCH = "crud:saved-search";

    Cache<Key, ObjectNode> cache;

    private static final Path P_NAME = new Path("name");
    private static final Path P_ENTITY = new Path("entity");
    private static final Path P_VERSIONS = new Path("versions");

    private static final Value NULL_VALUE = new Value(null);

    public final String savedSearchEntity;
    public final String savedSearchVersion;

    public static class RetrievalError extends RuntimeException {
        public final List<Error> errors;

        public RetrievalError(List<Error> errors) {
            this.errors = errors;
        }

        @Override
        public String toString() {
            return errors.toString() + "\n" + super.toString();
        }
    }

    private static class Key {
        final String searchName;
        final String entity;
        final String version;

        @Override
        public boolean equals(Object o) {
            if (o instanceof Key) {
                return Objects.equals(((Key) o).searchName, searchName) && Objects.equals(((Key) o).entity, entity)
                        && Objects.equals(((Key) o).version, version);
            }
            return false;
        }

        @Override
        public int hashCode() {
            return (version == null ? 1 : version.hashCode()) * searchName.hashCode() * entity.hashCode();
        }

        @Override
        public String toString() {
            return searchName + ":" + entity + ":" + version;
        }

        public Key(String searchName, String entity, String version) {
            this.searchName = searchName;
            this.entity = entity;
            this.version = version;
        }
    }

    public SavedSearchCache(SavedSearchConfiguration cfg) {
        if (cfg != null) {
            savedSearchEntity = cfg.getEntity();
            savedSearchVersion = cfg.getEntityVersion();
            initializeCache(cfg.getCacheConfig());
        } else {
            savedSearchEntity = "savedSearch";
            savedSearchVersion = null;
            initializeCache(null);
        }
    }

    private void initializeCache(String spec) {
        if (spec == null) {
            cache = CacheBuilder.newBuilder().maximumSize(2048).expireAfterAccess(2, TimeUnit.MINUTES).softValues()
                    .build();
        } else {
            cache = CacheBuilder.from(spec).build();
        }
    }

    /**
     * Retrieves a saved search from the database. 
     *
     * @param m Mediator instance
     * @param clid The client id
     * @param searchName name of the saved search
     * @param entit Name of the entity
     * @param version Entity version the search should run on
     * 
     * The returned JsonNode can be an ObjectNode, or an ArrayNode
     * containing zero or more documents. If there are more than one
     * documents, only one of them is for the requested version, and
     * the other is for the null version that applies to all
     * versions. It returns null or empty array if nothing is found.
     * In case of retrieval error, a RetrievalError is thrown
     * containing the errors.
     */
    public JsonNode getSavedSearchFromDB(Mediator m, ClientIdentification clid, String searchName, String entity,
            String version) {

        FindRequest findRequest = new FindRequest();
        findRequest.setEntityVersion(new EntityVersion(savedSearchEntity, savedSearchVersion));
        findRequest.setClientId(clid);
        List<Value> versionList = new ArrayList<>(2);
        versionList.add(NULL_VALUE);
        // Include all segments of the version in the search list
        if (version != null) {
            int index = 0;
            while ((index = version.indexOf('.', index)) != -1) {
                versionList.add(new Value(version.substring(0, index)));
                index++;
            }
            versionList.add(new Value(version));
        }
        QueryExpression q = new NaryLogicalExpression(NaryLogicalOperator._and,
                new ValueComparisonExpression(P_NAME, BinaryComparisonOperator._eq, new Value(searchName)),
                new ValueComparisonExpression(P_ENTITY, BinaryComparisonOperator._eq, new Value(entity)),
                new ArrayContainsExpression(P_VERSIONS, ContainsOperator._any, versionList));
        LOGGER.debug("Searching {}", q);
        findRequest.setQuery(q);
        findRequest.setProjection(FieldProjection.ALL);
        Response response = m.find(findRequest);
        if (response.getErrors() != null && !response.getErrors().isEmpty())
            throw new RetrievalError(response.getErrors());
        LOGGER.debug("Found {}", response.getEntityData());
        return response.getEntityData();
    }

    /**
     * Either loads the saved search from the db, or from the
     * cache. 
     *
     * @param m Mediator instance
     * @param clid The client id
     * @param searchName name of the saved search
     * @param entit Name of the entity
     * @param version Entity version the search should run on
     *
     * @return The saved search document, or null if not found
     */
    public JsonNode getSavedSearch(Mediator m, ClientIdentification clid, String searchName, String entity,
            String version) {
        LOGGER.debug("Loading {}:{}:{}", searchName, entity, version);
        ObjectNode doc = null;
        String loadVersion;
        if (version == null) {
            LOGGER.debug("{} version is null, attempting to find default version for entity", entity);
            EntityMetadata md = m.metadata.getEntityMetadata(entity, null);
            if (md == null)
                throw Error.get(CrudConstants.ERR_UNKNOWN_ENTITY, entity + ":" + version);
            loadVersion = md.getVersion().getValue();
            LOGGER.debug("Loading {}:{}:{}", searchName, entity, loadVersion);
        } else {
            loadVersion = version;
        }
        Key key = new Key(searchName, entity, loadVersion);
        LOGGER.debug("Lookup {}", key);
        doc = cache.getIfPresent(key);
        if (doc == null) {
            key = new Key(searchName, entity, null);
            LOGGER.debug("Lookup {}", key);
            doc = cache.getIfPresent(key);
        }
        if (doc == null) {
            LOGGER.debug("Loading {} from DB", searchName);
            JsonNode node = getSavedSearchFromDB(m, clid, searchName, entity, loadVersion);
            if (node instanceof ObjectNode) {
                LOGGER.debug("Loaded a single search");
                doc = (ObjectNode) node;
                store(doc);
            } else if (node instanceof ArrayNode) {
                LOGGER.debug("Loaded an array of searches");
                store((ArrayNode) node);
                doc = findDocForVersion((ArrayNode) node, loadVersion);
            }
            if (doc != null) {
                store(doc);
            }
        }
        return doc;
    }

    private ObjectNode findDocForVersion(ArrayNode node, String version) {
        LOGGER.debug("Searching {} in the array", version);
        ObjectNode ret = null;
        String matchedVersion = null;
        for (Iterator<JsonNode> itr = node.elements(); itr.hasNext();) {
            JsonNode searchNode = itr.next();
            if (searchNode instanceof ObjectNode) {
                JsonNode versionsNode = searchNode.get("versions");
                LOGGER.debug("Versions in search:{}", versionsNode);
                if (versionsNode instanceof ArrayNode && versionsNode.size() > 0) {
                    LOGGER.debug("Looking up in versions array");
                    for (Iterator<JsonNode> vitr = versionsNode.elements(); vitr.hasNext();) {
                        JsonNode versionNode = vitr.next();
                        if (versionNode instanceof NullNode) {
                            if (betterMatch(version, null, matchedVersion)) {
                                ret = (ObjectNode) searchNode;
                                matchedVersion = null;
                            }
                        } else if (versionNode instanceof TextNode) {
                            String v = versionNode.asText();
                            if (betterMatch(version, v, matchedVersion)) {
                                ret = (ObjectNode) searchNode;
                                matchedVersion = v;
                            }
                        }
                    }
                } else {
                    if (ret == null) {
                        ret = (ObjectNode) searchNode;
                        matchedVersion = null;
                    }
                }
                LOGGER.debug("Current best match after {}:{}", searchNode, ret);
            }
        }
        LOGGER.debug("Best match for version {}:{}", version, ret);
        return ret;
    }

    /**
     * Returns true if newVersion is a better match to searchedVersion than matchedVersion
     */
    private boolean betterMatch(String searchedVersion, String newVersion, String matchedVersion) {
        if (searchedVersion == null) {
            return newVersion == null;
        } else {
            if (newVersion == null) { // Match any version
                return matchedVersion == null;
            } else {
                // newVersion not null
                // If searched version is this version, than it is a perfect match
                if (searchedVersion.equals(newVersion)) {
                    return true;
                } else {
                    // if newVersion is a longer prefix of searchVersion than matchedVersion is, then it is a better match
                    int newMatchingPrefix = getMatchingPrefix(searchedVersion, newVersion);
                    int oldMatchingPrefix = getMatchingPrefix(searchedVersion, matchedVersion);
                    LOGGER.debug("Comparing to {}: {}: prefix={} {}: prefix={}", searchedVersion, newVersion,
                            newMatchingPrefix, matchedVersion, oldMatchingPrefix);
                    return newMatchingPrefix > oldMatchingPrefix;
                }
            }
        }
    }

    /**
     * Return the length of the matching version prefix
     */
    private int getMatchingPrefix(String fullVersion, String prefix) {
        if (prefix == null) {
            return 0;
        } else {
            int p = prefix.length();
            if (p <= fullVersion.length()) {
                if (fullVersion.startsWith(prefix)) {
                    if (p < fullVersion.length()) {
                        if (fullVersion.charAt(p) == '.') {
                            return prefix.length();
                        } else {
                            return 0;
                        }
                    } else {
                        return fullVersion.length();
                    }
                } else {
                    return 0;
                }
            } else {
                return 0;
            }
        }
    }

    private synchronized void store(ObjectNode doc) {
        String name = doc.get("name").asText();
        String entity = doc.get("entity").asText();
        ArrayNode arr = (ArrayNode) doc.get("versions");
        Key key = null;
        if (arr != null) {
            for (Iterator<JsonNode> itr = arr.elements(); itr.hasNext();) {
                JsonNode version = itr.next();
                if (version instanceof NullNode) {
                    key = new Key(name, entity, null);
                } else {
                    key = new Key(name, entity, version.asText());
                }
            }
        }
        if (key == null) {
            key = new Key(name, entity, null);
        }
        LOGGER.debug("Adding {} to cache", key);
        cache.put(key, doc);
    }

    private synchronized void store(ArrayNode arr) {
        for (Iterator<JsonNode> itr = arr.elements(); itr.hasNext();) {
            JsonNode node = itr.next();
            if (node instanceof ObjectNode)
                store((ObjectNode) node);
        }
    }
}