com.burkeware.search.api.internal.lucene.DefaultIndexer.java Source code

Java tutorial

Introduction

Here is the source code for com.burkeware.search.api.internal.lucene.DefaultIndexer.java

Source

/**
 * The contents of this file are subject to the OpenMRS Public License
 * Version 1.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://license.openmrs.org
 *
 * Software distributed under the License is distributed on an "AS IS"
 * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
 * License for the specific language governing rights and limitations
 * under the License.
 *
 * Copyright (C) OpenMRS, LLC.  All Rights Reserved.
 */
package com.burkeware.search.api.internal.lucene;

import com.burkeware.search.api.internal.provider.SearcherProvider;
import com.burkeware.search.api.internal.provider.WriterProvider;
import com.burkeware.search.api.logger.Logger;
import com.burkeware.search.api.registry.Registry;
import com.burkeware.search.api.resource.Resource;
import com.burkeware.search.api.resource.SearchableField;
import com.burkeware.search.api.serialization.Algorithm;
import com.burkeware.search.api.util.CollectionUtil;
import com.burkeware.search.api.util.StreamUtil;
import com.burkeware.search.api.util.StringUtil;
import com.google.inject.Inject;
import com.google.inject.name.Named;
import com.jayway.jsonpath.JsonPath;
import net.minidev.json.JSONArray;
import net.minidev.json.JSONObject;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.queryParser.ParseException;
import org.apache.lucene.queryParser.QueryParser;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.util.Version;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

public class DefaultIndexer implements Indexer {

    private Logger logger;

    private IndexWriter indexWriter;

    private IndexSearcher indexSearcher;

    private WriterProvider writerProvider;

    private SearcherProvider searcherProvider;

    private Registry<String, Resource> resourceRegistry;

    private final QueryParser parser;

    private static final String DEFAULT_FIELD_UUID = "_uuid";

    private static final String DEFAULT_FIELD_JSON = "_json";

    private static final String DEFAULT_FIELD_CLASS = "_class";

    private static final String DEFAULT_FIELD_RESOURCE = "_resource";

    private static final Integer DEFAULT_MAX_DOCUMENTS = 20;

    @Inject
    protected DefaultIndexer(final @Named("configuration.lucene.document.key") String defaultField,
            final Version version, final Analyzer analyzer) {
        this.parser = new QueryParser(version, defaultField, analyzer);
    }

    /**
     * Private Getter and Setter section **
     */

    private Logger getLogger() {
        return logger;
    }

    @Inject
    private void setLogger(final Logger logger) {
        this.logger = logger;
    }

    private IndexWriter getIndexWriter() throws IOException {
        if (indexWriter == null)
            indexWriter = getWriterProvider().get();
        return indexWriter;
    }

    private void setIndexWriter(final IndexWriter indexWriter) {
        this.indexWriter = indexWriter;
    }

    private IndexSearcher getIndexSearcher() throws IOException {
        try {
            if (indexSearcher == null)
                indexSearcher = getSearcherProvider().get();
        } catch (IOException e) {
            // silently ignoring this exception.
        }
        return indexSearcher;
    }

    private void setIndexSearcher(final IndexSearcher indexSearcher) {
        this.indexSearcher = indexSearcher;
    }

    private WriterProvider getWriterProvider() {
        return writerProvider;
    }

    @Inject
    private void setWriterProvider(final WriterProvider writerProvider) {
        this.writerProvider = writerProvider;
    }

    private SearcherProvider getSearcherProvider() {
        return searcherProvider;
    }

    @Inject
    private void setSearcherProvider(final SearcherProvider searcherProvider) {
        this.searcherProvider = searcherProvider;
    }

    private Registry<String, Resource> getResourceRegistry() {
        return resourceRegistry;
    }

    @Inject
    private void setResourceRegistry(final Registry<String, Resource> resourceRegistry) {
        this.resourceRegistry = resourceRegistry;
    }

    /**
     * Low level lucene operation **
     */

    /**
     * Commit the changes in the index. This method will ensure that deletion, update and addition to the lucene index
     * are written to the filesystem (persisted).
     *
     * @throws IOException when the operation encounter errors.
     */
    @Override
    public void commit() throws IOException {
        if (getIndexWriter() != null) {
            getIndexWriter().commit();
            getIndexWriter().close();
        }
        // remove the instance
        setIndexWriter(null);
        setIndexSearcher(null);
    }

    /**
     * Create a single term lucene query. The value for the query will be surrounded with single quote.
     *
     * @param field the field on which the query should be performed.
     * @param value the value for the field
     * @return the valid lucene query for single term.
     */
    private String createQuery(final String field, final String value) {
        return "(" + field + ":" + StringUtil.quote(value) + ")";
    }

    /**
     * Create lucene query string based on the searchable field name and value. The values for the searchable field
     * will be retrieved from the <code>jsonObject</code>. This method will try to create a unique query in the case
     * where a searchable field is marked as unique. Otherwise the method will create a query string using all
     * available searchable fields.
     *
     * @param jsonObject       the json object from which the value for each field can be retrieved from.
     * @param searchableFields the searchable fields definition
     * @return query string which could be either a unique or full searchable field based query.
     */
    private String createSearchableFieldQuery(final Object jsonObject,
            final List<SearchableField> searchableFields) {
        boolean uniqueExists = false;
        StringBuilder fullQuery = new StringBuilder();
        StringBuilder uniqueQuery = new StringBuilder();
        for (SearchableField searchableField : searchableFields) {
            String value = JsonPath.read(jsonObject, searchableField.getExpression()).toString();
            String query = createQuery(searchableField.getName(), value);

            if (searchableField.isUnique()) {
                uniqueExists = true;
                if (!StringUtil.isBlank(uniqueQuery.toString()))
                    uniqueQuery.append(" AND ");
                uniqueQuery.append(query);
            }

            // only create the full query if we haven't found any unique key in the searchable fields.
            if (!uniqueExists) {
                if (!StringUtil.isBlank(fullQuery.toString()))
                    fullQuery.append(" AND ");
                fullQuery.append(query);
            }
        }

        if (uniqueExists)
            return uniqueQuery.toString();
        else
            return fullQuery.toString();
    }

    /**
     * Create query fragment for a certain class. Calling this method will ensure the documents returned will of the
     * <code>clazz</code> type meaning the documents can be converted into object of type <code>clazz</code>. Converting
     * the documents need to be performed by getting the correct resource object from the document and then calling the
     * serialize method or getting the algorithm class and perform the serialization process from the algorithm object.
     * <p/>
     * Example use case: please retrieve all patients data. This should be performed by querying all object of certain
     * class type  because the caller is interested only in the type of object, irregardless of the resources from
     * which the objects are coming from.
     *
     * @param clazz the clazz for which the query is based on
     * @return the base query for a resource
     */
    private String createClassQuery(final Class clazz) {
        StringBuilder builder = new StringBuilder();
        builder.append(createQuery(DEFAULT_FIELD_CLASS, clazz.getName()));
        return builder.toString();
    }

    /**
     * Create query for a certain resource object. Calling this method will ensure the documents returned will be
     * documents that was indexed using the resource object.
     *
     * @param resource the resource for which the query is based on
     * @return the base query for a resource
     */
    private String createResourceQuery(final Resource resource) {
        StringBuilder builder = new StringBuilder();
        builder.append(createQuery(DEFAULT_FIELD_RESOURCE, resource.getName()));
        return builder.toString();
    }

    /**
     * Search the local lucene repository for documents with similar information with information inside the
     * <code>query</code>. Search can return multiple documents with similar information or empty list when no
     * document have similar information with the <code>query</code>.
     *
     * @param query the lucene query.
     * @return objects with similar information with the query.
     * @throws IOException when the search encounter error.
     */
    private List<Document> findDocuments(final Query query) throws IOException {
        List<Document> documents = new ArrayList<Document>();
        IndexSearcher searcher = getIndexSearcher();
        if (searcher != null) {
            TopDocs docs = searcher.search(query, DEFAULT_MAX_DOCUMENTS);
            ScoreDoc[] hits = docs.scoreDocs;
            for (ScoreDoc hit : hits)
                documents.add(searcher.doc(hit.doc));
        }
        return documents;
    }

