All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.marklogic.client.impl.PojoRepositoryImpl Maven / Gradle / Ivy

/*
 * Copyright 2012-2016 MarkLogic Corporation
 *
 * 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.marklogic.client.impl;

import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.URLEncoder;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationConfig;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition;
import com.marklogic.client.DatabaseClient;
import com.marklogic.client.MarkLogicBindingException;
import com.marklogic.client.MarkLogicInternalException;
import com.marklogic.client.ResourceNotFoundException;
import com.marklogic.client.Transaction;
import com.marklogic.client.document.DocumentPage;
import com.marklogic.client.document.DocumentWriteSet;
import com.marklogic.client.document.JSONDocumentManager;
import com.marklogic.client.io.DocumentMetadataHandle;
import com.marklogic.client.io.JacksonDatabindHandle;
import com.marklogic.client.io.SearchHandle;
import com.marklogic.client.io.marker.SearchReadHandle;
import com.marklogic.client.pojo.PojoPage;
import com.marklogic.client.pojo.PojoQueryBuilder;
import com.marklogic.client.pojo.PojoQueryDefinition;
import com.marklogic.client.pojo.PojoRepository;
import com.marklogic.client.pojo.annotation.Id;
import com.marklogic.client.query.DeleteQueryDefinition;
import com.marklogic.client.query.QueryManager;
import com.marklogic.client.query.QueryManager.QueryView;
import com.sun.jersey.api.client.ClientHandlerException;

public class PojoRepositoryImpl
    implements PojoRepository
{
    private static final Pattern getterPattern = Pattern.compile("^(get|is)(.)(.*)");
    private final String EXTENSION = ".json";

    private DatabaseClient client;
    private Class entityClass;
    @SuppressWarnings("unused")
    private Class idClass;
    private JSONDocumentManager docMgr;
    private PojoQueryBuilder qb;
    private Method idMethod;
    private Field idProperty;
    @SuppressWarnings("unused")
    private String idPropertyName;
    private static final String ISO_8601_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX";
    private static SimpleDateFormat simpleDateFormat8601;
    static {
        try {
            simpleDateFormat8601 = new SimpleDateFormat(ISO_8601_FORMAT);
        // Java 1.6 doesn't yet know about X (ISO 8601 format)
        } catch (IllegalArgumentException e) {
            if ( "Illegal pattern character 'X'".equals(e.getMessage()) ) {
                simpleDateFormat8601 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
            }
        }
    }
    static { simpleDateFormat8601.setTimeZone(TimeZone.getTimeZone("UTC")); }
    private ObjectMapper objectMapper = new ObjectMapper()
        // if we don't do the next two lines Jackson will automatically close our streams which is undesirable
        .configure(JsonGenerator.Feature.AUTO_CLOSE_TARGET, false)
        .configure(JsonParser.Feature.AUTO_CLOSE_SOURCE, false)
        // we do the next two so dates are written in xs:dateTime format
        // which makes them ready for range indexes in MarkLogic Server
        .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
        .setDateFormat(simpleDateFormat8601)
        // enableDefaultTyping just means include types in the serialized output
        // we need this to do strongly-typed queries 
        .enableDefaultTyping(
            // ObjectMapper.DefaultTyping.NON_FINAL means that typing in serialized output
            // for all non-final types except the "natural" types (String, Boolean, Integer, Double), 
            // which can be correctly inferred from JSON; as well as for all arrays of non-final types.
            ObjectMapper.DefaultTyping.NON_FINAL, 
            // JsonTypeInfo.As.WRAPPER_OBJECT means add a type wrapper around the data so then
            // our strongly-typed queries can use parent-child scoped queries or path index queries
            JsonTypeInfo.As.WRAPPER_OBJECT);
    PojoRepositoryImpl(DatabaseClient client, Class entityClass) {
        this.client = client;
        this.entityClass = entityClass;
        this.idClass = null;
        this.docMgr = client.newJSONDocumentManager();
        this.qb = new PojoQueryBuilderImpl(entityClass);
    }

    PojoRepositoryImpl(DatabaseClient client, Class entityClass, Class idClass) {
        this(client, entityClass);
        this.idClass = idClass;
        findId();
        if ( idMethod == null && idProperty == null ) {
            throw new IllegalArgumentException("Your class " + entityClass.getName() +
                " does not have a method or field annotated with com.marklogic.client.pojo.annotation.Id");
        }
    }

    @Override
    public void write(T entity) {
        write(entity, null, (String[]) null);
    }
    @Override
    public void write(T entity, String... collections) {
        write(entity, null, collections);
    }
    @Override
    public void write(T entity, Transaction transaction) {
        write(entity, transaction, (String[]) null);
    }
    @Override
    public void write(T entity, Transaction transaction, String... collections) {
        if ( entity == null ) return;
        JacksonDatabindHandle contentHandle = new JacksonDatabindHandle(entity);
        contentHandle.setMapper(objectMapper); 
        DocumentMetadataHandle metadataHandle = new DocumentMetadataHandle();
        metadataHandle = metadataHandle.withCollections(entityClass.getName());
        if ( collections != null && collections.length > 0 ) {
            metadataHandle = metadataHandle.withCollections(collections);
        }
        DocumentWriteSet writeSet = docMgr.newWriteSet();
        writeSet.add(getDocumentUri(entity), metadataHandle, contentHandle);
        try {
            docMgr.write(writeSet, transaction);
        } catch(ClientHandlerException e) {
            checkForEmptyBeans(e);
            throw e;
        }
    }

    private void checkForEmptyBeans(Throwable e) {
        Throwable cause = e.getCause();
        if ( cause != null ) {
            if ( cause instanceof JsonMappingException && 
                 cause.getMessage() != null &&
                 cause.getMessage().contains("SerializationFeature.FAIL_ON_EMPTY_BEANS") )
            {
                throw new MarkLogicBindingException(
                    "Each of your pojo beans and descendent beans must have public fields or paired get/set methods",
                    cause);
            } else {
                checkForEmptyBeans(cause);
            }
        }
    }


    @Override
    public boolean exists(ID id) {
        return docMgr.exists(createUri(id)) != null;
    }

    @Override
    public boolean exists(ID id, Transaction transaction) {
        return docMgr.exists(createUri(id), transaction) != null;
    }

    @Override
    public long count() {
        return count((PojoQueryDefinition) null, null);
    }

    @Override
    public long count(String... collections) {
        return count(collections, null);
    }

    @Override
    public long count(PojoQueryDefinition query) {
        return count((PojoQueryDefinition) null, null);
    }
  
    @Override
    public long count(Transaction transaction) {
        return count((PojoQueryDefinition) null, transaction);
    }

    @Override
    public long count(String[] collections, Transaction transaction) {
        if ( collections != null && collections.length > 0 ) {
            if ( collections.length > 1 || collections[0] != null ) {
                return count(qb.collection(collections), transaction);
            }
        }
        return count((PojoQueryDefinition) null, transaction);
    }

    @Override
    public long count(PojoQueryDefinition query, Transaction transaction) {
        long pageLength = getPageLength();
        setPageLength(0);
        PojoPage page = search(query, 1, transaction);
        setPageLength(pageLength);
        return page.getTotalSize();
    }

    @Override
    public void delete(ID... ids) {
        delete(ids, null);
    }

    @Override
    public void delete(ID[] ids, Transaction transaction) {
        for ( ID id : ids ) {
            docMgr.delete(createUri(id), transaction);
        }
    }

    @Override
    public void deleteAll() {
        deleteAll(null);
    }

    @Override
    public void deleteAll(Transaction transaction) {
        QueryManager queryMgr = client.newQueryManager();
        DeleteQueryDefinition deleteQuery = queryMgr.newDeleteDefinition();
        deleteQuery.setCollections(entityClass.getName());
        queryMgr.delete(deleteQuery, transaction);
    }
    /* REST API does not currently support DELETE /search with multiple collection arguments
    @Override
    public void deleteAll(String... collections) {
        if ( collections == null || collections.length == 0 ) {
            throw new IllegalArgumentException("You must specify at least one collection");
        } else if ( collections[0] == null ) {
            throw new IllegalArgumentException("Collection argument must not be null");
        }
        QueryManager queryMgr = client.newQueryManager();
        DeleteQueryDefinition deleteQuery = queryMgr.newDeleteDefinition();
        deleteQuery.setCollections(collections);
        queryMgr.delete((DeleteQueryDefinition) wrapQuery(deleteQuery));
    }
    */
  
    @Override
    public T read(ID id) {
        return read(id, null);
    }
    @Override
    public T read(ID id, Transaction transaction) {
        ArrayList ids = new ArrayList();
        ids.add(id);
        @SuppressWarnings("unchecked")
        PojoPage page = read(ids.toArray((ID[])new Serializable[0]), transaction);
        if ( page == null || page.hasNext() == false ) {
            throw new ResourceNotFoundException("Could not find document of type " +
                entityClass.getName() + " with id " + id);
        }
        return page.next();
    }
    @Override
    public PojoPage read(ID[] ids) {
        return read(ids, null);
    }
    @Override
    public PojoPage read(ID[] ids, Transaction transaction) {
        ArrayList uris = new ArrayList();
        for ( ID id : ids ) {
            uris.add(createUri(id));
        }
        DocumentPage docPage = (DocumentPage) docMgr.read(transaction, uris.toArray(new String[0]));
        PojoPage pojoPage = new PojoPageImpl(docPage, entityClass);
        return pojoPage;
    }
    @Override
    public PojoPage readAll(long start) {
        return search(null, start, null, null);
    }
    @Override
    public PojoPage readAll(long start, Transaction transaction) {
        return search(null, start, null, transaction);
    }

    @Override
    public PojoPage search(long start, String... collections) {
        return search(qb.collection(collections), start, null, null);
    }
    @Override
    public PojoPage search(long start, Transaction transaction, String... collections) {
        return search(qb.collection(collections), start, null, transaction);
    }

    @Override
    public PojoPage search(PojoQueryDefinition query, long start) {
        return search(query, start, null, null);
    }
    @Override
    public PojoPage search(PojoQueryDefinition query, long start, Transaction transaction) {
        return search(query, start, null, transaction);
    }
    @Override
    public PojoPage search(PojoQueryDefinition query, long start, SearchReadHandle searchHandle) {
        return search(query, start, searchHandle, null);
    }
    @Override
    public PojoPage search(PojoQueryDefinition query, long start, SearchReadHandle searchHandle, Transaction transaction) {
        if ( searchHandle != null ) {
            HandleAccessor.checkHandle(searchHandle, "search");
            if (searchHandle instanceof SearchHandle) {
                SearchHandle responseHandle = (SearchHandle) searchHandle;
                if ( docMgr instanceof DocumentManagerImpl ) {
                    responseHandle.setHandleRegistry(((DocumentManagerImpl) docMgr).getHandleRegistry());
                }
                responseHandle.setQueryCriteria(query);
            }
        }

        DocumentPage docPage = docMgr.search(wrapQuery(query), start, searchHandle, transaction);
        PojoPage pojoPage = new PojoPageImpl(docPage, entityClass);
        return pojoPage;
    }
 
    @Override
    public PojoQueryBuilder getQueryBuilder() {
        return qb;
    }

    @Override
    public long getPageLength() {
        return docMgr.getPageLength();
    }
    @Override
    public void setPageLength(long length) {
        docMgr.setPageLength(length);
    }
    
    public QueryView getSearchView() {
        return docMgr.getSearchView();
    }

    public void setSearchView(QueryView view) {
        docMgr.setSearchView(view);
    }

    public ObjectMapper getObjectMapper() {
        return objectMapper;
    }

    public void setObjectMapper(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }

    private PojoQueryDefinition wrapQuery(PojoQueryDefinition query) {
        if ( query == null ) {
            return qb.collection(entityClass.getName());
        } else {
            List collections = Arrays.asList(query.getCollections());
            HashSet collectionSet = new HashSet(collections);
            collectionSet.add(entityClass.getName());
            query.setCollections(collectionSet.toArray(new String[0]));
            return query;
        }
    }

    @Override
    public String getDocumentUri(T entity) {
        return createUri(getId(entity));
    }

    private String createUri(ID id) {
        if ( id == null ) {
            throw new IllegalStateException("id cannot be null");
        }
        try {
            return entityClass.getName() + "/" + URLEncoder.encode(id.toString(), "UTF-8") + EXTENSION;
        } catch (UnsupportedEncodingException e) {
            throw new MarkLogicInternalException(e);
        }
    }

    private void findId() {
        if ( idMethod == null && idProperty == null ) {
            SerializationConfig serializationConfig = objectMapper.getSerializationConfig();
            JavaType javaType = serializationConfig.constructType(entityClass);
            BeanDescription beanDescription = serializationConfig.introspect(javaType);
            List properties = beanDescription.findProperties();
            for ( BeanPropertyDefinition property : properties ) {
                /* Constructor parameters don't work because they give us no value accessor
                if ( property.hasConstructorParameter() ) {
                    AnnotatedParameter parameter = property.getConstructorParameter();
                    if ( parameter.getAnnotation(Id.class) != null ) {
                        idPropertyName = property.getName();
                    }
                }
                */
                if ( property.hasField() ) {
                    Field field = property.getField().getAnnotated();
                    if ( field.getAnnotation(Id.class) != null ) {
                        idPropertyName = property.getName();
                        idProperty = field;
                        break;
                    }
                }
                if ( property.hasGetter() ) {
                    Method getter = property.getGetter().getAnnotated();
                    if ( getter.getAnnotation(Id.class) != null ) {
                        idPropertyName = property.getName();
                        idMethod = getter;
                        break;
                    }
                    if ( property.hasSetter() ) {
                        Method setter = property.getSetter().getAnnotated();
                        if ( setter.getAnnotation(Id.class) != null ) {
                            idPropertyName = property.getName();
                            idMethod = getter;
                            break;
                        }
                    }
                }
                // setter only doesn't work because it gives us no value accessor
            }
        }
        // Jackson's introspect approach should find it, but our old approach below 
        // gives some helpful errors
        if ( idMethod == null && idProperty == null ) {
            for ( Method method : entityClass.getDeclaredMethods() ) {
                if ( method.isAnnotationPresent(Id.class) ) {
                    Class[] parameters = method.getParameterTypes();
                    if ( ! Modifier.isPublic(method.getModifiers()) ) {
                        throw new IllegalStateException("Your method, " + method.getName() +
                            ", annotated with com.marklogic.client.pojo.annotation.Id " + 
                            " must be public");
                    }
                    if ( parameters == null || parameters.length == 0 ) {
                        Matcher matcher = getterPattern.matcher(method.getName());
                        if ( matcher.matches() ) {
                            idPropertyName = matcher.group(2).toLowerCase() + matcher.group(3);
                            idMethod = method;
                            break;
                        } else {
                            throw new IllegalStateException("Your no-args method, " + method.getName() +
                                ", annotated with com.marklogic.client.pojo.annotation.Id " + 
                                " must be a proper getter method and begin with \"get\" or \"is\"");
                        }
                    } else {
                        Matcher getterMatcher = getterPattern.matcher(method.getName());
                        if ( getterMatcher.matches() ) {
                            throw new IllegalStateException("Your getter method, " + method.getName() +
                                ", annotated with com.marklogic.client.pojo.annotation.Id " + 
                                " must not require any arguments");
                        } else if ( method.getName().startsWith("set") ) {
                            throw new MarkLogicInternalException("Your setter method, " + method.getName() +
                                ", annotated with com.marklogic.client.pojo.annotation.Id " +
                                "was not found by Jackson for some reason.  Please report this to " +
                                "MarkLogic support.");
                        } else {
                            throw new IllegalStateException("Your setter method, " + method.getName() +
                                ", annotated with com.marklogic.client.pojo.annotation.Id " + 
                                " must be a proper setter method (beginning with \"set\")");
                        }
                    }
                }
            }
            if ( idMethod == null ) {
                for ( Field field : entityClass.getDeclaredFields() ) {
                    if ( field.isAnnotationPresent(Id.class) ) {
                        if ( ! Modifier.isPublic(field.getModifiers()) ) {
                            throw new IllegalStateException("Your field, " + field.getName() +
                                ", annotated with com.marklogic.client.pojo.annotation.Id " + 
                                " must be public");
                        }
                        idProperty = field;
                        break;
                    }
                }
            }
        }
    }

    @SuppressWarnings("unchecked")
    public ID getId(T entity) {
        findId();
        if ( idMethod != null ) {
            try {
                return (ID) idMethod.invoke(entity);
            } catch (Exception e) {
                throw new IllegalStateException("Error invoking " + entityClass.getName() + " method " +
                    idMethod.getName(), e);
            }
        } else if ( idProperty != null ) {
            try {
                return (ID) idProperty.get(entity);
            } catch (Exception e) {
                throw new IllegalStateException("Error retrieving " + entityClass.getName() + " field " +
                    idProperty.getName(), e);
            }
        } else {
            throw new IllegalArgumentException("Your class " + entityClass.getName() +
                " does not have a method or field annotated with com.marklogic.client.pojo.annotation.Id");
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy