/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 org.apache.openjpa.abstractstore;

import java.util.BitSet;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;

import org.apache.openjpa.conf.OpenJPAConfiguration;
import org.apache.openjpa.conf.OpenJPAConfigurationImpl;
import org.apache.openjpa.kernel.FetchConfiguration;
import org.apache.openjpa.kernel.FetchConfigurationImpl;
import org.apache.openjpa.kernel.OpenJPAStateManager;
import org.apache.openjpa.kernel.PCState;
import org.apache.openjpa.kernel.Seq;
import org.apache.openjpa.kernel.StoreContext;
import org.apache.openjpa.kernel.StoreManager;
import org.apache.openjpa.kernel.StoreQuery;
import org.apache.openjpa.lib.rop.ResultObjectProvider;
import org.apache.openjpa.meta.ClassMetaData;
import org.apache.openjpa.meta.FieldMetaData;
import org.apache.openjpa.meta.JavaTypes;
import org.apache.openjpa.meta.ValueStrategies;
import org.apache.openjpa.util.ApplicationIds;
import org.apache.openjpa.util.Id;
import org.apache.openjpa.util.ImplHelper;

/**
 * Abstract store manager implementation to ease development of custom
 * OpenJPA back-ends. A concrete subclass must define implementations for the
 * following methods:
 * <ul>
 * <li>{@link StoreManager#exists}</li>
 * <li>{@link #initialize}</li>
 * <li>{@link #load}</li>
 * <li>{@link
 * #flush(Collection,Collection,Collection,Collection,Collection)}</li>
 * <li>{@link #executeExtent}</li>
 * </ul> Additionally, subclasses should not attempt to acquire resources
 * until {@link #open} has been called. Store manager instances might be
 * created to call metadata methods such as {@link #newConfiguration} or
 * {@link #getUnsupportedOptions} and never opened. These instances should
 * not consume any data store resources.
 *  Notes:
 * <ul>
 * <li>The {@link StoreManager#initialize} method is responsible
 * for creating new instances of objects freshly loaded from the
 * database. The method will be invoked with a {@link OpenJPAStateManager}
 * that the newly-loaded object should be associated with. To create the
 * new object and set up this association correctly, the implementation
 * should use the {@link OpenJPAStateManager#initialize} method.</li>
 * <li>If your data store supports some sort of transaction or
 * unit of work, you should override the {@link #begin}, {@link #commit},
 * and {@link #rollback} methods.</li>
 * <li>This class provides no infrastructure support for optimistic
 * transactions. To provide optimistic transaction support:
 * <ul>
 * <li>Override {@link #beginOptimistic}, {@link #rollbackOptimistic},
 * and {@link #syncVersion}.</li>
 * <li>Override {@link #getUnsupportedOptions} to not include {@link
 * OpenJPAConfiguration#OPTION_OPTIMISTIC} in the list of unsupported
 * options.</li>
 * <li>Ensure that your flush implementation sets the next
 * version for each modified object via the {@link
 * OpenJPAStateManager#setNextVersion} method.</li>
 * <li>If your version object does not implement {@link Comparable},
 * override {@link #compareVersion}, which relies on the
 * {@link Comparable#compareTo} method.</li>
 * </ul></li>
 * <li>If your data store supports a mechanism for automatically
 * generating and managing identity values (or if you want to
 * provide that facility on top of your data store), implement
 * the {@link #getDataStoreIdSequence} method if you want to use a
 * <code>long</code> as your datastore identity type and are
 * happy with OpenJPA's {@link Id} class. To use another datastore identity
 * type, override {@link #getManagedType},
 * {@link #getDataStoreIdType}, {@link #copyDataStoreId}, and
 * {@link #newDataStoreId} instead. In either case, override
 * {@link #getUnsupportedOptions} to not include
 * {@link OpenJPAConfiguration#OPTION_ID_DATASTORE} in the list of
 * unsupported options.</li>
 * <li>If your data store does not support queries (or if you do
 * not want to convert OpenJPA's query parse tree into a
 * datastore-specific query), you still have two options in terms
 * of query execution:
 * <ul>
 * <li><em>In-memory execution</em>: If you
 * execute a query against an extent or a class, OpenJPA will
 * automatically load the full extent of objects into memory and
 * execute the query in memory.</li>
 * <li><em>openjpa.MethodQL</em>: MethodQL allows
 * you to use the query APIs to execute a method that finds
 * data in your back-end and returns that data as a
 * {@link org.apache.openjpa.lib.rop.ResultList}. For more details on
 * MethodQL, see the OpenJPA Reference Guide.</li>
 * </ul></li>
 * </ul>
 *
 * @since 0.3.1
 */
public abstract class AbstractStoreManager
    implements StoreManager {

    protected StoreContext ctx;

    @Override
    public final void setContext(StoreContext ctx) {
        this.ctx = ctx;
        open();
    }

    /**
     * Returns the {@link StoreContext} that this store manager is
     * associated with.
     */
    public StoreContext getContext() {
        return ctx;
    }

    /**
     * No-op implementation. Ready this store manager for persistent operations.
     */
    protected void open() {
    }

    /**
     * No-op implementation. Override this method to provide optimistic
     * locking semantics for your data store if you need notification of
     * the beginning of an optimistic transaction.
     */
    @Override
    public void beginOptimistic() {
    }

    /**
     * No-op implementation. Override this method to provide optimistic
     * locking semantics for your data store if you need notification of
     * a rollback of an optimistic transaction before {@link #begin} is invoked.
     */
    @Override
    public void rollbackOptimistic() {
    }

    /**
     * OpenJPA assumes that after this method is invoked, all data
     * accesses through this store manager will be part of a single
     * unit of work that can be rolled back.
     *  This is a no-op implementation. If your data store does not
     * support any concept of locking or transactions, you need not
     * override this method.
     */
    @Override
    public void begin() {
    }

    /**
     * This is a no-op implementation. If your data store does not
     * have a concept of transactions or a unit of work, you need not
     * override this method. If it does, then override this method to
     * notify the data store that the current transaction should be committed.
     */
    @Override
    public void commit() {
    }

    /**
     * This is a no-op implementation. If your data store does not
     * have a concept of transactions or a unit of work, you need not
     * override this method. If it does, then override this method to
     * notify the data store that the current transaction should be rolled back.
     */
    @Override
    public void rollback() {
    }

    /**
     * Since this store manager does not provide optimistic locking
     * support, this method always returns <code>true</code>.
     */
    @Override
    public boolean syncVersion(OpenJPAStateManager sm, Object edata) {
        return true;
    }

    /**
     * This method is invoked when OpenJPA needs to load an object whose
     * identity is known but which has not yet been loaded from the data
     * store. <code>sm</code> is a partially-set-up state manager for this
     * object. The ID and least-derived type information for the instance
     * to load can be obtained by invoking
     * <code>sm.getObjectId()</code> and <code>sm.getMetaData()</code>.
     *
     *  When implementing this method, load the data for this object from
     * the data store, determine the most-derived subclass of the newly-loaded
     * data, and then use the {@link OpenJPAStateManager#initialize} method to
     * populate <code>sm</code> with a new instance of the appropriate type.
     * Once {@link OpenJPAStateManager#initialize} has been invoked, proceed to
     * load field data into <code>sm</code> as in the {@link #load} method, by
     * using {@link OpenJPAStateManager#store} (or the appropriate
     * <code>OpenJPAStateManager.store<em>type</em></code> method) to put the
     * data into the object.
     */
    @Override
    public abstract boolean initialize(OpenJPAStateManager sm, PCState state,
        FetchConfiguration fetch, Object edata);

    /**
     * This method is invoked when OpenJPA needs to load additional data
     * into an object that has already been at least partially loaded by
     * a previous {@link #initialize} invocation.
     *  Load data into <code>sm</code> by using {@link
     * OpenJPAStateManager#store} (or the appropriate
     * <code>OpenJPAStateManager.store<em>type</em></code> method) to put the
     * data into the object.
     */
    @Override
    public abstract boolean load(OpenJPAStateManager sm, BitSet fields,
        FetchConfiguration fetch, int lockLevel, Object edata);

    /**
     * This implementation just delegates to the proper singular
     * method ({@link StoreManager#initialize} or {@link StoreManager#load})
     * depending on each state manager's state. If your data store provides
     * bulk loading APIs, overriding this method to be more clever may be
     * advantageous.
     */
    @Override
    public Collection<Object> loadAll(Collection<OpenJPAStateManager> sms, PCState state, int load,
        FetchConfiguration fetch, Object edata) {
        return ImplHelper.loadAll(sms, this, state, load, fetch, edata);
    }

    /**
     * Breaks down <code>states</code> based on the objects' current
     * states, and delegates to
     * {@link #flush(Collection,Collection,Collection,Collection,Collection)}.
     */
    @Override
    public Collection<Exception> flush(Collection<OpenJPAStateManager> sms) {
        // break down state managers by state; initialize as empty lists;
        // use constants for efficiency
        Collection<OpenJPAStateManager> pNew = new LinkedList<>();
        Collection<OpenJPAStateManager> pNewUpdated = new LinkedList<>();
        Collection<OpenJPAStateManager> pNewFlushedDeleted = new LinkedList<>();
        Collection<OpenJPAStateManager> pDirty = new LinkedList<>();
        Collection<OpenJPAStateManager> pDeleted = new LinkedList<>();

        for (OpenJPAStateManager sm : sms) {
            if (sm.getPCState() == PCState.PNEW && !sm.isFlushed())
                pNew.add(sm);
            else if (sm.getPCState() == PCState.PNEW && sm.isFlushed())
                pNewUpdated.add(sm);
            else if (sm.getPCState() == PCState.PNEWFLUSHEDDELETED)
                pNewFlushedDeleted.add(sm);
            else if (sm.getPCState() == PCState.PDIRTY)
                pDirty.add(sm);
            else if (sm.getPCState() == PCState.PDELETED)
                pDeleted.add(sm);
        }

        // no dirty instances to flush?
        if (pNew.isEmpty() && pNewUpdated.isEmpty()
            && pNewFlushedDeleted.isEmpty() && pDirty.isEmpty()
            && pDeleted.isEmpty())
            return Collections.EMPTY_LIST;

        return flush(pNew, pNewUpdated, pNewFlushedDeleted, pDirty, pDeleted);
    }

    @Override
    public void beforeStateChange(OpenJPAStateManager sm, PCState fromState,
        PCState toState) {
    }

    @Override
    public boolean assignObjectId(OpenJPAStateManager sm, boolean preFlush) {
        ClassMetaData meta = sm.getMetaData();
        if (meta.getIdentityType() == ClassMetaData.ID_APPLICATION)
            return ApplicationIds.assign(sm, this, preFlush);

        // datastore identity
        Object val = ImplHelper.generateIdentityValue(ctx, meta,
            JavaTypes.LONG);
        return assignDataStoreId(sm, val);
    }

    /**
     * Assign a new datastore identity to the given instance. This given
     * value may be null.
     */
    protected boolean assignDataStoreId(OpenJPAStateManager sm, Object val) {
        ClassMetaData meta = sm.getMetaData();
        if (val == null && meta.getIdentityStrategy() != ValueStrategies.NATIVE)
            return false;
        if (val == null)
            val = getDataStoreIdSequence(meta).next(ctx, meta);
        sm.setObjectId(newDataStoreId(val, meta));
        return true;
    }

    @Override
    public boolean assignField(OpenJPAStateManager sm, int field,
        boolean preFlush) {
        FieldMetaData fmd = sm.getMetaData().getField(field);
        Object val = ImplHelper.generateFieldValue(ctx, fmd);
        if (val == null)
            return false;
        sm.store(field, val);
        return true;
    }

    @Override
    public Class<?> getManagedType(Object oid) {
        if (oid instanceof Id)
            return ((Id) oid).getType();
        return null;
    }

    @Override
    public Class<?> getDataStoreIdType(ClassMetaData meta) {
        return Id.class;
    }

    @Override
    public Object copyDataStoreId(Object oid, ClassMetaData meta) {
        Id id = (Id) oid;
        return new Id(meta.getDescribedType(), id.getId(),
            id.hasSubclasses());
    }

    @Override
    public Object newDataStoreId(Object val, ClassMetaData meta) {
        // we use base types for all oids
        while (meta.getPCSuperclass() != null)
            meta = meta.getPCSuperclassMetaData();
        return Id.newInstance(meta.getDescribedType(), val);
    }

    /**
     * Override to retain a dedicated connection.
     */
    @Override
    public void retainConnection() {
    }

    /**
     * Override to release previously-retained connection.
     */
    @Override
    public void releaseConnection() {
    }

    /**
     * Returns <code>null</code>. If your data store can provide a
     * distinct connection object, return it here.
     */
    @Override
    public Object getClientConnection() {
        return null;
    }

    /**
     * Create a {@link ResultObjectProvider} that can return all instances
     * of <code>type</code>, optionally including subclasses as defined
     * by <code>subclasses</code>.
     *  The implementation of the result provider will typically execute
     * some sort of data store query to find all the applicable objects, loop
     * through the results, extracting object IDs from the data, and invoke
     * {@link StoreContext#find(Object,FetchConfiguration,BitSet,Object,int)}
     * on each OID. When invoking this method, the first argument is the OID.
     * The second is the given fetch configuration. The
     * third argument is a mask of fields to exclude from loading; it will
     * typically be null. The fourth argument is an object that will be passed
     * through to {@link #initialize} or {@link #load}, and typically will
     * contain the actual data to load. For example, for a JDBC-based store
     * manager, this might be the result set that is being iterated over. If
     * this argument is <code>null</code>, then the {@link #initialize} or
     * {@link #load} method will have to issue another command to the data
     * store in order to fetch the data to be loaded.
     */
    @Override
    public abstract ResultObjectProvider executeExtent(ClassMetaData meta,
        boolean subs, FetchConfiguration fetch);

    @Override
    public StoreQuery newQuery(String language) {
        return null;
    }

    @Override
    public FetchConfiguration newFetchConfiguration() {
        return new FetchConfigurationImpl();
    }

    /**
     * Casts <code>v1</code> and <code>v2</code> to {@link Comparable}, and
     * invokes <code>v1.compareTo (v2)</code>. If <code>v1</code> is less
     * than <code>v2</code>, returns {@link #VERSION_EARLIER}. If the same,
     * returns {@link #VERSION_SAME}. Otherwise, returns {@link
     * #VERSION_LATER}. If either <code>v1</code> or <code>v2</code> are
     * <code>null</code>, returns {@link #VERSION_DIFFERENT}.
     */
    @Override
    public int compareVersion(OpenJPAStateManager state, Object v1, Object v2) {
        if (v1 == null || v2 == null)
            return VERSION_DIFFERENT;

        int compare = ((Comparable) v1).compareTo((Comparable) v2);
        if (compare < 0)
            return VERSION_EARLIER;
        if (compare == 0)
            return VERSION_SAME;
        return VERSION_LATER;
    }

    /**
     * Returns the system-configured sequence. To use some other sort
     * of datastore identifier (a GUID, string, or someting of that nature),
     * override {@link #getManagedType},
     * {@link #getDataStoreIdType}, {@link #copyDataStoreId},
     * {@link #newDataStoreId}.
     */
    @Override
    public Seq getDataStoreIdSequence(ClassMetaData forClass) {
        return ctx.getConfiguration().getSequenceInstance();
    }

    /**
     * Returns null.
     */
    @Override
    public Seq getValueSequence(FieldMetaData forField) {
        return null;
    }

    /**
     * Returns <code>false</code>. If your data store supports
     * cancelling queries, this method should cancel any
     * currently-running queries and return <code>true</code> if any
     * were cancelled.
     */
    @Override
    public boolean cancelAll() {
        return false;
    }

    @Override
    public void close() {
    }

    /**
     * Responsible for writing modifications happened back to the data
     * store. If you do not remove the
     * {@link OpenJPAConfiguration#OPTION_INC_FLUSH} option in
     * {@link #getUnsupportedOptions}, this will be called only once at the
     * end	of a transaction. Otherwise, it may be called periodically
     * throughout the course of a transaction.
     *  If this store manager supports optimistic transactions, datastore
     * version information should be updated during flush, and the state
     * manager's version indicator should be updated through the
     * {@link OpenJPAStateManager#setNextVersion} method.
     *  This method will only be invoked if there are meaningful changes
     * to store. This differs from the behavior of {@link StoreManager#flush},
     * which may be invoked with a collection of objects in states that
     * do not require any datastore action (for example, objects in the
     * transient-transactional state).
     *
     * @param pNew Objects that should be added to the store,
     * and that have not previously been flushed.
     * @param pNewUpdated New objects that have been modified since
     * they were initially flushed. These were
     * in <code>persistentNew</code> in an earlier flush invocation.
     * @param pNewFlushedDeleted New objects that have been deleted since
     * they were initially flushed. These were
     * in <code>persistentNew</code> in an earlier flush invocation.
     * @param pDirty Objects that were loaded from the data
     * store and have since been modified.
     * @param pDeleted Objects that were loaded from the data
     * store and have since been deleted. These
     * may have been in a previous flush invocation's persistentDirty list.
     * @return a collection of exceptions encountered during flushing.
     */
    protected abstract Collection<Exception> flush(Collection<OpenJPAStateManager> pNew,
        Collection<OpenJPAStateManager> pNewUpdated, Collection<OpenJPAStateManager> pNewFlushedDeleted,
        Collection<OpenJPAStateManager> pDirty, Collection<OpenJPAStateManager> pDeleted);

    /**
     * Return a new configuration instance for this runtime. Configuration
     * data is maintained at the factory level and is available to all OpenJPA
     * components; therefore it is a good place to maintain shared resources
     * such as connection pools, etc.
     */
    protected OpenJPAConfiguration newConfiguration() {
        return new OpenJPAConfigurationImpl();
    }

    /**
     * Returns a set of option names that this store manager does
     * not support. By default, returns the following:
     * <ul>
     * <li>{@link OpenJPAConfiguration#OPTION_OPTIMISTIC}</li>
     * <li>{@link OpenJPAConfiguration#OPTION_ID_DATASTORE}</li>
     * <li>{@link OpenJPAConfiguration#OPTION_INC_FLUSH}</li>
     * <li>{@link OpenJPAConfiguration#OPTION_VALUE_AUTOASSIGN}</li>
     * <li>{@link OpenJPAConfiguration#OPTION_VALUE_INCREMENT}</li>
     * <li>{@link OpenJPAConfiguration#OPTION_DATASTORE_CONNECTION}</li>
     * </ul>
     */
    protected Collection<String> getUnsupportedOptions() {
        Collection<String> c = new HashSet<>();
        c.add(OpenJPAConfiguration.OPTION_OPTIMISTIC);
        c.add(OpenJPAConfiguration.OPTION_ID_DATASTORE);
        c.add(OpenJPAConfiguration.OPTION_INC_FLUSH);
        c.add(OpenJPAConfiguration.OPTION_VALUE_AUTOASSIGN);
        c.add(OpenJPAConfiguration.OPTION_VALUE_INCREMENT);
        c.add(OpenJPAConfiguration.OPTION_DATASTORE_CONNECTION);
        return c;
    }

    /**
     * Returns a string name to identify the platform of this
     * store manager. Returns the class name of this store manager by default.
     */
    protected String getPlatform ()
	{
		return getClass ().getName ();
	}
}
