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

alpine.persistence.AbstractAlpineQueryManager Maven / Gradle / Ivy

The newest version!
/*
 * This file is part of Alpine.
 *
 * 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.
 *
 * SPDX-License-Identifier: Apache-2.0
 * Copyright (c) Steve Springett. All Rights Reserved.
 */
package alpine.persistence;

import alpine.common.validation.RegexSequence;
import alpine.resources.AlpineRequest;
import org.apache.commons.collections4.CollectionUtils;
import org.datanucleus.api.jdo.JDOQuery;

import javax.jdo.FetchPlan;
import javax.jdo.PersistenceManager;
import javax.jdo.Query;
import javax.jdo.metadata.MemberMetadata;
import javax.jdo.metadata.TypeMetadata;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.Callable;

/**
 * Base persistence manager that implements AutoCloseable so that the PersistenceManager will
 * be automatically closed when used in a try-with-resource block.
 *
 * @author Steve Springett
 * @since 1.0.0
 */
public abstract class AbstractAlpineQueryManager implements AutoCloseable {

    private static final ServiceLoader IpmfServiceLoader = ServiceLoader.load(IPersistenceManagerFactory.class);

    protected final Principal principal;
    protected Pagination pagination;
    protected final String filter;
    protected final String orderBy;
    protected final OrderDirection orderDirection;

    protected final PersistenceManager pm;

    public static Optional getPersistenceManagerFactory() {
        return IpmfServiceLoader.findFirst();
    }

    /**
     * Specifies a non-default PersistenceManager to use.
     * @param pm the JDO PersistenceManager to use
     * @since 1.4.3
     */
    public AbstractAlpineQueryManager(final PersistenceManager pm) {
        this.pm = pm;
        principal = null;
        pagination = new Pagination(Pagination.Strategy.NONE, 0, 0);
        filter = null;
        orderBy = null;
        orderDirection = OrderDirection.UNSPECIFIED;
    }

    /**
     * Default constructor
     */
    public AbstractAlpineQueryManager() {
        final Optional ipmf = getPersistenceManagerFactory();
        pm = (ipmf.isEmpty()) ? null : ipmf.get().getPersistenceManager();
        principal = null;
        pagination = new Pagination(Pagination.Strategy.NONE, 0, 0);
        filter = null;
        orderBy = null;
        orderDirection = OrderDirection.UNSPECIFIED;
    }

    /**
     * Constructs a new QueryManager with the following:
     * @param principal a Principal, or null
     * @param pagination a Pagination request, or null
     * @param filter a String filter, or null
     * @param orderBy the field to order by
     * @param orderDirection the sorting direction
     * @since 1.0.0
     */
    public AbstractAlpineQueryManager(final Principal principal, final Pagination pagination, final String filter,
                                      final String orderBy, final OrderDirection orderDirection) {
        final Optional ipmf = getPersistenceManagerFactory();
        pm = (ipmf.isEmpty()) ? null : ipmf.get().getPersistenceManager();
        this.principal = principal;
        this.pagination = pagination;
        this.filter = filter;
        this.orderBy = orderBy;
        this.orderDirection = orderDirection;
    }

    /**
     * Constructs a new QueryManager. Deconstructs the specified AlpineRequest
     * into its individual components including pagination and ordering.
     * @param request an AlpineRequest object
     * @since 1.0.0
     */
    public AbstractAlpineQueryManager(final AlpineRequest request) {
        final Optional ipmf = getPersistenceManagerFactory();
        pm = (ipmf.isEmpty()) ? null : ipmf.get().getPersistenceManager();
        this.principal = request.getPrincipal();
        this.pagination = request.getPagination();
        this.filter = request.getFilter();
        this.orderBy = request.getOrderBy();
        this.orderDirection = request.getOrderDirection();
    }



    /**
     * Constructs a new QueryManager. Deconstructs the specified AlpineRequest
     * into its individual components including pagination and ordering.
     * @param pm the JDO PersistenceManager to use
     * @param request an AlpineRequest object
     * @since 1.9.3
     */
    public AbstractAlpineQueryManager(final PersistenceManager pm, final AlpineRequest request) {
        this.pm = pm;
        this.principal = request.getPrincipal();
        this.pagination = request.getPagination();
        this.filter = request.getFilter();
        this.orderBy = request.getOrderBy();
        this.orderDirection = request.getOrderDirection();
    }

