org.phenotips.entities.internal.AbstractPrimaryEntityManager.java Source code

Java tutorial

Introduction

Here is the source code for org.phenotips.entities.internal.AbstractPrimaryEntityManager.java

Source

/*
 * See the NOTICE file distributed with this work for additional
 * information regarding copyright ownership.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see http://www.gnu.org/licenses/
 */
package org.phenotips.entities.internal;

import org.phenotips.Constants;
import org.phenotips.entities.PrimaryEntity;
import org.phenotips.entities.PrimaryEntityManager;

import org.xwiki.bridge.DocumentAccessBridge;
import org.xwiki.bridge.DocumentModelBridge;
import org.xwiki.model.EntityType;
import org.xwiki.model.reference.DocumentReference;
import org.xwiki.model.reference.DocumentReferenceResolver;
import org.xwiki.model.reference.EntityReference;
import org.xwiki.model.reference.EntityReferenceSerializer;
import org.xwiki.query.Query;
import org.xwiki.query.QueryException;
import org.xwiki.query.QueryManager;
import org.xwiki.stability.Unstable;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Provider;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;

import com.xpn.xwiki.XWikiContext;
import com.xpn.xwiki.doc.XWikiDocument;

/**
 * Base class for implementing specific entity managers. This can be used as an almost complete base implementation,
 * since the only unimplemented method is {@link #getDataSpace()}, however, several requirements are imposed on the
 * subclasses to fully work:
 * <ul>
 * <li>the concrete implementation must have its {@code @Named} annotation set to reference the XClass used for the
 * primary entity, the one that's also returned by the {@link PrimaryEntity#getType()}</li>
 * <li>the {@code <E>} parameter must be set to the concrete class managed</li>
 * <li>the class used for {@code <E>} must have a constructor that takes as an argument a {@link DocumentModelBridge} or
 * a {@link XWikiDocument} argument</li>
 * <li>all documents {@link #create() created} by this manager will have the name in the format
 * {@code <PREFIX><7 digit sequential number>}, unless {@link #getNextDocument()} is overridden, where:
 * <ul>
 * <li>the prefix is computed from the uppercase letters of the XClass name, excluding {@code Class}, e.g. for
 * {@code PhenoTips.DiseaseStudyClass} the prefix will be {@code DS}; override {@link #getIdPrefix()} to change this
 * behavior</li>
 * <li>the number is a 0-padded 7 digit number, starting at {@code 0000001} and automatically incremented for each new
 * entity created</li>
 * </ul>
 * </li>
 * </ul>
 *
 * @param <E> the type of entities handled by this manager
 * @version $Id: 37eb7eeeed35b01c0bf7c7db7c1902c964e284e3 $
 * @since 1.3M2
 */
@Unstable("New class and interface added in 1.3")
public abstract class AbstractPrimaryEntityManager<E extends PrimaryEntity> implements PrimaryEntityManager<E> {
    /** Logging helper object. */
    @Inject
    protected Logger logger;

    /** Runs queries for finding entities. */
    @Inject
    protected QueryManager qm;

    /** Provides access to the current execution context. */
    @Inject
    protected Provider<XWikiContext> xcontextProvider;

    /** Provides access to the XWiki data. */
    @Inject
    protected DocumentAccessBridge bridge;

    /** Parses string representations of document references into proper references. */
    @Inject
    @Named("current")
    protected DocumentReferenceResolver<String> stringResolver;

    /** Fills in missing reference fields with those from the current context to create a full reference. */
    @Inject
    @Named("current")
    protected DocumentReferenceResolver<EntityReference> referenceResolver;

    /** Serializes references without the wiki prefix. */
    @Inject
    @Named("local")
    protected EntityReferenceSerializer<String> localSerializer;

    /** The concrete {@link PrimaryEntity} instance class being managed. */
    private Class<? extends E> eclass;

    /** The constructor for concrete {@link PrimaryEntity} instance class being managed. */
    private Constructor<? extends E> econstructor;

    @Override
    public E create() {
        return create(this.bridge.getCurrentUserReference());
    }

    @Override
    public synchronized E create(DocumentReference creator) {
        try {
            XWikiContext context = this.xcontextProvider.get();
            DocumentReference newDoc = getNextDocument();
            XWikiDocument doc = (XWikiDocument) this.bridge.getDocument(newDoc);

            DocumentReference template = getEntityXClassReference();
            template = new DocumentReference(template.getName().concat("Template"),
                    template.getLastSpaceReference());
            if (!this.bridge.exists(template)) {
                template = new DocumentReference(template.getName().replaceAll("Class(Template)$", "$1"),
                        template.getLastSpaceReference());
            }
            if (this.bridge.exists(template)) {
                doc.readFromTemplate(template, context);
            }
            if (creator != null) {
                doc.setCreatorReference(creator);
                doc.setAuthorReference(creator);
                doc.setContentAuthorReference(creator);
            }
            context.getWiki().saveDocument(doc, context);

            return load(doc);
        } catch (Exception ex) {
            this.logger.warn("Failed to create entity: {}", ex.getMessage(), ex);
            return null;
        }
    }

