/*
 *  Copyright 2016-2019 Netflix, Inc.
 *
 *     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.netflix.hollow.api.producer;

import static com.netflix.hollow.api.consumer.HollowConsumer.AnnouncementWatcher.NO_ANNOUNCEMENT_AVAILABLE;

import com.netflix.hollow.api.consumer.HollowConsumer;
import com.netflix.hollow.api.metrics.HollowMetricsCollector;
import com.netflix.hollow.api.metrics.HollowProducerMetrics;
import com.netflix.hollow.api.producer.enforcer.BasicSingleProducerEnforcer;
import com.netflix.hollow.api.producer.enforcer.SingleProducerEnforcer;
import com.netflix.hollow.api.producer.fs.HollowFilesystemBlobStager;
import com.netflix.hollow.api.producer.listener.HollowProducerEventListener;
import com.netflix.hollow.api.producer.validation.ValidatorListener;
import com.netflix.hollow.core.read.engine.HollowReadStateEngine;
import com.netflix.hollow.core.schema.HollowSchema;
import com.netflix.hollow.core.util.HollowObjectHashCodeFinder;
import com.netflix.hollow.core.write.HollowBlobWriter;
import com.netflix.hollow.core.write.HollowWriteStateEngine;
import com.netflix.hollow.core.write.objectmapper.HollowObjectMapper;
import com.netflix.hollow.core.write.objectmapper.RecordPrimaryKey;
import com.netflix.hollow.tools.compact.HollowCompactor;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;

/**
 * A HollowProducer is the top-level class used by producers of Hollow data to populate, publish, and announce data states.
 * The interactions between the "blob" store and announcement mechanism are defined by this class, and the implementations
 * of the data publishing and announcing are abstracted in interfaces which are provided to this class.
 *

 * To obtain a HollowProducer, you should use a builder pattern, for example:
 *
 *

 * {@code
 *
 * HollowProducer producer = HollowProducer.withPublisher(publisher)
 *                                         .withAnnouncer(announcer)
 *                                         .build();
 * }
 *

 *

 * The following components are injectable, but only an implementation of the HollowProducer.Publisher is
 * required to be injected, all other components are optional. :
 *
 *

 *
{@link HollowProducer.Publisher}

 *
Implementations of this class define how to publish blob data to the blob store.

 *
 *
{@link HollowProducer.Announcer}

 *
Implementations of this class define the announcement mechanism, which is used to track the version of the
 * currently announced state.

 *
 *
One or more event listeners

 *
Listeners are notified about the progress and status of producer cycles throughout the various cycle stages.
 * Of special note are {@link ValidatorListener} that allow for semantic validation of the data contained in
 * a state prior to announcement.  If a {@code RuntimeException} is thrown during validation, the state will not be
 * announced, and the producer will be automatically rolled back to the prior state.
 *

 *
 *
A Blob staging directory

 *
Before blobs are published, they must be written and inspected/validated.  A directory may be specified as a File to which
 * these "staged" blobs will be written prior to publish.  Staged blobs will be cleaned up automatically after publish.

 *
 *
{@link HollowProducer.BlobCompressor}

 *
Implementations of this class intercept blob input/output streams to allow for compression in the blob store.

 *
 *
{@link HollowProducer.BlobStager}

 *
Implementations will define how to stage blobs, if the default behavior of staging blobs on local disk is not desirable.
 * If a {@link BlobStager} is provided, then neither a blob staging directory or {@link BlobCompressor} should be provided.

 *
 *
An Executor for publishing snapshots

 *
When consumers start up, if the latest announced version does not have a snapshot, they can load an earlier snapshot
 * and follow deltas to get up-to-date.  A state can therefore be available and announced prior to the availability of
 * the snapshot.  If an Executor is supplied here, then it will be used to publish snapshots.  This can be useful if
 * snapshot publishing takes a long time -- subsequent cycles may proceed while snapshot uploads are still in progress.

 *
 *
Number of cycles between snapshots

 *
Because snapshots are not necessary for a data state to be announced, they need not be published every cycle.
 * If this parameter is specified, then a snapshot will be produced only every (n+1)th cycle.

 *
 *
{@link HollowProducer.VersionMinter}

 *
Allows for a custom version identifier minting strategy.

 *
 *
Target max type shard size

 *
Specify a target max type shard size.  Defaults to 16MB.  See http://hollow.how/advanced-topics/#type-sharding

 *

 *
 * @author Tim Taylor {@literal<tim@toolbear.io>}
 */
public class HollowProducer extends AbstractHollowProducer {

    /*
     * HollowProducer and HollowProducer.Incremental extend from a package protected AbstractHollowProducer
     * for sharing common functionality.
     * To preserve binary compatibility HollowProducer overrides many methods on AbstractHollowProducer
     * and directly defers to them (in effect explicit bridge methods).
     */

    @Deprecated
    public HollowProducer(
            Publisher publisher,
            Announcer announcer) {
        super(publisher, announcer);
    }

    // The only constructor should be that which accepts a builder
    // This ensures that if the builder modified to include new state that
    // extended builders will not require modification to pass on that new state
    protected HollowProducer(Builder<?> b) {
        super(b);
    }

    /**
     * @return the metrics for this producer
     */
    @Override
    public HollowProducerMetrics getMetrics() {
        return super.getMetrics();
    }

    /**
     * Initializes the data model for the given classes.
     *

     * Data model initialization is required prior to {@link #restore(long, HollowConsumer.BlobRetriever) restoring}
     * the producer.
     * This ensures that restoration can correctly compare the producer's current data model
     * with the data model of the restored data state and manage any differences in those models
     * (such as not restoring state for any types in the restoring data model not present in the
     * producer's current data model).
     *

     * After initialization a data model initialization event will be emitted
     * to all registered data model initialization listeners
     * {@link com.netflix.hollow.api.producer.listener.DataModelInitializationListener listeners}.
     *
     * @param classes the data model classes
     * @throws IllegalArgumentException if {@code classes} is empty
     * @see #restore(long, HollowConsumer.BlobRetriever)
     */
    @Override
    public void initializeDataModel(Class<?>... classes) {
        super.initializeDataModel(classes);
    }

    /**
     * Initializes the producer data model for the given schemas.
     *

     * Data model initialization is required prior to {@link #restore(long, HollowConsumer.BlobRetriever) restoring}
     * the producer.
     * This ensures that restoration can correctly compare the producer's current data model
     * with the data model of the restored data state and manage any differences in those models
     * (such as not restoring state for any types in the restoring data model not present in the
     * producer's current data model).
     *

     * After initialization a data model initialization event will be emitted
     * to all registered data model initialization listeners
     * {@link com.netflix.hollow.api.producer.listener.DataModelInitializationListener listeners}.
     *
     * @param schemas the data model classes
     * @throws IllegalArgumentException if {@code schemas} is empty
     * @see #restore(long, HollowConsumer.BlobRetriever)
     */
    @Override
    public void initializeDataModel(HollowSchema... schemas) {
        super.initializeDataModel(schemas);
    }

    /**
     * Restores the data state to a desired version.
     *

     * Data model {@link #initializeDataModel(Class[]) initialization} is required prior to
     * restoring the producer.  This ensures that restoration can correctly compare the producer's
     * current data model with the data model of the restored data state and manage any differences
     * in those models (such as not restoring state for any types in the restoring data model not
     * present in the producer's current data model)
     *
     * @param versionDesired the desired version
     * @param blobRetriever the blob retriever
     * @return the read state of the restored state
     * @throws IllegalStateException if the producer's data model has not been initialized
     * @see #initializeDataModel(Class[])
     */
    @Override
    public HollowProducer.ReadState restore(long versionDesired, HollowConsumer.BlobRetriever blobRetriever) {
        return super.restore(versionDesired, blobRetriever);
    }

    @Override
    public HollowWriteStateEngine getWriteEngine() {
        return super.getWriteEngine();
    }

    @Override
    public HollowObjectMapper getObjectMapper() {
        return super.getObjectMapper();
    }

    /**
     * Invoke this method to alter runCycle behavior. If this Producer is not primary, runCycle is a no-op. Note that by default,
     * SingleProducerEnforcer is instantiated as BasicSingleProducerEnforcer, which is initialized to return true for isPrimary()
     *
     * @param doEnable true if enable primary producer, if false
     * @return true if the intended action was successful
     */
    @Override
    public boolean enablePrimaryProducer(boolean doEnable) {
        return super.enablePrimaryProducer(doEnable);
    }

    /**
     * Runs a cycle to populate, publish, and announce a new single data state.
     *
     * @param task the populating task to add complete state
     * @return the version identifier of the announced state, otherwise the
     * last successful announced version if 1) there were no data changes compared to that version;
     * or 2) the producer is not the primary producer
     * @throws RuntimeException if the cycle failed
     */
    // @@@ Should this be marked as synchronized?
    public long runCycle(HollowProducer.Populator task) {
        return runCycle(null, task);
    }