    /**
     * Wrapper around {@link Query#executeWithArray(Object...)} that adds transparent support for
     * pagination and ordering of results via {@link #decorate(Query)}.
     * @param query the JDO Query object to execute
     * @param parameters the Object array with all the parameters
     * @return a PaginatedResult object
     * @since 1.0.0
     */
    public PaginatedResult execute(final Query query, final Object... parameters) {
        final long count = getCount(query, parameters);
        decorate(query);
        return new PaginatedResult()
                .objects(executeAndCloseWithArray(query, parameters))
                .total(count);
    }

    /**
     * Wrapper around {@link Query#executeWithMap(Map)} that adds transparent support for
     * pagination and ordering of results via {@link #decorate(Query)}.
     * @param query the JDO Query object to execute
     * @param parameters the Map containing all the parameters.
     * @return a PaginatedResult object
     * @since 1.0.0
     */
    public PaginatedResult execute(final Query query, final Map parameters) {
        final long count = getCount(query, parameters);
        decorate(query);
        return new PaginatedResult()
                .objects(executeAndCloseWithMap(query, parameters))
                .total(count);
    }

    /**
     * Advances the pagination based on the previous pagination settings. This is purely a
     * convenience method as the method by itself is not aware of the query being executed,
     * the result count, etc.
     * @since 1.0.0
     */
    public void advancePagination() {
        if (pagination.isPaginated()) {
            pagination = new Pagination(pagination.getStrategy(), pagination.getOffset() + pagination.getLimit(), pagination.getLimit());
        }
    }

    /**
     * Given a query, this method will decorate that query with pagination, ordering,
     * and sorting direction. Specific checks are performed to ensure the execution
     * of the query is capable of being paged and that ordering can be securely performed.
     * @param query the JDO Query object to execute
     * @return a Collection of objects
     * @since 1.0.0
     */
    public  Query decorate(final Query query) {
        // Clear the result to fetch if previously specified (i.e. by getting count)
        query.setResult(null);
        if (pagination != null && pagination.isPaginated()) {
            final long begin = pagination.getOffset();
            final long end = begin + pagination.getLimit();
            query.setRange(begin, end);
        }
        if (orderBy != null && RegexSequence.Pattern.STRING_IDENTIFIER.matcher(orderBy).matches() && orderDirection != OrderDirection.UNSPECIFIED) {
            // Check to see if the specified orderBy field is defined in the class being queried.
            boolean found = false;
            // NB: Only persistent fields can be used as sorting subject.
            final org.datanucleus.store.query.Query iq = ((JDOQuery) query).getInternalQuery();
            final String candidateField = orderBy.contains(".") ? orderBy.substring(0, orderBy.indexOf('.')) : orderBy;
            final TypeMetadata candidateTypeMetadata = pm.getPersistenceManagerFactory().getMetadata(iq.getCandidateClassName());
            if (candidateTypeMetadata == null) {
                // NB: If this happens then the entire query is broken and needs programmatic fixing.
                // Throwing an exception here to make this painfully obvious.
                throw new IllegalStateException("""
                        Persistence type metadata for candidate class %s could not be found. \
                        Querying for non-persistent types is not supported, correct your query.\
                        """.formatted(iq.getCandidateClassName()));
            }
            boolean foundPersistentMember = false;
            for (final MemberMetadata memberMetadata : candidateTypeMetadata.getMembers()) {
                if (candidateField.equals(memberMetadata.getName())) {
                    foundPersistentMember = true;
                    break;
                }
            }
            if (foundPersistentMember) {
                query.setOrdering(orderBy + " " + orderDirection.name().toLowerCase());
            } else {
                // Is it a non-persistent (transient) field?
                final boolean foundNonPersistentMember = Arrays.stream(iq.getCandidateClass().getDeclaredFields())
                        .anyMatch(field -> field.getName().equals(candidateField));
                if (foundNonPersistentMember) {
                    throw new NotSortableException(iq.getCandidateClass().getSimpleName(), candidateField,
                            "The field is computed and can not be queried or sorted by");
                }

                throw new NotSortableException(iq.getCandidateClass().getSimpleName(), candidateField,
                        "The field does not exist");
            }
        }
        return query;
    }

    /**
     * Returns the number of items that would have resulted from returning all object.
     * This method is performant in that the objects are not actually retrieved, only
     * the count.
     * @param query the query to return a count from
     * @param parameters the Object array with all the parameters
     * @return the number of items
     * @since 1.0.0
     */
    public long getCount(final Query query, final Object... parameters) {
        final org.datanucleus.store.query.Query internalQuery = ((JDOQuery) query).getInternalQuery();
        final String originalOrdering = internalQuery.getOrdering();
        query.setOrdering(null);
        query.setResult("count(this)");
        try {
            // NB: Don't close the query as it is to be reused.
            return (Long) query.executeWithArray(parameters);
        } finally {
            query.setOrdering(originalOrdering);
            query.setResult(null);
        }
    }