    @Override
    public E get(String id) {
        return get(this.stringResolver.resolve(id, getDataSpace()));
    }

    @Override
    public E get(DocumentReference reference) {
        try {
            DocumentModelBridge document = this.bridge.getDocument(reference);
            if (document == null || ((XWikiDocument) document).isNew()) {
                return null;
            }
            return load(document);
        } catch (Exception ex) {
            this.logger.error("Failed to read document [{}]: {}", reference, ex.getMessage());
        }
        return null;
    }

    /**
     * {@inheritDoc}
     * <p>
     * This assumes that the name is stored in the document title. Override for a different behavior.
     * </p>
     */
    @Override
    public E getByName(String name) {
        try {
            Query q = this.qm
                    .createQuery("select doc.fullName from Document doc, doc.object("
                            + this.localSerializer.serialize(getEntityXClassReference())
                            + ") as entity where doc.space = :space and doc.title = :name", Query.XWQL)
                    .bindValue("space", this.getDataSpace().getName()).bindValue("name", name);
            List<String> docNames = q.execute();
            for (String docName : docNames) {
                return get(docName);
            }
        } catch (QueryException ex) {
            this.logger.warn("Failed to retrieve the entity named [{}]: {}", name, ex.getMessage());
        }
        return null;
    }

    @Override
    public Iterator<E> getAll() {
        try {
            Query q = this.qm
                    .createQuery("select doc.fullName from Document as doc, doc.object("
                            + this.localSerializer.serialize(getEntityXClassReference())
                            + ") as entity where doc.name not in (:template1, :template2) order by doc.name asc",
                            Query.XWQL)
                    .bindValue("template1", this.getEntityXClassReference().getName() + "Template")
                    .bindValue("template2",
                            StringUtils.removeEnd(this.getEntityXClassReference().getName(), "Class") + "Template");
            List<String> docNames = q.execute();
            return new LazyPrimaryEntityIterator<>(docNames, this);
        } catch (QueryException ex) {
            this.logger.warn("Failed to query all entities of type [{}]: {}", getEntityXClassReference(),
                    ex.getMessage());
        }
        return Collections.emptyIterator();
    }

    @Override
    public boolean delete(E entity) {
        try {
            XWikiContext xcontext = this.xcontextProvider.get();
            XWikiDocument doc = xcontext.getWiki().getDocument(entity.getDocumentReference(), xcontext);
            xcontext.getWiki().deleteDocument(doc, xcontext);
            return true;
        } catch (Exception ex) {
            this.logger.warn("Failed to delete entity [{}]: {}", entity.getDocumentReference(), ex.getMessage());
        }
        return false;
    }

    @Override
    public E load(DocumentModelBridge document) throws IllegalArgumentException {
        try {
            return getEntityConstructor().newInstance(document);
        } catch (InstantiationException | IllegalAccessException | InvocationTargetException ex) {
            this.logger.error("Failed to instantiate primary entity of type [{}] from document [{}]: {}",
                    getEntityXClassReference(), document, ex.getMessage());
        }
        return null;
    }

    /**
     * Gets a reference to the next document that can be used for a newly created entity. It uses {@link #getIdPrefix()
     * a short prefix} and {@link #getLastUsedId() a sequence number} for the document name, and {@link #getDataSpace()
     * a space that can be configured by subclases}.
     *
     * @return a reference for a new document
     */
    protected DocumentReference getNextDocument() {
        String prefix = getIdPrefix();
        long id = getLastUsedId();
        DocumentReference newDoc;
        do {
            newDoc = this.referenceResolver.resolve(
                    new EntityReference(prefix + String.format("%07d", ++id), EntityType.DOCUMENT, getDataSpace()));
        } while (this.bridge.exists(newDoc));
        return newDoc;
    }

    /**
     * Gets a prefix for all {@code #create() generated} documents. This implementation computes it from the uppercase
     * letters of the XClass name, excluding {@code Class}, e.g. for {@code PhenoTips.DiseaseStudyClass} the prefix will
     * be {@code DS}.
     *
     * @return a short string
     */
    protected String getIdPrefix() {
        String name = getEntityXClassReference().getName();
        name = StringUtils.removeEnd(name, "Class");
        return name.replaceAll("\\p{Lower}++", "");
    }