    /**
     * Write json representation of a single object as a single document entry inside Lucene index.
     *
     * @param jsonObject the json object to be written to the index
     * @param resource   the configuration to transform json to lucene document
     * @param writer     the lucene index writer
     * @throws java.io.IOException when writing document failed
     */
    private void writeObject(final Object jsonObject, final Resource resource, final IndexWriter writer)
            throws IOException {

        Document document = new Document();
        document.add(new Field(DEFAULT_FIELD_JSON, jsonObject.toString(), Field.Store.YES, Field.Index.NO));
        document.add(new Field(DEFAULT_FIELD_UUID, UUID.randomUUID().toString(), Field.Store.YES,
                Field.Index.ANALYZED_NO_NORMS));
        document.add(new Field(DEFAULT_FIELD_CLASS, resource.getResourceObject().getName(), Field.Store.YES,
                Field.Index.ANALYZED_NO_NORMS));
        document.add(new Field(DEFAULT_FIELD_RESOURCE, resource.getName(), Field.Store.YES,
                Field.Index.ANALYZED_NO_NORMS));

        for (SearchableField searchableField : resource.getSearchableFields()) {
            Object value = JsonPath.read(jsonObject, searchableField.getExpression());
            document.add(new Field(searchableField.getName(), String.valueOf(value), Field.Store.YES,
                    Field.Index.ANALYZED_NO_NORMS));
        }

        if (getLogger().isDebugEnabled())
            getLogger().debug(this.getClass().getSimpleName(), "Writing document: " + document);

        writer.addDocument(document);
    }

    /**
     * Delete an entry from the lucene index. The method will search for a single entry in the index (throwing
     * IOException when more than one index match the object).
     *
     * @param jsonObject  the json object to be deleted.
     * @param resource    the resource definition used to register the json to lucene index.
     * @param indexWriter the index writer used to delete the index.
     * @throws ParseException when the json can't be used to create a query to identify the correct lucene index.
     * @throws IOException    when other error happens during the deletion process.
     */
    private void deleteObject(final Object jsonObject, final Resource resource, final IndexWriter indexWriter)
            throws ParseException, IOException {
        String queryString = createResourceQuery(resource) + " AND "
                + createSearchableFieldQuery(jsonObject, resource.getSearchableFields());

        if (getLogger().isDebugEnabled())
            getLogger().debug(this.getClass().getSimpleName(), "Query deleteObject(): " + queryString);

        Query query = parser.parse(queryString);
        List<Document> documents = findDocuments(query);
        if (!CollectionUtil.isEmpty(documents) && documents.size() > 1)
            throw new IOException("Unable to uniquely identify an object using the json object in the repository.");
        indexWriter.deleteDocuments(query);
    }

    /**
     * Update an object inside the lucene index with a new data. Updating process practically means deleting old object
     * and then adding the new object.
     *
     * @param jsonObject  the json object to be updated.
     * @param resource    the resource definition used to register the json to lucene index.
     * @param indexWriter the index writer used to delete the index.
     * @throws ParseException when the json can't be used to create a query to identify the correct lucene index.
     * @throws IOException    when other error happens during the deletion process.
     */
    private void updateObject(final Object jsonObject, final Resource resource, final IndexWriter indexWriter)
            throws ParseException, IOException {
        // search for the same object, if they exists, delete them :)
        deleteObject(jsonObject, resource, indexWriter);
        // write the new object
        writeObject(jsonObject, resource, indexWriter);
    }

    @Override
    public void loadObjects(final Resource resource, final InputStream inputStream)
            throws ParseException, IOException {
        InputStreamReader reader = new InputStreamReader(inputStream);
        loadObjects(resource, reader);
    }

    @Override
    public void loadObjects(final Resource resource, final Reader reader) throws ParseException, IOException {
        String json = StreamUtil.readAsString(reader);
        Object jsonObject = JsonPath.read(json, resource.getRootNode());
        if (jsonObject instanceof JSONArray) {
            JSONArray array = (JSONArray) jsonObject;
            for (Object element : array)
                updateObject(element, resource, getIndexWriter());
        } else if (jsonObject instanceof JSONObject) {
            updateObject(jsonObject, resource, getIndexWriter());
        }
    }

    @Override
    public <T> T getObject(final String key, final Class<T> clazz) throws ParseException, IOException {
        T object = null;

        String queryString = createClassQuery(clazz);
        if (!StringUtil.isEmpty(key))
            queryString = queryString + " AND " + key;

        if (getLogger().isDebugEnabled())
            getLogger().debug(this.getClass().getSimpleName(), "Query getObject(String, Class): " + queryString);

        Query query = parser.parse(queryString);
        List<Document> documents = findDocuments(query);

        if (!CollectionUtil.isEmpty(documents) && documents.size() > 1)
            throw new IOException(
                    "Unable to uniquely identify an object using key: '" + key + "'in the repository.");

        for (Document document : documents) {
            String resourceName = document.get(DEFAULT_FIELD_RESOURCE);
            Resource resource = getResourceRegistry().getEntryValue(resourceName);
            Algorithm algorithm = resource.getAlgorithm();
            String json = document.get(DEFAULT_FIELD_JSON);
            object = clazz.cast(algorithm.deserialize(json));
        }

        return object;
    }

    @Override
    public Object getObject(final String key, final Resource resource) throws ParseException, IOException {
        Object object = null;

        String queryString = createResourceQuery(resource);
        if (!StringUtil.isEmpty(key))
            queryString = queryString + " AND " + key;

        if (getLogger().isDebugEnabled())
            getLogger().debug(this.getClass().getSimpleName(),
                    "Query getObject(String,  Resource): " + queryString);

        Query query = parser.parse(queryString);
        List<Document> documents = findDocuments(query);

        if (!CollectionUtil.isEmpty(documents) && documents.size() > 1)
            throw new IOException(
                    "Unable to uniquely identify an object using key: '" + key + "'in the repository.");

        Algorithm algorithm = resource.getAlgorithm();
        for (Document document : documents) {
            String json = document.get(DEFAULT_FIELD_JSON);
            object = algorithm.deserialize(json);
        }

        return object;
    }

    @Override
    public <T> List<T> getObjects(final String searchString, final Class<T> clazz)
            throws ParseException, IOException {
        List<T> objects = new ArrayList<T>();

        String queryString = createClassQuery(clazz);
        if (!StringUtil.isEmpty(searchString))
            queryString = queryString + " AND " + searchString;

        if (getLogger().isDebugEnabled())
            getLogger().debug(this.getClass().getSimpleName(), "Query getObjects(String, Class): " + queryString);

        Query query = parser.parse(queryString);
        List<Document> documents = findDocuments(query);
        for (Document document : documents) {
            String resourceName = document.get(DEFAULT_FIELD_RESOURCE);
            Resource resource = getResourceRegistry().getEntryValue(resourceName);
            Algorithm algorithm = resource.getAlgorithm();
            String json = document.get(DEFAULT_FIELD_JSON);
            objects.add(clazz.cast(algorithm.deserialize(json)));
        }
        return objects;
    }

    @Override
    public List<Object> getObjects(final String searchString, final Resource resource)
            throws ParseException, IOException {
        List<Object> objects = new ArrayList<Object>();

        String queryString = createResourceQuery(resource);
        if (!StringUtil.isEmpty(searchString))
            queryString = queryString + " AND " + searchString;

        if (getLogger().isDebugEnabled())
            getLogger().debug(this.getClass().getSimpleName(),
                    "Query getObjects(String, Resource): " + queryString);

        Query query = parser.parse(queryString);
        List<Document> documents = findDocuments(query);
        Algorithm algorithm = resource.getAlgorithm();
        for (Document document : documents) {
            String json = document.get(DEFAULT_FIELD_JSON);
            objects.add(algorithm.deserialize(json));
        }
        return objects;
    }

    @Override
    public Object createObject(final Object object, final Resource resource) throws ParseException, IOException {
        String jsonString = resource.serialize(object);
        Object jsonObject = JsonPath.read(jsonString, "$");
        writeObject(jsonObject, resource, getIndexWriter());
        commit();
        return object;
    }

    @Override
    public Object deleteObject(final Object object, final Resource resource) throws ParseException, IOException {
        String jsonString = resource.serialize(object);
        Object jsonObject = JsonPath.read(jsonString, "$");
        deleteObject(jsonObject, resource, getIndexWriter());
        commit();
        return object;
    }

    @Override
    public Object updateObject(final Object object, final Resource resource) throws ParseException, IOException {
        String jsonString = resource.serialize(object);
        Object jsonObject = JsonPath.read(jsonString, "$");
        updateObject(jsonObject, resource, getIndexWriter());
        commit();
        return object;
    }
}