    /**
     * Returns the number of items that would have resulted from returning all object.
     * This method is performant in that the objects are not actually retrieved, only
     * the count.
     * @param query the query to return a count from
     * @param parameters the Map containing all the parameters.
     * @return the number of items
     * @since 1.0.0
     */
    public long getCount(final Query query, final Map parameters) {
        final org.datanucleus.store.query.Query internalQuery = ((JDOQuery) query).getInternalQuery();
        final String originalOrdering = internalQuery.getOrdering();
        query.setOrdering(null);
        query.setResult("count(this)");
        try {
            // NB: Don't close the query as it is to be reused.
            return (Long) query.executeWithMap(parameters);
        } finally {
            query.setOrdering(originalOrdering);
            query.setResult(null);
        }
    }

    /**
     * Returns the number of items that would have resulted from returning all object.
     * This method is performant in that the objects are not actually retrieved, only
     * the count.
     * @param cls the persistence-capable class to query
     * @return the number of items
     * @param  candidate type for the query
     * @since 1.0.0
     */
    public  long getCount(final Class cls) {
        final Query query = pm.newQuery(cls);
        query.setResult("count(id)");
        return executeAndCloseResultUnique(query, Long.class);
    }

    /**
     * Persists the specified PersistenceCapable object.
     * @param object a PersistenceCapable object
     * @param  the type to return
     * @return the persisted object
     */
    public  T persist(T object) {
        return callInTransaction(() -> pm.makePersistent(object));
    }

    /**
     * Persists the specified PersistenceCapable objects.
     * @param pcs an array of PersistenceCapable objects
     * @param  the type to return
     * @return the persisted objects
     */
    public  T[] persist(T... pcs) {
        return callInTransaction(() -> pm.makePersistentAll(pcs));
    }

    /**
     * Persists the specified PersistenceCapable objects.
     * @param pcs a collection of PersistenceCapable objects
     * @param  the type to return
     * @return the persisted objects
     */
    public  Collection persist(Collection pcs) {
        return callInTransaction(() -> pm.makePersistentAll(pcs));
    }

    /**
     * Deletes one or more PersistenceCapable objects.
     * @param objects an array of one or more objects to delete
     * @since 1.0.0
     */
    public void delete(Object... objects) {
        runInTransaction(() -> pm.deletePersistentAll(objects));
    }

    /**
     * Deletes one or more PersistenceCapable objects.
     * @param collection a collection of one or more objects to delete
     * @since 1.0.0
     */
    public void delete(Collection collection) {
        runInTransaction(() -> pm.deletePersistentAll(collection));
    }

    /**
     * Refreshes and detaches an object by its ID.
     * @param  A type parameter. This type will be returned
     * @param clazz the persistence class to retrieve the ID for
     * @param id the object id to retrieve
     * @return an object of the specified type
     * @since 1.3.0
     */
    public  T detach(Class clazz, Object id) {
        try (var ignored = new ScopedCustomization(pm).withDetachmentOptions(FetchPlan.DETACH_LOAD_FIELDS)) {
            return pm.detachCopy(pm.getObjectById(clazz, id));
        }
    }

    public  T detach(final T object) {
        try (var ignored = new ScopedCustomization(pm).withDetachmentOptions(FetchPlan.DETACH_LOAD_FIELDS)) {
            return pm.detachCopy(object);
        }
    }

    /**
     * Refreshes and detaches an objects.
     * @param pcs the instances to detach
     * @param  the type to return
     * @return the detached instances
     * @since 1.3.0
     */
    public  List detach(List pcs) {
        try (var ignored = new ScopedCustomization(pm).withDetachmentOptions(FetchPlan.DETACH_LOAD_FIELDS)) {
            return new ArrayList<>(pm.detachCopyAll(pcs));
        }
    }

    /**
     * Refreshes and detaches an objects.
     * @param pcs the instances to detach
     * @param  the type to return
     * @return the detached instances
     * @since 1.3.0
     */
    public  Set detach(Set pcs) {
        try (var ignored = new ScopedCustomization(pm).withDetachmentOptions(FetchPlan.DETACH_LOAD_FIELDS)) {
            return new LinkedHashSet<>(pm.detachCopyAll(pcs));
        }
    }

    /**
     * Transition {@code object} into the transient state, detaching it from the persistence context.
     * This does not create a copy of {@code object}!
     *
     * @param object The object to make transient
     * @param     The type of {@code object}
     * @return The transitioned object
     * @see JDO Object Lifecycle
     */
    public  T makeTransient(final T object) {
        pm.makeTransient(object);
        return object;
    }

    /**
     * Transitions {@code collection} into the transient state, detaching its items from the persistence context.
     * This does not create a copy of {@code collection}, or the items within it!
     *
     * @param collection The collection to make transient
     * @param         The type of {@code collection}
     * @param         The type of the items within {@code collection}
     * @return The transitioned collection
     * @see JDO Object Lifecycle
     */
    public > C makeTransientAll(final C collection) {
        pm.makeTransientAll(collection);
        return collection;
    }

    /**
     * Retrieves an object by its ID.
     * @param  A type parameter. This type will be returned
     * @param clazz the persistence class to retrieve the ID for
     * @param id the object id to retrieve
     * @return an object of the specified type
     * @since 1.0.0
     */
    public  T getObjectById(Class clazz, Object id) {
        return pm.getObjectById(clazz, id);
    }

    /**
     * Retrieves an object by its UUID.
     * @param  A type parameter. This type will be returned
     * @param clazz the persistence class to retrieve the ID for
     * @param uuid the uuid of the object to retrieve
     * @return an object of the specified type
     * @since 1.0.0
     */
    public  T getObjectByUuid(Class clazz, UUID uuid) {
        final Query query = pm.newQuery(clazz, "uuid == :uuid");
        query.setParameters(uuid);
        return executeAndCloseUnique(query);
    }

    /**
     * Retrieves an object by its UUID.
     * @param  A type parameter. This type will be returned
     * @param clazz the persistence class to retrieve the ID for
     * @param uuid the uuid of the object to retrieve
     * @return an object of the specified type
     * @since 1.0.0
     */
    public  T getObjectByUuid(Class clazz, String uuid) {
        return getObjectByUuid(clazz, UUID.fromString(uuid));
    }

    /**
     * Retrieves an object by its UUID.
     * @param  A type parameter. This type will be returned
     * @param clazz the persistence class to retrieve the ID for
     * @param uuid the uuid of the object to retrieve
     * @param fetchGroup the JDO fetch group to use when making the query
     * @return an object of the specified type
     * @since 1.0.0
     */
    public  T getObjectByUuid(Class clazz, UUID uuid, String fetchGroup) {
        final Query query = pm.newQuery(clazz, "uuid == :uuid");
        query.getFetchPlan().addGroup(fetchGroup);
        query.setParameters(uuid);
        return executeAndCloseUnique(query);
    }

    /**
     * Retrieves an object by its UUID.
     * @param  A type parameter. This type will be returned
     * @param clazz the persistence class to retrieve the ID for
     * @param uuid the uuid of the object to retrieve
     * @param fetchGroup the JDO fetch group to use when making the query
     * @return an object of the specified type
     * @since 1.0.0
     */
    public  T getObjectByUuid(Class clazz, String uuid, String fetchGroup) {
        return getObjectByUuid(clazz, UUID.fromString(uuid), fetchGroup);
    }

    /**
     * Used to return the first record in a collection. This method is intended to be used
     * to wrap {@link Query#execute()} and its derivatives.
     * @param object a collection object (or anything that extends collection)
     * @param  the type of object returned, or null if object was null, not a collection, or collection was empty
     * @return A single results
     * @since 1.4.4
     */
    @SuppressWarnings("unchecked")
    public  T singleResult(Object object) {
        if (object == null) {
            return null;
        }
        if (object instanceof Collection) {
            final Collection result = (Collection)object;
            return CollectionUtils.isEmpty(result) ? null : result.iterator().next();
        }
        return null;
    }

    /**
     * Closes the PersistenceManager instance.
     * @since 1.0.0
     */
    public void close() {
        if (pm != null) {
            pm.close();
        }
    }

    public PersistenceManager getPersistenceManager() {
        return pm;
    }

    /**
     * Execute a {@link Callable} within the context of a transaction.
     *
     * @param options  The {@link Transaction.Options} to apply to the transaction
     * @param callable The {@link Callable} to execute
     * @param       Type of the result returned by {@code callable}
     * @return The result of {@code callable} after transaction commit
     */
    public  T callInTransaction(final Transaction.Options options, final Callable callable) {
        return Transaction.call(pm, options, callable);
    }