    protected long getLastUsedId() {
        long crtMaxID = 0;
        try {
            Query q = this.qm
                    .createQuery("select doc.name from Document doc, doc.object("
                            + this.localSerializer.serialize(getEntityXClassReference())
                            + ") as entity where doc.space = :space order by doc.name desc", Query.XWQL)
                    .bindValue("space", this.getDataSpace().getName()).setLimit(1);
            List<String> crtMaxIDList = q.execute();
            if (!crtMaxIDList.isEmpty() && crtMaxIDList.get(0) != null) {
                crtMaxID = Integer.parseInt(crtMaxIDList.get(0).replaceAll("\\D++", ""));
            }
            crtMaxID = Math.max(crtMaxID, 0);
        } catch (QueryException ex) {
            this.logger.warn("Failed to get the last used identifier: {}", ex.getMessage());
        }
        return crtMaxID;
    }

    /**
     * Gets a reference to {@link PrimaryEntity#getType() the XClass used} for the primary entities being managed. The
     * base implementation assumes that this class is annotated with a {@code Named} with its value set to a partial
     * reference to the target XClass. This name shouldn't have a wiki reference, since this depends on the current
     * instance. If the name doesn't have a space, it will be assumed to be {@code PhenoTips}. If the name doesn't end
     * with {@code Class}, it is added automatically. For example, assuming the targeted XClass is
     * {@code PhenoTips.PatientClass}, the following names are supported, in order of preference:
     * <ul>
     * <li>Patient</li>
     * <li>PhenoTips.PatientClass</li>
     * <li>PhenoTips.Patient</li>
     * <li>PatientClass</li>
     * </ul>
     * If, instead, the targeted XClass is {@code OncoTips.Family}, then only that exact name is supported.
     *
     * @return a full reference resolved in the current virtual instance
     */
    protected DocumentReference getEntityXClassReference() {
        if (this.getClass().getAnnotation(Named.class) == null) {
            this.logger.error("Invalid component configuration: {} does not have a @Named annotation",
                    this.getClass().getName());
            throw new AbstractMethodError("Missing @Named annotation on PrimaryEntityManager class "
                    + this.getClass().getCanonicalName());
        }
        String name = this.getClass().getAnnotation(Named.class).value();
        DocumentReference result = this.stringResolver.resolve(name, Constants.CODE_SPACE_REFERENCE);
        if (!this.bridge.exists(result) && !name.endsWith("Class")) {
            result = this.stringResolver.resolve(name + "Class", Constants.CODE_SPACE_REFERENCE);
        }
        if (!this.bridge.exists(result)) {
            this.logger.error("Invalid component configuration: {} does not have a valid @Named annotation",
                    this.getClass().getName());
            throw new AbstractMethodError(
                    "The @Named annotation on PrimaryEntityManager class " + this.getClass().getCanonicalName()
                            + " must be a reference to the XClass used by its managed entity");
        }
        return result;
    }

    /**
     * Gets the concrete class of the managed PrimaryEntity. The base implementation assumes that the class is passed
     * for the first generic parameter.
     *
     * @return a class
     * @throws AbstractMethodError if the manager class doesn't properly define the generic parameter
     */
    @SuppressWarnings("unchecked")
    protected Class<? extends E> getEntityClass() {
        if (this.eclass == null) {
            Type cls = this.getClass().getGenericSuperclass();
            while (cls instanceof Class) {
                cls = ((Class<?>) cls).getGenericSuperclass();
            }
            ParameterizedType pcls = (ParameterizedType) cls;
            if (pcls.getActualTypeArguments().length == 0 || !(pcls.getActualTypeArguments()[0] instanceof Class)) {
                this.logger.error(
                        "Invalid component configuration: {} does not define a real class for the <E> parameter",
                        this.getClass().getName());
                throw new AbstractMethodError("The PrimaryEntityManager class " + this.getClass().getCanonicalName()
                        + " must define a real class for the <E> parameter");
            }
            this.eclass = (Class<E>) pcls.getActualTypeArguments()[0];
        }
        return this.eclass;
    }

    /**
     * Gets the constructor of the managed PrimaryEntity concrete class that accepts an XDocument as the only parameter.
     * Override if another constructor is needed, but keep in mind that {@link #load(DocumentModelBridge)} must also be
     * overridden to pass in the required arguments.
     *
     * @return a constructor that requires an XWikiDocument as its only parameter
     * @throws AbstractMethodError if no such constructor can be found
     */
    protected Constructor<? extends E> getEntityConstructor() throws AbstractMethodError {
        if (this.econstructor == null) {
            try {
                this.econstructor = getEntityClass().getConstructor(XWikiDocument.class);
            } catch (NoSuchMethodException | SecurityException e) {
                try {
                    this.econstructor = getEntityClass().getConstructor(DocumentModelBridge.class);
                } catch (NoSuchMethodException | SecurityException ex) {
                    this.logger.error("Cannot instantiate primary entity [{}]: {}", getEntityClass().getName(),
                            ex.getMessage());
                    throw new AbstractMethodError("The PrimaryEntity class " + getEntityClass().getCanonicalName()
                            + " must have a public constructor that accepts an XWikiDocument parameter");
                }
            }
        }
        return this.econstructor;
    }
}