    /**
     * Run a compaction cycle, will produce a data state with exactly the same data as currently, but
     * reorganized so that ordinal holes are filled.  This may need to be run multiple times to arrive
     * at an optimal state.
     *
     * @param config specifies what criteria to use to determine whether a compaction is necessary
     * @return the version identifier of the produced state, or AnnouncementWatcher.NO_ANNOUNCEMENT_AVAILABLE if compaction was unnecessary.
     */
    public long runCompactionCycle(HollowCompactor.CompactionConfig config) {
        if (config != null && readStates.hasCurrent()) {
            final HollowCompactor compactor = new HollowCompactor(getWriteEngine(),
                    readStates.current().getStateEngine(), config);
            if (compactor.needsCompaction()) {
                return runCycle(newState -> compactor.compact());
            }
        }

        return NO_ANNOUNCEMENT_AVAILABLE;
    }

    /**
     * Adds a listener to this producer.
     *

     * If the listener was previously added to this consumer, as determined by reference equality or {@code Object}
     * equality, then this method does nothing.
     *

     * If a listener is added, concurrently, during the occurrence of a cycle or restore then the listener will not
     * receive events until the next cycle or restore.  The listener may also be removed concurrently.
     *
     * @param listener the listener to add
     */
    @Override
    public void addListener(HollowProducerListener listener) {
        super.addListener(listener);
    }

    /**
     * Adds a listener to this producer.
     *

     * If the listener was previously added to this consumer, as determined by reference equality or {@code Object}
     * equality, then this method does nothing.
     *

     * If a listener is added, concurrently, during the occurrence of a cycle or restore then the listener will not
     * receive events until the next cycle or restore.  The listener may also be removed concurrently.
     *
     * @param listener the listener to add
     */
    @Override
    public void addListener(HollowProducerEventListener listener) {
        super.addListener(listener);
    }

    /**
     * Removes a listener to this producer.
     *

     * If the listener was not previously added to this producer, as determined by reference equality or {@code Object}
     * equality, then this method does nothing.
     *

     * If a listener is removed, concurrently, during the occurrence of a cycle or restore then the listener will
     * receive all events for that cycle or restore but not receive events for a subsequent cycle or restore.
     *
     * @param listener the listener to remove
     */
    @Override
    public void removeListener(HollowProducerListener listener) {
        super.removeListener(listener);
    }

    /**
     * Removes a listener to this producer.
     *

     * If the listener was not previously added to this producer, as determined by reference equality or {@code Object}
     * equality, then this method does nothing.
     *

     * If a listener is removed, concurrently, during the occurrence of a cycle or restore then the listener will
     * receive all events for that cycle or restore but not receive events for a subsequent cycle or restore.
     *
     * @param listener the listener to remove
     */
    @Override
    public void removeListener(HollowProducerEventListener listener) {
        super.removeListener(listener);
    }

    public static final class ChecksumValidationException extends IllegalStateException {
        private static final long serialVersionUID = -4399719849669674206L;

        ChecksumValidationException(Blob.Type type) {
            super(type.name() + " checksum invalid");
        }
    }

    public interface VersionMinter {
        /**
         * Create a new state version.
         *

         * State versions should be ascending -- later states have greater versions.

         *
         * @return a new state version
         */
        long mint();
    }

    /**
     * Represents a task that populates a new data state within a {@link HollowProducer} cycle.
     *
     *
This is a functional interface whose functional method is
     * {@link #populate(HollowProducer.WriteState)}.
     */
    @FunctionalInterface
    public interface Populator {

        /**
         * Populates the provided {@link WriteState} with new objects. Often written as a lambda passed in to
         * {@link HollowProducer#runCycle(Populator)}:
         *
         *
{@code
         * producer.runCycle(state -> {
         *     sourceOfTruthA = queryA();
         *     for (Record r : sourceOfTruthA) {
         *         Model m = new Model(r);
         *         state.add(m);
         *     }
         *
         *     sourceOfTruthB = queryB();
         *     // ...
         * });
         * }

         *
         *
Notes:
         *
         *

         *
all data for the new state must be added; data from previous cycles is not carried
         * over automatically

         *
caught exceptions that are unrecoverable must be rethrown

         *
the provided {@code WriteState} will be inoperable when this method returns; method
         * calls against it will throw {@code IllegalStateException}

         *
the {@code WriteState} is thread safe

         *

         *
         *

         * Populating asynchronously has these additional requirements:
         *

         *
MUST NOT return from this method until all workers have completed – either normally
         * or exceptionally – or have been cancelled

         *
MUST throw an exception if any worker completed exceptionally. MAY cancel remaining tasks
         * or wait for the remainder to complete.

         *

         *
         * @param newState the new state to add objects to
         * @throws Exception if population fails.  If failure occurs the data from the previous cycle is retained.
         */
        void populate(HollowProducer.WriteState newState) throws Exception;
    }

    /**
     * Representation of new write state.
     */
    public interface WriteState {
        /**
         * Adds the specified POJO to the state engine. See {@link HollowObjectMapper#add(Object)} for details.
         *
         *
Calling this method after the producer's populate stage has completed is an error.
         *
         * @param o the POJO to add
         * @return the ordinal associated with the added POJO
         * @throws IllegalStateException if called after the populate stage has completed (see
         * {@link Populator} for details on the contract)
         */
        int add(Object o) throws IllegalStateException;

        /**
         * For advanced use-cases, access the underlying {@link HollowObjectMapper}. Prefer using {@link #add(Object)}
         * on this class instead.
         *
         *
Calling this method after the producer's populate stage has completed is an error. Exercise caution when
         * saving the returned reference in a local variable that is closed over by an asynchronous task as that
         * circumvents this guard. It is safest to call {@code writeState.getObjectMapper()} within the closure.
         *
         * @return the object mapper
         * @throws IllegalStateException if called after the populate stage has completed (see
         * {@link Populator} for details on the contract)
         */
        HollowObjectMapper getObjectMapper() throws IllegalStateException;

        /**
         * For advanced use-cases, access the underlying {@link HollowWriteStateEngine}. Prefer using
         * {@link #add(Object)} on this class instead.
         *
         *
Calling this method after the producer's populate stage has completed is an error. Exercise caution when
         * saving the returned reference in a local variable that is closed over by an asynchronous task as that
         * circumvents this guard. It is safest to call {@code writeState.getStateEngine()} within the closure.
         *
         * @return the write state engine
         * @throws IllegalStateException if called after the populate stage has completed (see
         * {@link Populator} for details on the contract)
         */
        HollowWriteStateEngine getStateEngine() throws IllegalStateException;

        /**
         * For advanced use-cases, access the ReadState of the prior successful cycle.
         *
         *
Calling this method after the producer's populate stage has completed is an error. Exercise caution when
         * saving the returned reference in a local variable that is closed over by an asynchronous task as that
         * circumvents this guard. It is safest to call {@code writeState.getPriorState()} within the closure.
         *
         * @return the prior read state
         * @throws IllegalStateException if called after the populate stage has completed (see
         * {@link Populator} for details on the contract)
         */
        ReadState getPriorState() throws IllegalStateException;

        /**
         * Returns the version of the current producer cycle being populated.
         *
         *
Calling this method after the producer's populate stage has completed is an error.
         *
         * @return the version of the current producer cycle
         * @throws IllegalStateException if called after the populate stage has completed (see
         * {@link Populator} for details on the contract)
         */
        long getVersion() throws IllegalStateException;
    }

    /**
     * Representation of read state computed from the population of new write state.
     */
    public interface ReadState {
        /**
         * Returns the version of the read state
         *
         * @return the version
         */
        long getVersion();

        /**
         * Returns the read state engine
         *
         * @return the read state engine
         */
        HollowReadStateEngine getStateEngine();
    }

    public interface BlobStager {
        /**
         * Returns a blob with which a {@code HollowProducer} will write a snapshot for the version specified.
         *

         * The producer will pass the returned blob back to this publisher when calling {@link Publisher#publish(HollowProducer.Blob)}.
         *
         * @param version the blob version
         * @return a {@link HollowProducer.Blob} representing a snapshot for the {@code version}
         */
        HollowProducer.Blob openSnapshot(long version);

        /**
         * Returns a blob with which a {@code HollowProducer} will write a forward delta from the version specified to
         * the version specified, i.e. {@code fromVersion => toVersion}.
         *

         * The producer will pass the returned blob back to this publisher when calling {@link Publisher#publish(HollowProducer.Blob)}.
         *

         * In the delta chain {@code fromVersion} is the older version such that {@code fromVersion < toVersion}.
         *
         * @param fromVersion the data state this delta will transition from
         * @param toVersion the data state this delta will transition to
         * @return a {@link HollowProducer.Blob} representing a snapshot for the {@code version}
         */
        HollowProducer.Blob openDelta(long fromVersion, long toVersion);

        /**
         * Returns a blob with which a {@code HollowProducer} will write a reverse delta from the version specified to
         * the version specified, i.e. {@code fromVersion <= toVersion}.
         *

         * The producer will pass the returned blob back to this publisher when calling {@link Publisher#publish(HollowProducer.Blob)}.
         *

         * In the delta chain {@code fromVersion} is the older version such that {@code fromVersion < toVersion}.
         *
         * @param fromVersion version in the delta chain immediately after {@code toVersion}
         * @param toVersion version in the delta chain immediately before {@code fromVersion}
         * @return a {@link HollowProducer.Blob} representing a snapshot for the {@code version}
         */
        HollowProducer.Blob openReverseDelta(long fromVersion, long toVersion);
    }

    public interface BlobCompressor {
        BlobCompressor NO_COMPRESSION = new BlobCompressor() {
            @Override
            public OutputStream compress(OutputStream os) {
                return os;
            }

            @Override
            public InputStream decompress(InputStream is) {
                return is;
            }
        };

        /**
         * This method provides an opportunity to wrap the OutputStream used to write the blob (e.g. with a GZIPOutputStream).
         *
         * @param is the uncompressed output stream
         * @return the compressed output stream
         */
        OutputStream compress(OutputStream is);

        /**
         * This method provides an opportunity to wrap the InputStream used to write the blob (e.g. with a GZIPInputStream).
         *
         * @param is the compressed input stream
         * @return the uncompressed input stream
         */
        InputStream decompress(InputStream is);
    }

    public interface Publisher {

        /**
         * Publish the blob specified to this publisher's blobstore.
         *

         * It is guaranteed that {@code blob} was created by calling one of
         * {@link BlobStager#openSnapshot(long)}, {@link BlobStager#openDelta(long, long)}, or
         * {@link BlobStager#openReverseDelta(long, long)} on this publisher.
         *
         * @param blob the blob to publish
         */
        void publish(HollowProducer.Blob blob);
    }

    public static abstract class Blob {

        protected final long fromVersion;
        protected final long toVersion;
        protected final Blob.Type type;

        protected Blob(long fromVersion, long toVersion, Blob.Type type) {
            this.fromVersion = fromVersion;
            this.toVersion = toVersion;
            this.type = type;
        }

        protected abstract void write(HollowBlobWriter writer) throws IOException;

        public abstract InputStream newInputStream() throws IOException;

        public abstract void cleanup();

        @Deprecated
        public File getFile() {
            throw new UnsupportedOperationException("File is not available");
        }

        public Path getPath() {
            throw new UnsupportedOperationException("Path is not available");
        }

        public Type getType() {
            return this.type;
        }

        public long getFromVersion() {
            return this.fromVersion;
        }

        public long getToVersion() {
            return this.toVersion;
        }

        /**
         * Hollow blob types are {@code SNAPSHOT}, {@code DELTA} and {@code REVERSE_DELTA}.
         */
        public enum Type {
            SNAPSHOT("snapshot"),
            DELTA("delta"),
            REVERSE_DELTA("reversedelta");

            public final String prefix;

            Type(String prefix) {
                this.prefix = prefix;
            }
        }
    }

    public interface Announcer {
        void announce(long stateVersion);
    }

    public static HollowProducer.Builder<?> withPublisher(HollowProducer.Publisher publisher) {
        Builder<?> builder = new Builder<>();
        return builder.withPublisher(publisher);
    }

    @SuppressWarnings("unchecked")
    public static class Builder<B extends HollowProducer.Builder> {
        BlobStager stager;
        BlobCompressor compressor;
        File stagingDir;
        Publisher publisher;
        Announcer announcer;
        List<HollowProducerEventListener> eventListeners = new ArrayList<>();
        VersionMinter versionMinter = new VersionMinterWithCounter();
        Executor snapshotPublishExecutor = null;
        int numStatesBetweenSnapshots = 0;
        long targetMaxTypeShardSize = DEFAULT_TARGET_MAX_TYPE_SHARD_SIZE;
        HollowMetricsCollector<HollowProducerMetrics> metricsCollector;
        BlobStorageCleaner blobStorageCleaner = new DummyBlobStorageCleaner();
        SingleProducerEnforcer singleProducerEnforcer = new BasicSingleProducerEnforcer();
        HollowObjectHashCodeFinder hashCodeFinder = null;

        public B withBlobStager(HollowProducer.BlobStager stager) {
            this.stager = stager;
            return (B) this;
        }

        public B withBlobCompressor(HollowProducer.BlobCompressor compressor) {
            this.compressor = compressor;
            return (B) this;
        }

        public B withBlobStagingDir(File stagingDir) {
            this.stagingDir = stagingDir;
            return (B) this;
        }

        public B withPublisher(HollowProducer.Publisher publisher) {
            this.publisher = publisher;
            return (B) this;
        }

        public B withAnnouncer(HollowProducer.Announcer announcer) {
            this.announcer = announcer;
            return (B) this;
        }

        /**
         * Registers an event listener that will receive events in accordance to the event listener
         * types that are implemented.
         *
         * @param listener the event listener
         * @return this builder
         * @throws IllegalArgumentException if the listener does not implement a recognized event listener type
         */
        public B withListener(HollowProducerEventListener listener) {
            if (!ListenerSupport.isValidListener(listener)) {
                throw new IllegalArgumentException(
                        "Listener does not implement a recognized event listener type: " + listener);
            }
            this.eventListeners.add(listener);
            return (B) this;
        }

        /**
         * Registers one or more event listeners each of which will receive events in accordance to the
         * event listener types that are implemented.
         *
         * @param listeners one or more event listeners
         * @return this builder
         * @throws IllegalArgumentException if the listener does not implement a recognized event listener type
         */
        public B withListeners(HollowProducerEventListener... listeners) {
            for (HollowProducerEventListener listener : listeners) {
                if (!ListenerSupport.isValidListener(listener)) {
                    throw new IllegalArgumentException(
                            "Listener does not implement a recognized event listener type: " + listener);
                }
                this.eventListeners.add(listener);
            }
            return (B) this;
        }

        /**
         * Registers a validator event listener that will receive a validator event
         * during the validator stage.
         *
         * @param validator the validator listener
         * @return this builder
         * @apiNote This method is equivalent to registering the listener with
         * {@link #withListener(HollowProducerEventListener)}.
         * @see #withListener(HollowProducerEventListener)
         */
        public B withValidator(ValidatorListener validator) {
            return withListener(validator);
        }

        /**
         * Register a one or more validator event listeners each of which will receive a validator event
         * during the validator stage.
         *
         * @param validators one or more validator listeners
         * @return this builder
         * @apiNote This method is equivalent to registering the listeners with
         * {@link #withListeners(HollowProducerEventListener...)}.
         * @see #withListeners(HollowProducerEventListener...)
         */
        public B withValidators(ValidatorListener... validators) {
            return withListeners(validators);
        }

        public B withListener(HollowProducerListener listener) {
            return withListener((HollowProducerEventListener) listener);
        }

        public B withListeners(HollowProducerListener... listeners) {
            return withListeners((HollowProducerEventListener[]) listeners);
        }

        public B withVersionMinter(HollowProducer.VersionMinter versionMinter) {
            this.versionMinter = versionMinter;
            return (B) this;
        }

        public B withSnapshotPublishExecutor(Executor executor) {
            this.snapshotPublishExecutor = executor;
            return (B) this;
        }

        public B withNumStatesBetweenSnapshots(int numStatesBetweenSnapshots) {
            this.numStatesBetweenSnapshots = numStatesBetweenSnapshots;
            return (B) this;
        }

        public B withTargetMaxTypeShardSize(long targetMaxTypeShardSize) {
            this.targetMaxTypeShardSize = targetMaxTypeShardSize;
            return (B) this;
        }

        public B withMetricsCollector(HollowMetricsCollector<HollowProducerMetrics> metricsCollector) {
            this.metricsCollector = metricsCollector;
            return (B) this;
        }

        public B withBlobStorageCleaner(BlobStorageCleaner blobStorageCleaner) {
            this.blobStorageCleaner = blobStorageCleaner;
            return (B) this;
        }

        public B withSingleProducerEnforcer(SingleProducerEnforcer singleProducerEnforcer) {
            this.singleProducerEnforcer = singleProducerEnforcer;
            return (B) this;
        }

        public B noSingleProducerEnforcer() {
            this.singleProducerEnforcer = null;
            return (B) this;
        }

        @Deprecated
        public B withHashCodeFinder(HollowObjectHashCodeFinder hashCodeFinder) {
            this.hashCodeFinder = hashCodeFinder;
            return (B) this;
        }

        protected void checkArguments() {
            if (stager != null && compressor != null) {
                throw new IllegalArgumentException(
                        "Both a custom BlobStager and BlobCompressor were specified -- please specify only one of these.");
            }
            if (stager != null && stagingDir != null) {
                throw new IllegalArgumentException(
                        "Both a custom BlobStager and a staging directory were specified -- please specify only one of these.");
            }

            if (this.stager == null) {
                BlobCompressor compressor = this.compressor != null ? this.compressor : BlobCompressor.NO_COMPRESSION;
                File stagingDir = this.stagingDir != null ? this.stagingDir : new File(
                        System.getProperty("java.io.tmpdir"));
                this.stager = new HollowFilesystemBlobStager(stagingDir.toPath(), compressor);
            }
        }

        /**
         * Builds a producer where the complete data is populated.
         *
         * @return a producer.
         */
        public HollowProducer build() {
            checkArguments();
            return new HollowProducer(this);
        }

        /**
         * Builds an incremental producer where changes (additions, modifications, removals) are
         * populated.
         *
         * @return an incremental producer
         */
        public HollowProducer.Incremental buildIncremental() {
            checkArguments();
            return new HollowProducer.Incremental(this);
        }
    }

    /**
     * Provides the opportunity to clean the blob storage.
     * It allows users to implement logic base on Blob Type.
     */
    public static abstract class BlobStorageCleaner {
        public void clean(Blob.Type blobType) {
            switch (blobType) {
                case SNAPSHOT:
                    cleanSnapshots();
                    break;
                case DELTA:
                    cleanDeltas();
                    break;
                case REVERSE_DELTA:
                    cleanReverseDeltas();
                    break;
            }
        }

        /**
         * This method provides an opportunity to remove old snapshots.
         */
        public abstract void cleanSnapshots();

        /**
         * This method provides an opportunity to remove old deltas.
         */
        public abstract void cleanDeltas();

        /**
         * This method provides an opportunity to remove old reverse deltas.
         */
        public abstract void cleanReverseDeltas();
    }

    /**
     * A producer of Hollow data that populates with given data changes (addition, modification or removal), termed
     * incremental production.
     *

     * A {@link HollowProducer} populates with all the given data after which changes are determined.  Generally in
     * other respects {@code HollowProducer} and {@code HollowProducer.Incremental} have the same behaviour when
     * publishing and announcing data states.
     */
    public static class Incremental extends AbstractHollowProducer {
        protected Incremental(Builder<?> b) {
            super(b);
        }

        /**
         * Restores the data state to a desired version.
         *

         * Data model {@link #initializeDataModel(Class[]) initialization} is required prior to
         * restoring the producer.  This ensures that restoration can correctly compare the producer's
         * current data model with the data model of the restored data state and manage any differences
         * in those models (such as not restoring state for any types in the restoring data model not
         * present in the producer's current data model)
         *
         * @param versionDesired the desired version
         * @param blobRetriever the blob retriever
         * @return the read state of the restored state
         * @throws IllegalStateException if the producer's data model has not been initialized
         * @see #initializeDataModel(Class[])
         */
        @Override
        public HollowProducer.ReadState restore(long versionDesired, HollowConsumer.BlobRetriever blobRetriever) {
            return super.hardRestore(versionDesired, blobRetriever);
        }

        /**
         * Runs a cycle to incrementally populate, publish, and announce a new single data state.
         *
         * @param task the incremental populating task to add changes (additions, modifications, or deletions)
         * @return the version identifier of the announced state, otherwise the
         * last successful announced version if 1) there were no data changes compared to that version;
         * or 2) the producer is not the primary producer
         * @throws RuntimeException if the cycle failed
         */
        // @@@ Should this be marked as synchronized?
        public long runIncrementalCycle(Incremental.IncrementalPopulator task) {
            return runCycle(task, null);
        }

        /**
         * Represents a task that incrementally populates a new data state within a {@link HollowProducer} cycle (
         * more specifically within the population stage)
         *
         *
This is a functional interface whose functional method is
         * {@link #populate(IncrementalWriteState)}.
         */
        @FunctionalInterface
        public interface IncrementalPopulator {

            /**
             * Incrementally populates the provided {@link IncrementalWriteState} with new additions, modifications or
             * removals of objects. Often written as a lambda passed in to
             * {@link Incremental#runIncrementalCycle(IncrementalPopulator)}:
             *
             *
{@code
             * producer.runIncrementalCycle(incrementalState -> {
             *     Collection<C> changesA = queryA();
             *     for (Change c : changesA) {
             *         if (c.isAddOrModify() {
             *             incrementalState.addOrModify(c.getObject());
             *         } else {
             *             incrementalState.delete(c.getObject());
             *         }
             *     }
             *
             *     changesB = queryB();
             *     // ...
             * });
             * }

             *
             *
Notes:
             *
             *

             *
The data from the previous cycle is carried over with changes (additions, modifications or removals).
             *
caught exceptions that are unrecoverable must be rethrown

             *
the provided {@code IncrementalWriteState} will be inoperable when this method returns; method
             * calls against it will throw {@code IllegalStateException}

             *
the {@code IncrementalWriteState} is thread safe

             *

             *
             *

             * Incrementally populating asynchronously has these additional requirements:
             *

             *
MUST NOT return from this method until all workers have completed – either normally
             * or exceptionally – or have been cancelled

             *
MUST throw an exception if any worker completed exceptionally. MAY cancel remaining tasks
             * or wait for the remainder to complete.

             *

             *
             * @param state the state to add, modify or delete objects
             * @throws Exception if population fails.  If failure occurs the data from the previous cycle is not changed.
             */
            void populate(IncrementalWriteState state) throws Exception;
        }

        /**
         * Representation of write state that can be incrementally changed.
         */
        public interface IncrementalWriteState {

            /**
             * Adds a new object or modifies an existing object.  The object must define a primary key.
             *

             * Calling this method after the producer's incremental populate stage has completed is an error.
             *
             * @param o the object
             * @throws IllegalArgumentException if the object does not have primary key defined
             */
            void addOrModify(Object o);

            /**
             * Adds a new object.  If the object already exists no action is taken and the object is left unmodified.
             * The object must define a primary key.
             *

             * Calling this method after the producer's incremental populate stage has completed is an error.
             *
             * @param o the object
             * @throws IllegalArgumentException if the object does not have primary key defined
             */
            void addIfAbsent(Object o);

            /**
             * Deletes an object.  The object must define a primary key.
             *

             * This action is ignored if the object does not exist.
             *

             * Calling this method after the producer's incremental populate stage has completed is an error.
             *
             * @param o the object
             * @throws IllegalArgumentException if the object does not have primary key defined
             */
            void delete(Object o);

            /**
             * Deletes an object given its primary key value.
             *

             * If the object does not exist (has already been deleted, say) then this action as no effect on the
             * write state.
             *

             * Calling this method after the producer's incremental populate stage has completed is an error.
             *
             * @param key the primary key value
             * @throws IllegalArgumentException if the field path of the primary key is not resolvable
             */
            void delete(RecordPrimaryKey key);
        }

    }
}


/*
 *  Copyright 2016-2019 Netflix, Inc.
 *
 *     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.netflix.hollow.core.write;

import com.netflix.hollow.core.HollowBlobHeader;
import com.netflix.hollow.core.memory.encoding.VarInt;
import com.netflix.hollow.core.schema.HollowSchema;
import com.netflix.hollow.core.util.SimultaneousExecutor;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;

/**
 * A {@link HollowBlobWriter} is used to serialize snapshot, delta, and reverse delta blobs based on the data state
 * contained in a {@link HollowWriteStateEngine}.
 */
public class HollowBlobWriter {

    private final HollowWriteStateEngine stateEngine;
    private final HollowBlobHeaderWriter headerWriter;

    public HollowBlobWriter(HollowWriteStateEngine stateEngine) {
        this.stateEngine = stateEngine;
        this.headerWriter = new HollowBlobHeaderWriter();
    }

    /**
     * Write the current state as a snapshot blob.
     * @param os the output stream to write the snapshot blob
     * @throws IOException if the snapshot blob could not be written
     */
    public void writeSnapshot(OutputStream os) throws IOException {
        stateEngine.prepareForWrite();

        DataOutputStream dos = new DataOutputStream(os);
        writeHeader(dos, stateEngine.getSchemas(), false);

        VarInt.writeVInt(dos, stateEngine.getOrderedTypeStates().size());

        SimultaneousExecutor executor = new SimultaneousExecutor(getClass(), "write-snapshot");

        for(final HollowTypeWriteState typeState : stateEngine.getOrderedTypeStates()) {
            executor.execute(new Runnable() {
                public void run() {
                    typeState.calculateSnapshot();
                }
            });
        }

        try {
            executor.awaitSuccessfulCompletion();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

        for(HollowTypeWriteState typeState : stateEngine.getOrderedTypeStates()) {
            HollowSchema schema = typeState.getSchema();
            schema.writeTo(dos);

            writeNumShards(dos, typeState.getNumShards());

            typeState.writeSnapshot(dos);
        }
        os.flush();
    }

    /**
     * Serialize the changes necessary to transition a consumer from the previous state
     * to the current state as a delta blob.
     *
     * @param os the output stream to write the delta blob
     * @throws IOException if the delta blob could not be written
     * @throws IllegalStateException if the current state is restored from the previous state
     * and current state contains unrestored state for one or more types.  This indicates those
     * types have not been declared to the producer as part it's initialized data model.
     * @see com.netflix.hollow.api.producer.HollowProducer#initializeDataModel(Class[])
     */
    public void writeDelta(OutputStream os) throws IOException {
        stateEngine.prepareForWrite();

        if(stateEngine.isRestored())
            stateEngine.ensureAllNecessaryStatesRestored();

        List<HollowSchema> changedTypes = changedTypes();

        DataOutputStream dos = new DataOutputStream(os);
        writeHeader(dos, changedTypes, false);

        VarInt.writeVInt(dos, changedTypes.size());

        SimultaneousExecutor executor = new SimultaneousExecutor(getClass(), "write-delta");

        for(final HollowTypeWriteState typeState : stateEngine.getOrderedTypeStates()) {
            executor.execute(new Runnable() {
                public void run() {
                    if(typeState.hasChangedSinceLastCycle())
                        typeState.calculateDelta();
                }
            });
        }

        try {
            executor.awaitSuccessfulCompletion();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

        for(HollowTypeWriteState typeState : stateEngine.getOrderedTypeStates()) {
            if(typeState.hasChangedSinceLastCycle()) {
                HollowSchema schema = typeState.getSchema();
                schema.writeTo(dos);

                writeNumShards(dos, typeState.getNumShards());

                typeState.writeDelta(dos);
            }
        }
        os.flush();
    }

    /**
     * Serialize the changes necessary to transition a consumer from the current state to the
     * previous state as a delta blob.
     *
     * @param os the output stream to write the reverse delta blob
     * @throws IOException if the reverse delta blob could not be written
     * @throws IllegalStateException if the current state is restored from the previous state
     * and current state contains unrestored state for one or more types.  This indicates those
     * types have not been declared to the producer as part it's initialized data model.
     * @see com.netflix.hollow.api.producer.HollowProducer#initializeDataModel(Class[])
     */
    public void writeReverseDelta(OutputStream os) throws IOException {
        stateEngine.prepareForWrite();

        if(stateEngine.isRestored())
            stateEngine.ensureAllNecessaryStatesRestored();

        List<HollowSchema> changedTypes = changedTypes();

        DataOutputStream dos = new DataOutputStream(os);
        writeHeader(dos, changedTypes, true);

        VarInt.writeVInt(dos, changedTypes.size());

        SimultaneousExecutor executor = new SimultaneousExecutor(getClass(), "write-reverse-delta");

        for(final HollowTypeWriteState typeState : stateEngine.getOrderedTypeStates()) {
            executor.execute(new Runnable() {
                public void run() {
                    if(typeState.hasChangedSinceLastCycle())
                        typeState.calculateReverseDelta();
                }
            });
        }

        try {
            executor.awaitSuccessfulCompletion();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

        for(HollowTypeWriteState typeState : stateEngine.getOrderedTypeStates()) {
            if(typeState.hasChangedSinceLastCycle()) {
                HollowSchema schema = typeState.getSchema();
                schema.writeTo(dos);

                writeNumShards(dos, typeState.getNumShards());

                typeState.writeReverseDelta(dos);
            }
        }
        os.flush();
    }

    private List<HollowSchema> changedTypes() {
        List<HollowSchema> changedTypes = new ArrayList<HollowSchema>();

        List<HollowTypeWriteState> orderedTypeStates = stateEngine.getOrderedTypeStates();
        for(int i=0;i<orderedTypeStates.size();i++) {
            HollowTypeWriteState writeState = orderedTypeStates.get(i);
            if(writeState.hasChangedSinceLastCycle())
                changedTypes.add(writeState.getSchema());
        }

        return changedTypes;
    }

    private void writeNumShards(DataOutputStream dos, int numShards) throws IOException {
        VarInt.writeVInt(dos, 1 + VarInt.sizeOfVInt(numShards)); /// pre 2.1.0 forwards compatibility:
                                                                 /// skip new forwards-compatibility and num shards

        VarInt.writeVInt(dos, 0); /// 2.1.0 forwards-compatibility, can write number of bytes for older readers to skip here.

        VarInt.writeVInt(dos, numShards);
    }

    private void writeHeader(DataOutputStream os, List<HollowSchema> schemasToInclude, boolean isReverseDelta) throws IOException {
        HollowBlobHeader header = new HollowBlobHeader();
        header.setHeaderTags(stateEngine.getHeaderTags());
        if(isReverseDelta) {
            header.setOriginRandomizedTag(stateEngine.getNextStateRandomizedTag());
            header.setDestinationRandomizedTag(stateEngine.getPreviousStateRandomizedTag());
        } else {
            header.setOriginRandomizedTag(stateEngine.getPreviousStateRandomizedTag());
            header.setDestinationRandomizedTag(stateEngine.getNextStateRandomizedTag());
        }
        header.setSchemas(schemasToInclude);
        headerWriter.writeHeader(header, os);
    }
}


/*
 *  Copyright 2016-2019 Netflix, Inc.
 *
 *     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.netflix.hollow.api.codegen;

import com.netflix.hollow.api.codegen.objects.HollowCollectionsGenerator;
import com.netflix.hollow.core.HollowDataset;
import com.netflix.hollow.core.schema.HollowListSchema;
import com.netflix.hollow.core.schema.HollowMapSchema;
import com.netflix.hollow.core.schema.HollowSchema;
import com.netflix.hollow.core.schema.HollowSetSchema;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * Not intended for external consumption.
 *
 * @see HollowAPIGenerator
 *
 * @author dsu
 */
public abstract class HollowConsumerJavaFileGenerator implements HollowJavaFileGenerator {
    protected final String packageName;
    protected final String subPackageName;
    protected final CodeGeneratorConfig config;
    protected final HollowDataset dataset;

    protected String className;
    protected boolean useCollectionsImport=false;

    public HollowConsumerJavaFileGenerator(String packageName, String subPackageName, HollowDataset dataset,
            CodeGeneratorConfig config) {
        this.dataset = dataset;
        this.packageName = packageName;
        this.subPackageName = subPackageName;
        this.config = config;
    }

    protected String hollowImplClassname(String typeName) {
        return HollowCodeGenerationUtils.hollowImplClassname(typeName, config.getClassPostfix(),
                config.isUseAggressiveSubstitutions(), config.isUseHollowPrimitiveTypes());
    }

    public String getSubPackageName() {
        return subPackageName;
    }

    @Override
    public final String getClassName() {
        return className;
    }

    public void useCollectionsImport() {
        this.useCollectionsImport=true;
    }

    protected void appendPackageAndCommonImports(StringBuilder builder) {
        appendPackageAndCommonImports(builder, null, new ArrayList<>());
    }

    protected void appendPackageAndCommonImports(StringBuilder builder,
            String apiClassname) {
        appendPackageAndCommonImports(builder, apiClassname, new ArrayList<>());
    }

    protected void appendPackageAndCommonImports(StringBuilder builder,
            String apiClassname, List<HollowSchema> schemasToImport) {
        String fullPackageName =
            createFullPackageName(packageName, subPackageName, config.isUsePackageGrouping());
        if (!isEmpty(fullPackageName)) {
            builder.append("package ").append(fullPackageName).append(";\n\n");

            if (config.isUseHollowPrimitiveTypes()) {
                builder.append("import com.netflix.hollow.core.type.*;\n");
            }

            if (config.isUsePackageGrouping()) {
                if (apiClassname != null) {
                    appendImportFromBasePackage(builder, apiClassname);
                }
                Set<String> schemaNameSet = new HashSet<>();
                for (HollowSchema schema : schemasToImport) {
                    switch (schema.getSchemaType()) {
                        case OBJECT:
                            addToSetIfNotPrimitiveOrCollection(schemaNameSet, schema.getName());
                            break;
                        case SET:
                            addToSetIfNotPrimitiveOrCollection(schemaNameSet,
                                    ((HollowSetSchema) schema).getElementType());
                            break;
                        case LIST:
                            addToSetIfNotPrimitiveOrCollection(schemaNameSet,
                                    ((HollowListSchema) schema).getElementType());
                            break;
                        case MAP:
                            HollowMapSchema mapSchema = (HollowMapSchema) schema;
                            addToSetIfNotPrimitiveOrCollection(schemaNameSet, mapSchema.getKeyType(),
                                    mapSchema.getValueType());
                            break;
                        default:
                            throw new IllegalArgumentException(
                                    "Unexpected HollowSchema to import: " + schema);

                    }
                }
                for (String schemaName : schemaNameSet) {
                    appendImportFromBasePackage(builder, schemaName + config.getClassPostfix());
                }
                appendImportFromBasePackage(builder, "core.*");
                if (useCollectionsImport) {
                    appendImportFromBasePackage(builder, HollowCollectionsGenerator.SUB_PACKAGE_NAME + ".*");
                }
                builder.append("\n");
            }
        }
    }

    private String createFullPackageName(String packageName, String subPackageName, boolean usePackageGrouping) {
        if (usePackageGrouping && !isEmpty(packageName) && !isEmpty(subPackageName)) {
            return packageName + "."  + subPackageName;
        } else {
            return packageName;
        }

    }

    private boolean isEmpty(String value) {
        return value == null || value.trim().isEmpty();
    }

    private void appendImportFromBasePackage(StringBuilder builder, String leaf) {
        builder.append("import ").append(packageName).append(".").append(leaf).append(";\n");
    }

    /**
     * Adds the schema name to the set if the schema name doesn't correspond to a Hollow
     * primitive type. Factored out to prevent bloat in the switch statement it is called
     * from.
     */
    private void addToSetIfNotPrimitiveOrCollection(Set<String> schemaNameSet, String... schemaNames) {
        for (String schemaName : schemaNames) {
            // collections schemas get brought in by a star import
            if (!HollowCodeGenerationUtils.isCollectionType(schemaName, dataset) &&
                    !HollowCodeGenerationUtils.isPrimitiveType(schemaName)) {
                schemaNameSet.add(schemaName);
            }
        }
    }
}


/*
 *  Copyright 2016-2019 Netflix, Inc.
 *
 *     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.netflix.hollow.api.producer;

import com.netflix.hollow.api.producer.HollowProducer.VersionMinter;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.TimeZone;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * A {@link VersionMinter}, which combines a timestamp with a repeating 3-digit sequence number
 *
 * @author Tim Taylor {@literal<tim@toolbear.io>}
 */
public class VersionMinterWithCounter implements HollowProducer.VersionMinter {

    private static AtomicInteger versionCounter = new AtomicInteger();

    /**
     * Create a new state version.

     *
     * State versions should be ascending -- later states have greater versions.

     *
     * Here we use an easily readable timestamp format.
     *
     * @return a new state version
     */
    public long mint() {
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmss");
        dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
        String formattedDate = dateFormat.format(new Date());

        String versionStr = formattedDate + String.format("%03d", versionCounter.incrementAndGet() % 1000);

        return Long.parseLong(versionStr);
    }

}


/*
 *  Copyright 2016-2019 Netflix, Inc.
 *
 *     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.netflix.hollow.api.producer.validation;

import com.netflix.hollow.api.producer.HollowProducer;
import com.netflix.hollow.api.producer.HollowProducer.ReadState;
import com.netflix.hollow.core.index.HollowPrimaryKeyIndex;
import com.netflix.hollow.core.index.key.PrimaryKey;
import com.netflix.hollow.core.read.engine.HollowReadStateEngine;
import com.netflix.hollow.core.read.engine.HollowTypeReadState;
import com.netflix.hollow.core.schema.HollowObjectSchema;
import com.netflix.hollow.core.schema.HollowSchema;
import com.netflix.hollow.core.schema.HollowSchema.SchemaType;
import com.netflix.hollow.core.write.HollowTypeWriteState;
import com.netflix.hollow.core.write.objectmapper.HollowPrimaryKey;
import com.netflix.hollow.core.write.objectmapper.HollowTypeMapper;
import java.util.Arrays;
import java.util.Collection;
import java.util.Objects;
import java.util.stream.Collectors;

/**
 * A validator that fails if two or more records of the same type are present and they have the same primary key.
 *

 * The primary key may be declared on a data model class using the {@link HollowPrimaryKey} annotation, may be
 * declared more directly using {@link PrimaryKey} with {@link HollowObjectSchema}, or declared explicitly when
 * instantiating this validator.
 *
 * @apiNote If all a record's fields are associated with a primary key (in other words, the record has no
 * fields that are not associated the primary key) then any duplicates added during the population
 * stage will be naturally de-duped resulting in just one record present.  Therefore a validator created for such a
 * record's type will never fail.
 */
public class DuplicateDataDetectionValidator implements ValidatorListener {
    private static final String DUPLICATE_KEYS_FOUND_ERRRO_MSG_FORMAT =
            "Duplicate keys found for type %s. Primarykey in schema is %s. "
                    + "Duplicate IDs are: %s";
    private static final String NO_PRIMARY_KEY_ERRRO_MSG_FORMAT =
            "DuplicateDataDetectionValidator defined but unable to find primary key "
                    + "for data type %s. Please check schema definition.";
    private static final String NOT_AN_OBJECT_ERROR_MSGR_FORMAT =
            "DuplicateDataDetectionValidator is defined but schema type of %s "
                    + "is not Object. This validation cannot be done.";

    private static final String FIELD_PATH_NAME = "FieldPaths";
    private static final String DATA_TYPE_NAME = "Typename";

    private static final String NAME = DuplicateDataDetectionValidator.class.getName();

    private final String dataTypeName;
    private final String[] fieldPathNames;

    /**
     * Creates a validator to detect duplicate records of the type that corresponds to the given data type class
     * annotated with {@link HollowPrimaryKey}.
     *
     * @param dataType the data type class
     * @throws IllegalArgumentException if the data type class is not annotated with {@link HollowPrimaryKey}
     */
    public DuplicateDataDetectionValidator(Class<?> dataType) {
        Objects.requireNonNull(dataType);

        if (!dataType.isAnnotationPresent(HollowPrimaryKey.class)) {
            throw new IllegalArgumentException("The data class " +
                    dataType.getName() +
                    " must be annotated with @HollowPrimaryKey");
        }

        this.dataTypeName = HollowTypeMapper.getDefaultTypeName(dataType);
        this.fieldPathNames = null;
    }

    /**
     * Creates a validator to detect duplicate records of the type that corresponds to the given data type name.
     *

     * The validator will fail, when {@link #onValidate validating}, if a schema with a primary key definition does not
     * exist for the given data type name.
     *
     * @param dataTypeName the data type name
     */
    public DuplicateDataDetectionValidator(String dataTypeName) {
        this.dataTypeName = Objects.requireNonNull(dataTypeName);
        this.fieldPathNames = null;
    }

    /**
     * Creates a validator to detect duplicate records of the type that corresponds to the given data type name
     * with a primary key composed from the given field paths.
     *

     * The validator will fail, when {@link #onValidate validating}, if a schema does not exist for the given data
     * type name or any field path is incorrectly specified.
     *
     * @param dataTypeName the data type name
     * @param fieldPathNames the field paths defining the primary key
     */
    public DuplicateDataDetectionValidator(String dataTypeName, String[] fieldPathNames) {
        this.dataTypeName = Objects.requireNonNull(dataTypeName);
        this.fieldPathNames = fieldPathNames.clone();
    }

    @Override
    public String getName() {
        return NAME + "_" + dataTypeName;
    }

    @Override
    public ValidationResult onValidate(ReadState readState) {
        ValidationResult.ValidationResultBuilder vrb = ValidationResult.from(this);
        vrb.detail(DATA_TYPE_NAME, dataTypeName);

        PrimaryKey primaryKey;
        if (fieldPathNames == null) {
            HollowSchema schema = readState.getStateEngine().getSchema(dataTypeName);
            if (schema.getSchemaType() != SchemaType.OBJECT) {
                return vrb.failed(String.format(NOT_AN_OBJECT_ERROR_MSGR_FORMAT, dataTypeName));
            }

            HollowObjectSchema oSchema = (HollowObjectSchema) schema;
            primaryKey = oSchema.getPrimaryKey();
            if (primaryKey == null) {
                return vrb.failed(String.format(NO_PRIMARY_KEY_ERRRO_MSG_FORMAT, dataTypeName));
            }
        } else {
            primaryKey = new PrimaryKey(dataTypeName, fieldPathNames);
        }

        String fieldPaths = Arrays.toString(primaryKey.getFieldPaths());
        vrb.detail(FIELD_PATH_NAME, fieldPaths);

        Collection<Object[]> duplicateKeys = getDuplicateKeys(readState.getStateEngine(), primaryKey);
        if (!duplicateKeys.isEmpty()) {
            String message = String.format(DUPLICATE_KEYS_FOUND_ERRRO_MSG_FORMAT, dataTypeName, fieldPaths,
                    duplicateKeysToString(duplicateKeys));
            return vrb.failed(message);
        }

        return vrb.passed();
    }

    private Collection<Object[]> getDuplicateKeys(HollowReadStateEngine stateEngine, PrimaryKey primaryKey) {
        HollowTypeReadState typeState = stateEngine.getTypeState(dataTypeName);
        HollowPrimaryKeyIndex hollowPrimaryKeyIndex = typeState.getListener(HollowPrimaryKeyIndex.class);
        if (hollowPrimaryKeyIndex == null) {
            hollowPrimaryKeyIndex = new HollowPrimaryKeyIndex(stateEngine, primaryKey);
        }
        return hollowPrimaryKeyIndex.getDuplicateKeys();
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        DuplicateDataDetectionValidator that = (DuplicateDataDetectionValidator) o;
        return dataTypeName.equals(that.dataTypeName) &&
                Arrays.equals(fieldPathNames, that.fieldPathNames);
    }

    @Override
    public int hashCode() {
        int result = Objects.hash(dataTypeName);
        result = 31 * result + Arrays.hashCode(fieldPathNames);
        return result;
    }

    private String duplicateKeysToString(Collection<Object[]> duplicateKeys) {
        return duplicateKeys.stream().map(Arrays::toString).collect(Collectors.joining(","));
    }

    /**
     * Registers {@code DuplicateDataDetectionValidator} validators with the given {@link HollowProducer producer} for
     * all object schema declared with a primary key.
     *

     * This requires that the producer's data model has been initialized
     * (see {@link HollowProducer#initializeDataModel(Class[])} or a prior run cycle has implicitly initialized
     * the data model.
     *

     * For each {@link HollowTypeWriteState write state} that has a {@link HollowObjectSchema object schema}
     * declared with a {@link PrimaryKey primary key} a {@code DuplicateDataDetectionValidator} validator
     * is instantiated, with the primary key type name, and registered with the given producer (if a
     * {@code DuplicateDataDetectionValidator} validator is not already registered for the same primary key type name).
     *
     * @param producer the producer
     * @apiNote This method registers a {@code DuplicateDataDetectionValidator} validator with only the primary key type
     * name and not, in addition, the primary key fields.  This is to ensure, for the common case, duplicate listeners
     * are not registered by this method if listeners with the same type names were explicitly registered when
     * building the producer.
     * @see HollowProducer#initializeDataModel(Class[])
     */
    public static void addValidatorsForSchemaWithPrimaryKey(HollowProducer producer) {
        producer.getWriteEngine().getOrderedTypeStates().stream()
                .filter(ts -> ts.getSchema().getSchemaType() == SchemaType.OBJECT)
                .map(ts -> (HollowObjectSchema) ts.getSchema())
                .filter(hos -> hos.getPrimaryKey() != null)
                .map(HollowObjectSchema::getPrimaryKey)
                .forEach(k -> producer.addListener(new DuplicateDataDetectionValidator(k.getType())));
    }
}


/*
 *  Copyright 2016-2019 Netflix, Inc.
 *
 *     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.netflix.hollow.api.codegen;

import com.netflix.hollow.api.objects.delegate.HollowListCachedDelegate;
import com.netflix.hollow.api.objects.delegate.HollowListDelegate;
import com.netflix.hollow.api.objects.delegate.HollowListLookupDelegate;
import com.netflix.hollow.api.objects.delegate.HollowMapCachedDelegate;
import com.netflix.hollow.api.objects.delegate.HollowMapDelegate;
import com.netflix.hollow.api.objects.delegate.HollowMapLookupDelegate;
import com.netflix.hollow.api.objects.delegate.HollowSetCachedDelegate;
import com.netflix.hollow.api.objects.delegate.HollowSetDelegate;
import com.netflix.hollow.api.objects.delegate.HollowSetLookupDelegate;
import com.netflix.hollow.core.HollowDataset;
import com.netflix.hollow.core.schema.HollowListSchema;
import com.netflix.hollow.core.schema.HollowMapSchema;
import com.netflix.hollow.core.schema.HollowObjectSchema;
import com.netflix.hollow.core.schema.HollowObjectSchema.FieldType;
import com.netflix.hollow.core.schema.HollowSchema;
import com.netflix.hollow.core.schema.HollowSetSchema;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * A class containing convenience methods for the {@link HollowAPIGenerator}.  Not intended for external consumption.
 */
public class HollowCodeGenerationUtils {

    private static final Set<String> PRIMITIVE_TYPES = new HashSet<>();
    private static final Map<String,String> DEFAULT_CLASS_NAME_SUBSTITUTIONS = new HashMap<String,String>();
    private static final Map<String,String> AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS = new HashMap<String,String>();

    static {
        for(Class<?> clzz : Arrays.asList(Boolean.class, Integer.class, Long.class, Float.class, Double.class, String.class)) {
            PRIMITIVE_TYPES.add(clzz.getSimpleName());
        }

        DEFAULT_CLASS_NAME_SUBSTITUTIONS.put("String", "HString");
        DEFAULT_CLASS_NAME_SUBSTITUTIONS.put("Integer", "HInteger");
        DEFAULT_CLASS_NAME_SUBSTITUTIONS.put("Long", "HLong");
        DEFAULT_CLASS_NAME_SUBSTITUTIONS.put("Float", "HFloat");
        DEFAULT_CLASS_NAME_SUBSTITUTIONS.put("Double", "HDouble");
        DEFAULT_CLASS_NAME_SUBSTITUTIONS.put("Boolean", "HBoolean");
        DEFAULT_CLASS_NAME_SUBSTITUTIONS.put("Object", "HObject");

        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("AbstractMethodError", "HAbstractMethodError");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("Appendable", "HAppendable");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("ArithmeticException", "HArithmeticException");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("ArrayIndexOutOfBoundsException", "HArrayIndexOutOfBoundsException");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("ArrayStoreException", "HArrayStoreException");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("AssertionError", "HAssertionError");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("AutoCloseable", "HAutoCloseable");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("Boolean", "HBoolean");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("BootstrapMethodError", "HBootstrapMethodError");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("Byte", "HByte");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("CharSequence", "HCharSequence");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("Character", "HCharacter");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("Class", "HClass");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("ClassCastException", "HClassCastException");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("ClassCircularityError", "HClassCircularityError");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("ClassFormatError", "HClassFormatError");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("ClassLoader", "HClassLoader");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("ClassNotFoundException", "HClassNotFoundException");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("ClassValue", "HClassValue");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("CloneNotSupportedException", "HCloneNotSupportedException");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("Cloneable", "HCloneable");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("Comparable", "HComparable");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("Compiler", "HCompiler");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("Deprecated", "HDeprecated");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("Double", "HDouble");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("Enum", "HEnum");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("EnumConstantNotPresentException", "HEnumConstantNotPresentException");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("Error", "HError");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("Exception", "HException");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("ExceptionInInitializerError", "HExceptionInInitializerError");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("Float", "HFloat");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("IllegalAccessError", "HIllegalAccessError");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("IllegalAccessException", "HIllegalAccessException");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("IllegalArgumentException", "HIllegalArgumentException");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("IllegalMonitorStateException", "HIllegalMonitorStateException");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("IllegalStateException", "HIllegalStateException");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("IllegalThreadStateException", "HIllegalThreadStateException");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("IncompatibleClassChangeError", "HIncompatibleClassChangeError");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("IndexOutOfBoundsException", "HIndexOutOfBoundsException");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("InheritableThreadLocal", "HInheritableThreadLocal");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("InstantiationError", "HInstantiationError");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("InstantiationException", "HInstantiationException");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("Integer", "HInteger");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("InternalError", "HInternalError");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("InterruptedException", "HInterruptedException");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("Iterable", "HIterable");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("LinkageError", "HLinkageError");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("Long", "HLong");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("Math", "HMath");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("NegativeArraySizeException", "HNegativeArraySizeException");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("NoClassDefFoundError", "HNoClassDefFoundError");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("NoSuchFieldError", "HNoSuchFieldError");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("NoSuchFieldException", "HNoSuchFieldException");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("NoSuchMethodError", "HNoSuchMethodError");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("NoSuchMethodException", "HNoSuchMethodException");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("NullPointerException", "HNullPointerException");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("Number", "HNumber");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("NumberFormatException", "HNumberFormatException");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("Object", "HObject");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("OutOfMemoryError", "HOutOfMemoryError");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("Override", "HOverride");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("Package", "HPackage");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("Process", "HProcess");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("ProcessBuilder", "HProcessBuilder");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("Readable", "HReadable");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("ReflectiveOperationException", "HReflectiveOperationException");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("Runnable", "HRunnable");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("Runtime", "HRuntime");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("RuntimeException", "HRuntimeException");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("RuntimePermission", "HRuntimePermission");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("SafeVarargs", "HSafeVarargs");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("SecurityException", "HSecurityException");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("SecurityManager", "HSecurityManager");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("Short", "HShort");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("StackOverflowError", "HStackOverflowError");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("StackTraceElement", "HStackTraceElement");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("StrictMath", "HStrictMath");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("String", "HString");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("StringBuffer", "HStringBuffer");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("StringBuilder", "HStringBuilder");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("StringIndexOutOfBoundsException", "HStringIndexOutOfBoundsException");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("SuppressWarnings", "HSuppressWarnings");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("System", "HSystem");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("Thread", "HThread");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("ThreadDeath", "HThreadDeath");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("ThreadGroup", "HThreadGroup");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("ThreadLocal", "HThreadLocal");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("Throwable", "HThrowable");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("TypeNotPresentException", "HTypeNotPresentException");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("UnknownError", "HUnknownError");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("UnsatisfiedLinkError", "HUnsatisfiedLinkError");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("UnsupportedClassVersionError", "HUnsupportedClassVersionError");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("UnsupportedOperationException", "HUnsupportedOperationException");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("VerifyError", "HVerifyError");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("VirtualMachineError", "HVirtualMachineError");
        AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.put("Void", "HVoid");
    }

    public static String typeAPIClassname(String typeName) {
        return uppercase(typeName) + "TypeAPI";
    }

    public static String hollowFactoryClassname(String typeName) {
        return substituteInvalidChars(uppercase(typeName)) + "HollowFactory";
    }

    public static String hollowObjectProviderName(String typeName) {
        return substituteInvalidChars(lowercase(typeName)) + "Provider";
    }

    public static String hollowImplClassname(String typeName, String classPostfix,
            boolean useAggressiveSubstitutions, boolean useHollowPrimitives) {
        String classname = substituteInvalidChars(uppercase(typeName));
        if (!useHollowPrimitives && !"".equals(classPostfix)) {
            // skip substitutions here to preserve legacy behaviour
            return classname + classPostfix;
        }
        String sub = useAggressiveSubstitutions ?
            AGGRESSIVE_CLASS_NAME_SUBSTITUTIONS.get(classname) :
            DEFAULT_CLASS_NAME_SUBSTITUTIONS.get(classname);
        return sub == null ? classname + classPostfix : sub;
    }

    public static String delegateInterfaceName(String typeName) {
        return substituteInvalidChars(uppercase(typeName)) + "Delegate";
    }

    public static String delegateInterfaceName(HollowSchema schema) {
        if(schema instanceof HollowObjectSchema)
            return delegateInterfaceName(schema.getName());
        if(schema instanceof HollowListSchema)
            return HollowListDelegate.class.getSimpleName();
        if(schema instanceof HollowSetSchema)
            return HollowSetDelegate.class.getSimpleName();
        if(schema instanceof HollowMapSchema)
            return HollowMapDelegate.class.getSimpleName();
        throw new UnsupportedOperationException("What kind of schema is a " + schema.getClass().getSimpleName() + "?");
    }

    public static String delegateCachedImplName(String typeName) {
        return substituteInvalidChars(uppercase(typeName)) + "DelegateCachedImpl";
    }

    public static String delegateCachedClassname(HollowSchema schema) {
        if(schema instanceof HollowObjectSchema)
            return delegateCachedImplName(schema.getName());
        if(schema instanceof HollowListSchema)
            return HollowListCachedDelegate.class.getSimpleName();
        if(schema instanceof HollowSetSchema)
            return HollowSetCachedDelegate.class.getSimpleName();
        if(schema instanceof HollowMapSchema)
            return HollowMapCachedDelegate.class.getSimpleName();
        throw new UnsupportedOperationException("What kind of schema is a " + schema.getClass().getSimpleName() + "?");
    }

    public static String delegateLookupImplName(String typeName) {
        return substituteInvalidChars(uppercase(typeName)) + "DelegateLookupImpl";
    }

    public static String delegateLookupClassname(HollowSchema schema) {
        if(schema instanceof HollowObjectSchema)
            return delegateLookupImplName(schema.getName());
        if(schema instanceof HollowListSchema)
            return HollowListLookupDelegate.class.getSimpleName();
        if(schema instanceof HollowSetSchema)
            return HollowSetLookupDelegate.class.getSimpleName();
        if(schema instanceof HollowMapSchema)
            return HollowMapLookupDelegate.class.getSimpleName();
        throw new UnsupportedOperationException("What kind of schema is a " + schema.getClass().getSimpleName() + "?");
    }

    public static String lowercase(String str) {
        if(str == null || str.length() == 0)
            return str;

        StringBuilder builder = new StringBuilder();

        builder.append(str.substring(0, 1).toLowerCase());
        builder.append(str.substring(1));

        return builder.toString();
    }

    public static String uppercase(String str) {
        return upperFirstChar(str);
    }

    public static String upperFirstChar(String str) {
        if(str == null || str.length() == 0)
            return str;

        StringBuilder builder = new StringBuilder();

        builder.append(str.substring(0, 1).toUpperCase());
        builder.append(str.substring(1));

        return builder.toString();
    }

    public static String substituteInvalidChars(String str) {
        str = str.replace(' ', '_');
        str = str.replace('.', '_');
        return str;
    }

    public static String getJavaBoxedType(FieldType fieldType) {
        switch(fieldType) {
            case BOOLEAN:
                return "Boolean";
            case BYTES:
                return "byte[]";
            case DOUBLE:
                return "Double";
            case FLOAT:
                return "Float";
            case LONG:
                return "Long";
            case INT:
            case REFERENCE:
                return "Integer";
            case STRING:
                return "String";
        }
        throw new IllegalArgumentException("Java boxed type is not known for FieldType." + fieldType.toString());
    }

    public static String getJavaScalarType(FieldType fieldType) {
        switch(fieldType) {
            case BOOLEAN:
                return "boolean";
            case BYTES:
                return "byte[]";
            case DOUBLE:
                return "double";
            case FLOAT:
                return "float";
            case LONG:
                return "long";
            case INT:
            case REFERENCE:
                return "int";
            case STRING:
                return "String";
        }
        throw new IllegalArgumentException("Java scalar type is not known for FieldType." + fieldType.toString());
    }

    private static final Set<String> booleanMethodPrefixes = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
            "is", "has", "do", "should", "was", "contains", "enable", "disable", "get")));
    public static Set<String> getBooleanMethodPrefixes() { return booleanMethodPrefixes; }

    /**
     * Rules: prepend "get" / "is" + upper case first char of field name
     *
     * boolean/Boolean field:
     *    - has a boolean prefix (@see {@link #booleanMethodPrefixes}), just return it; otherwise, prepend "get" + upper case first char
     *
     *      boolean isPrimary - isPrimary()
     *      boolean hasStreams - hasStreams()
     *      boolean playable - getPlayable()
     *      boolean value - getValue()
     *
     * other field type: prepend "get" + upper case first char
     *
     *      String title - getTitle()
     *
     * @param fieldName
     *            name of field
     * @param clazz
     *            type of field
     * @return accessor method name
     */
    public static String generateAccessortMethodName(String fieldName, Class<?> clazz) {
        String prefix = "get";
        if (boolean.class.equals(clazz) || Boolean.class.equals(clazz)) {
            for (String booleanPrefix : booleanMethodPrefixes) {
                if (fieldName.startsWith(booleanPrefix) && fieldName.length() > booleanPrefix.length()) {
                    char firstCharAfterBooleanPrefix = fieldName.charAt(booleanPrefix.length());
                    if (Character.isUpperCase(firstCharAfterBooleanPrefix)) {
                        return fieldName;
                    }
                }
            }
        }

        return substituteInvalidChars(prefix + uppercase(fieldName));
    }

    public static String generateBooleanAccessorMethodName(String fieldName, boolean useBooleanFieldErgonomics) {
        return useBooleanFieldErgonomics ? generateAccessortMethodName(fieldName, boolean.class) : "get" + uppercase(fieldName);
    }

    /**
     * Convert field path into Param name
     *
     * Eg:
     *  - Actor {@literal->} actor
     *  - Actor.name {@literal->} actorName
     *
     * @param fieldPath the field path
     * @return the param name
     */
    public static String normalizeFieldPathToParamName(String fieldPath) {
        String result = null;
        if (fieldPath.contains(".")) {
            String[] parts = fieldPath.split("\\.");
            StringBuilder sb = new StringBuilder();
            sb.append(lowercase(parts[0]));
            for (int i = 1; i < parts.length; i++) {
                sb.append(uppercase(parts[i]));
            }
            result = sb.toString();
        } else {
            result = lowercase(fieldPath);
        }

        if (result.endsWith("!")) {
            return result.substring(0, result.length() - 1);
        }
        return result;
    }

    public static boolean isPrimitiveType(String type) {
        return PRIMITIVE_TYPES.contains(type);
    }

    public static Set<String> getPrimitiveTypes(Collection<HollowSchema> schemaList) {
        Set<String> primitiveTypes = new HashSet<>();
        for (HollowSchema schema : schemaList) {
            String type = schema.getName();
            if (!isPrimitiveType(type)) continue;

            primitiveTypes.add(type);
        }
        return primitiveTypes;
    }

    public static boolean isCollectionType(String schemaName, HollowDataset dataset) {
        return dataset.getSchema(schemaName).getSchemaType() != HollowSchema.SchemaType.OBJECT;
    }
}