    /**
     * Execute a {@link Callable} within the context of a transaction.
     *
     * @param callable The {@link Callable} to execute
     * @param       Type of the result returned by {@code callable}
     * @return The result of {@code callable} after transaction commit
     */
    public  T callInTransaction(final Callable callable) {
        return callInTransaction(Transaction.defaultOptions(), callable);
    }

    /**
     * Execute a {@link Runnable} within the context of a transaction.
     *
     * @param options  The {@link Transaction.Options} to apply to the transaction
     * @param runnable The {@link Callable} to execute
     */
    public void runInTransaction(final Transaction.Options options, final Runnable runnable) {
        callInTransaction(options, () -> {
            runnable.run();
            return null;
        });
    }

    /**
     * Execute a {@link Runnable} within the context of a transaction.
     *
     * @param runnable The {@link Callable} to execute
     */
    public void runInTransaction(final Runnable runnable) {
        runInTransaction(Transaction.defaultOptions(), runnable);
    }

    /**
     * Wrapper around {@link Query#execute()} that closes the {@link Query}
     * after its result has been retrieved, to prevent resource leakage.
     *
     * @param query The {@link Query} to execute
     * @return The {@link Query}'s result
     */
    protected Object executeAndClose(final Query query) {
        try {
            final Object result = query.execute();
            if (result instanceof final Collection resultCollection) {
                return new ArrayList<>(resultCollection);
            }

            return result;
        } finally {
            query.closeAll();
        }
    }

    /**
     * Wrapper around {@link Query#executeWithArray(Object...)} that closes the {@link Query}
     * after its result has been retrieved, to prevent resource leakage.
     *
     * @param query      The {@link Query} to execute
     * @param parameters The query parameters
     * @return The {@link Query}'s result
     */
    protected Object executeAndCloseWithArray(final Query query, final Object... parameters) {
        try {
            final Object result = query.executeWithArray(parameters);
            if (result instanceof final Collection resultCollection) {
                return new ArrayList<>(resultCollection);
            }

            return result;
        } finally {
            query.closeAll();
        }
    }

    /**
     * Wrapper around {@link Query#executeWithMap(Map)} that closes the {@link Query}
     * after its result has been retrieved, to prevent resource leakage.
     *
     * @param query      The {@link Query} to execute
     * @param parameters The query parameters
     * @return The {@link Query}'s result
     */
    protected Object executeAndCloseWithMap(final Query query, final Map parameters) {
        try {
            final Object result = query.executeWithMap(parameters);
            if (result instanceof final Collection resultCollection) {
                return new ArrayList<>(resultCollection);
            }

            return result;
        } finally {
            query.closeAll();
        }
    }

    /**
     * Wrapper around {@link Query#executeUnique()} that closes the {@link Query}
     * after its result has been retrieved, to prevent resource leakage.
     *
     * @param query The {@link Query} to execute
     * @param    Type of the {@link Query}'s result
     * @return The {@link Query}'s result
     */
    protected  T executeAndCloseUnique(final Query query) {
        try {
            return query.executeUnique();
        } finally {
            query.closeAll();
        }
    }

    /**
     * Wrapper around {@link Query#executeList()} that closes the {@link Query}
     * after its result has been retrieved, to prevent resource leakage.
     *
     * @param query The {@link Query} to execute
     * @param    Type of the {@link Query}'s result
     * @return The {@link Query}'s result
     */
    protected  List executeAndCloseList(final Query query) {
        try {
            return new ArrayList<>(query.executeList());
        } finally {
            query.closeAll();
        }
    }

    /**
     * Wrapper around {@link Query#executeResultUnique()} that closes the {@link Query}
     * after its result has been retrieved, to prevent resource leakage.
     *
     * @param query       The {@link Query} to execute
     * @param resultClass The {@link Class} of the {@link Query}'s result
     * @param          Type of the {@link Query}'s result
     * @return The {@link Query}'s result
     */
    protected  T executeAndCloseResultUnique(final Query query, final Class resultClass) {
        try {
            return query.executeResultUnique(resultClass);
        } finally {
            query.closeAll();
        }
    }

    /**
     * Wrapper around {@link Query#executeResultList()} that closes the {@link Query}
     * after its result has been retrieved, to prevent resource leakage.
     *
     * @param query       The {@link Query} to execute
     * @param resultClass The {@link Class} of the {@link Query}'s result
     * @param          Type of the {@link Query}'s result
     * @return The {@link Query}'s result
     */
    protected  List executeAndCloseResultList(final Query query, final Class resultClass) {
        try {
            return new ArrayList<>(query.executeResultList(resultClass));
        } finally {
            query.closeAll();
        }
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy