package io.axway.iron.core.internal.definition;

import java.util.*;
import io.axway.iron.core.internal.utils.TypeConverter;
import io.axway.iron.error.StoreException;

import static java.util.Map.*;

public class DataTypeManager {
    private static final Set<Class<?>> JDK_DATA_TYPES = Set.of(boolean.class, Boolean.class, //
                                                               byte.class, Byte.class, //
                                                               char.class, Character.class, //
                                                               short.class, Short.class, //
                                                               int.class, Integer.class, //
                                                               long.class, Long.class, //
                                                               float.class, Float.class, //
                                                               double.class, Double.class, //
                                                               String.class, //
                                                               Date.class);

    private static final Map<Class<?>, Class<?>> PRIMITIVES_TYPES_BY_WRAPPER = Map.ofEntries( //
                                                                                entry(Boolean.class, boolean.class), //
                                                                                entry(Byte.class, byte.class), //
                                                                                entry(Character.class, char.class), //
                                                                                entry(Short.class, short.class), //
                                                                                entry(Integer.class, int.class), //
                                                                                entry(Long.class, long.class), //
                                                                                entry(Float.class, float.class), //
                                                                                entry(Double.class, double.class));

    private static final Map<Class<?>, TypeConverter<?>> TYPE_CONVERTERS;

    static {
        Map<Class<?>, TypeConverter<?>> converters = new HashMap<>();
        converters.put(Byte.class, value -> {
            if (value == null || value instanceof Byte) {
                return value;
            } else {
                return ((Number) value).byteValue();
            }
        });
        converters.put(byte.class, converters.get(Byte.class));

        converters.put(Character.class, value -> {
            if (value == null || value instanceof Character) {
                return value;
            } else if (value instanceof String) {
                String s = (String) value;
                if (s.length() == 1) {
                    return s.charAt(0);
                }
            }
            throw new StoreException("Unable to convert type", arguments -> //
                    arguments.add("source", value.getClass().getName()) //
                            .add("target", Character.class.getName()) //
                            .add("value", value));
        });
        converters.put(char.class, converters.get(Character.class));

        converters.put(Short.class, value -> {
            if (value == null || value instanceof Short) {
                return value;
            } else {
                return ((Number) value).shortValue();
            }
        });
        converters.put(short.class, converters.get(Short.class));

        converters.put(Integer.class, value -> {
            if (value == null || value instanceof Integer) {
                return value;
            } else {
                return ((Number) value).intValue();
            }
        });
        converters.put(int.class, converters.get(Integer.class));

        converters.put(Long.class, value -> {
            if (value == null || value instanceof Long) {
                return value;
            } else {
                return ((Number) value).longValue();
            }
        });
        converters.put(long.class, converters.get(Long.class));

        converters.put(Float.class, value -> {
            if (value == null || value instanceof Float) {
                return value;
            } else {
                return ((Number) value).floatValue();
            }
        });
        converters.put(float.class, converters.get(Float.class));

        converters.put(Double.class, value -> {
            if (value == null || value instanceof Double) {
                return value;
            } else {
                return ((Number) value).doubleValue();
            }
        });
        converters.put(double.class, converters.get(Double.class));

        converters.put(Date.class, value -> {
            if (value == null || value instanceof Date) {
                return value;
            } else if (value instanceof Long) {
                return new Date((long) value);
            }
            throw new StoreException("Unable to convert type", arguments -> //
                    arguments.add("source", value.getClass().getName()) //
                            .add("target", Date.class.getName()) //
                            .add("value", value));
        });

        TYPE_CONVERTERS = Map.copyOf(converters);
    }

    public boolean isValidDataType(Class<?> dataTypeClass) {
        return JDK_DATA_TYPES.contains(dataTypeClass);
    }

    public Class<?> getPrimitiveTypeOf(Class<?> clazz) {
        return PRIMITIVES_TYPES_BY_WRAPPER.get(clazz);
    }

    //TODO very ugly fix
    @SuppressWarnings("unchecked")
    public <T> TypeConverter<T> getTypeConverter(Class<T> clazz) {
        TypeConverter<T> converter = (TypeConverter<T>) TYPE_CONVERTERS.get(clazz);
        if (converter == null) {
            converter = value -> (T) value;
        }

        return converter;
    }

    public <T> TypeConverter<Collection<T>> getCollectionTypeConverter(Class<T> clazz) {
        TypeConverter<T> elementConverter = getTypeConverter(clazz);
        return value -> {
            if (value == null) {
                return null;
            }

            Collection<?> params = (Collection) value;
            Collection<T> converted = new ArrayList<>(params.size());
            for (Object param : params) {
                converted.add(elementConverter.convert(param));
            }

            return converted;
        };
    }
}


package io.axway.iron.description;

import java.util.*;
import io.axway.iron.functional.Accessor;

import static io.axway.iron.description.hook.DSLHelper.THREAD_LOCAL_DSL_HELPER;

public final class DSL {

    /**
     * This method has to be called in the default implementation of an entity interface to declare that a relation is the reciprocal of another relation:
     *

     *
the straight relation is defined from the {@code TAIL} entity to the {@code HEAD} entity. It must not have a default implementation

     *
the reverse relation is defined from the {@code HEAD} entity to the {@code TAIL} entity. It must have a default implementation that call this method

     *

     *

     * At runtime this call is only issued during the model analysis. The entity instances proxies overrides this default implementation to return the needed information.

     *
     * @param tailEntityClass the {@code TAIL} entity class
     * @param relationAccessor the accessor on the {@code TAIL} entity class that correspond to the straight relation.
     * @param <TAIL> the TAIL entity
     * @param  the HEAD entity
     * @return whatever is needed to make the compiler happy
     */
    public static <TAIL, HEAD> Collection<TAIL> reciprocalManyRelation(Class<TAIL> tailEntityClass, Accessor<TAIL, HEAD> relationAccessor) {
        return THREAD_LOCAL_DSL_HELPER.get().reciprocalManyRelation(tailEntityClass, relationAccessor);
    }

    private DSL() {
    }
}


package io.axway.iron.core.spi.file;

import java.io.*;
import java.math.BigInteger;
import java.nio.file.FileVisitResult;
import java.nio.file.FileVisitor;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.*;
import java.util.regex.*;
import java.util.stream.*;
import io.axway.iron.error.StoreException;

import static java.nio.file.Files.copy;
import static java.util.regex.Pattern.*;
import static java.util.stream.Collectors.*;

/**
 * Migration script to migrate iron snapshots files from 0.5.0 layout to 0.6.0 layout
 */
public class IronMigration {

    private static final String GLOBAL_DIRECTORY_NAME = "global";
    private static final String SNAPSHOT_DIRECTORY_NAME = "snapshot";
    private static final String SNAPSHOT_SUFFIX = ".snapshot";
    private static final String TX_DIRECTORY_NAME = "tx";
    private static final String TMP_DIRECTORY_NAME = ".tmp";
    private static final int TX_ID_FILENAME_LENGTH = 20;

    public static void main(String[] args) throws Exception {
        try {
            // Check arguments
            if (args.length != 4) {
                throw new IllegalArgumentException("Missing arguments");
            }
            Path originIronPath = Paths.get(args[0]);
            if (!originIronPath.toFile().exists() || !originIronPath.toFile().isDirectory()) {
                throw new IllegalArgumentException("Unknown iron directory : " + args[0]);
            }
            String globalStoreManagerName = args[1];
            String storesStoreManagerName = args[2];
            if (globalStoreManagerName.length() == 0 || storesStoreManagerName.length() == 0) {
                throw new IllegalArgumentException("globalStoreManagerName and storesStoreManagerName must not be empty");
            }
            String targetIronPath = args[3];

            Path globalPath = originIronPath.resolve(GLOBAL_DIRECTORY_NAME).resolve(SNAPSHOT_DIRECTORY_NAME);
            Path targetGlobalPath = Paths.get(targetIronPath).resolve(globalStoreManagerName);
            Path targetStoresPath = Paths.get(targetIronPath).resolve(storesStoreManagerName);

            copyGlobalSnapshots(globalPath, targetGlobalPath);
            copyTenantSnapshots(originIronPath, targetStoresPath);
            completeStoreSnapshotWithMissingInstanceSnapshots(targetStoresPath);

            System.out.printf("Migration done from %s to %s.%n", originIronPath, targetIronPath);
        } catch (Exception e) {
            System.err.println(
                    "Usage of migration tool : java -jar migrationTool.jar uriToIronDirectory globalStoreManagerName storesStoreManagerName uriToTargetDirectory\n"
                            + "\tglobalStoreManagerName and storesStoreManagerName must correspond to the names given in the code of traceability-ui");
            throw e;
        }
    }

    private static void copyTenantSnapshots(Path ironPath, Path targetStoresPath) throws IOException {
        Files.walkFileTree(ironPath, new FileVisitor<>() {
            @Override
            public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
                String name = dir.getFileName().toString();
                if (GLOBAL_DIRECTORY_NAME.equals(name) || TMP_DIRECTORY_NAME.equals(name) || TX_DIRECTORY_NAME.equals(name)) {
                    return FileVisitResult.SKIP_SUBTREE;
                }
                return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                String store = file.getName(file.getNameCount() - 3).toString();
                String tx = file.getFileName().toString().substring(0, TX_ID_FILENAME_LENGTH);
                Path snapshotDir = targetStoresPath.resolve(SNAPSHOT_DIRECTORY_NAME).resolve(tx);
                snapshotDir.toFile().mkdirs();
                copy(file, snapshotDir.resolve(store + SNAPSHOT_SUFFIX));
                return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult visitFileFailed(Path file, IOException exc) {
                throw new StoreException(exc);
            }

            @Override
            public FileVisitResult postVisitDirectory(Path dir, IOException exc) {
                return FileVisitResult.CONTINUE;
            }
        });
    }

    private static void copyGlobalSnapshots(Path globalPath, Path targetGlobalPath) throws IOException {
        try (Stream<Path> pathStream = Files.walk(globalPath)) {
            pathStream                                                    //
                    .map(Path::toFile)                                                //
                    .filter(File::isFile)                                             //
                    .filter(file -> file.getName().endsWith(SNAPSHOT_SUFFIX))             //
                    .forEach(file -> {                                                //
                        String tx = file.getName().substring(0, 20);
                        try {
                            Path snapshotDir = targetGlobalPath.resolve(SNAPSHOT_DIRECTORY_NAME).resolve(tx);
                            snapshotDir.toFile().mkdirs();
                            copy(file.toPath(), snapshotDir.resolve(GLOBAL_DIRECTORY_NAME + SNAPSHOT_SUFFIX));
                        } catch (IOException e) {
                            throw new UncheckedIOException(e);
                        }
                    });
        }
    }

    /**
     * Complete snapshot N with lacking instance snapshots from snapshot N - 1
     *
     * @param targetStoresPath path to store target iron
     */
    private static void completeStoreSnapshotWithMissingInstanceSnapshots(Path targetStoresPath) {
        String transactionIdRegexAlone = "\"transactionId\"\\s*:\\s*\\d+\\s*,";
        String transactionIdRegexReplace = "(.*\"transactionId\"\\s*:\\s*)\\d+(\\s*,.*)";
        Pattern transactionIdPattern = compile(transactionIdRegexAlone);
        Set<File> previousSnapshots = new HashSet<>();

        Arrays.stream(targetStoresPath.resolve(SNAPSHOT_DIRECTORY_NAME).toFile().listFiles()).
                sorted().
                forEach(snapshot -> {
                    Set<String> snapshotNames = Arrays.stream(snapshot.listFiles()).map(File::getName).collect(toSet());
                    previousSnapshots.stream().
                            filter(previousSnapshot -> !snapshotNames.contains(previousSnapshot.getName())).
                            forEach(previousSnapshot -> {
                                try {
                                    Path targetPath = snapshot.toPath().resolve(previousSnapshot.getName());
                                    Path sourcePath = previousSnapshot.toPath();
                                    long count = countTransactionId(transactionIdPattern, sourcePath);
                                    if (count != 1L) {
                                        throw new StoreException("transactionId not found once", args -> args.add("found count", count));
                                    }
                                    BigInteger newTransactionId = new BigInteger(snapshot.getName());
                                    replaceTransactionIdValue(transactionIdRegexReplace, sourcePath, targetPath, newTransactionId.toString());
                                } catch (IOException e) {
                                    throw new UncheckedIOException(e);
                                }
                            });
                    previousSnapshots.clear();
                    previousSnapshots.addAll(Arrays.stream(snapshot.listFiles()).collect(toSet()));
                });
    }

    private static long countTransactionId(Pattern transactionIdPattern, Path targetPath) throws IOException {
        try (Stream<String> lines = Files.lines(targetPath)) {
            return lines.mapToLong(line -> transactionIdPattern.matcher(line).results().count()).sum();
        }
    }

    private static void replaceTransactionIdValue(String transactionIdRegex, Path sourcePath, Path targetPath, String newTransactionId) throws IOException {
        try (Stream<String> lines = Files.lines(sourcePath)) {
            Files.write(targetPath, lines.map(line -> line.replaceAll(transactionIdRegex, "$1" + newTransactionId + "$2")).collect(toList()));
        }
    }
}


package io.axway.iron.spi.aws.kinesis;

import java.io.*;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.util.*;
import java.util.concurrent.atomic.*;
import javax.annotation.*;
import org.reactivestreams.Publisher;
import com.amazonaws.services.kinesis.AmazonKinesis;
import com.amazonaws.services.kinesis.model.DescribeStreamRequest;
import com.amazonaws.services.kinesis.model.DescribeStreamResult;
import com.amazonaws.services.kinesis.model.GetRecordsRequest;
import com.amazonaws.services.kinesis.model.GetRecordsResult;
import com.amazonaws.services.kinesis.model.GetShardIteratorRequest;
import com.amazonaws.services.kinesis.model.GetShardIteratorResult;
import com.amazonaws.services.kinesis.model.PutRecordRequest;
import com.amazonaws.services.kinesis.model.Record;
import com.amazonaws.services.kinesis.model.Shard;
import com.amazonaws.services.kinesis.model.ShardIteratorType;
import io.axway.alf.log.Logger;
import io.axway.alf.log.LoggerFactory;
import io.axway.iron.spi.StoreNamePrefixManagement;
import io.axway.iron.spi.storage.TransactionStore;
import io.reactivex.Flowable;
import io.reactivex.schedulers.Schedulers;

import static io.axway.alf.assertion.Assertion.checkState;
import static io.axway.iron.spi.StoreNamePrefixManagement.readStoreName;
import static io.axway.iron.spi.aws.AwsUtils.*;
import static io.axway.iron.spi.aws.kinesis.AwsKinesisUtils.*;
import static java.math.BigInteger.ZERO;

/**
 * Kinesis transaction store factory to build Kinesis TransactionStore.
 */
public class AwsKinesisTransactionStore implements TransactionStore {
    private static final Logger LOG = LoggerFactory.getLogger(AwsKinesisTransactionStore.class);

    private static final long INITIAL_MINIMAL_DURATION_BETWEEN_TWO_GET_SHARD_ITERATOR_REQUESTS = 1_000 / 4; // max 5 calls per second
    private static final String USELESS_PARTITION_KEY = "uselessPartitionKey";

    private final String m_streamName;
    private final Shard m_shard;
    private final AmazonKinesis m_kinesis;
    private BigInteger m_seekTransactionId = null;
    private String m_shardIterator;
    @Nullable
    private Long m_lastGetShardIteratorRequestTime = null;

    private final Flowable<TransactionInput> m_transactionsFlow;
    private final StoreNamePrefixManagement m_prefixManagement = new StoreNamePrefixManagement();

    private AtomicLong m_durationBetweenRequests = new AtomicLong(INITIAL_MINIMAL_DURATION_BETWEEN_TWO_GET_SHARD_ITERATOR_REQUESTS);

    /**
     * Create a KinesisTransactionStoreFactory with some properties set to configure Kinesis :
     *
     * @param accessKey aws access key (optional+)
     * @param secretKey aws secret key (optional+)
     * @param endpoint kinesis port (optional*)
     * @param port kinesis endpoint (optional*)
     * @param region aws region (optional*)
     * @param streamName stream name prefix
     * (+) to configure the access, both access key and secret key must be provided.
     * (*) to configure the endpoint URL, the endpoint, the port and the region must be provided.
     */
    AwsKinesisTransactionStore(String accessKey, String secretKey, String endpoint, Integer port, String region, String streamName) {
        m_kinesis = buildKinesisClient(accessKey, secretKey, endpoint, port, region);
        m_streamName = streamName;
        ensureStreamExists(m_kinesis, m_streamName);
        m_shard = getUniqueShard();

        GetShardIteratorRequest getShardIteratorRequest = new GetShardIteratorRequest().withStreamName(m_streamName)//
                .withShardIteratorType(ShardIteratorType.TRIM_HORIZON)//
                .withShardId(m_shard.getShardId());
        GetShardIteratorResult getShardIteratorResult = performAmazonActionWithRetry("describe stream " + m_streamName,
                                                                                () -> m_kinesis.getShardIterator(getShardIteratorRequest),
                                                                                DEFAULT_RETRY_COUNT, DEFAULT_RETRY_COUNT);
        m_shardIterator = getShardIteratorResult.getShardIterator();

        m_transactionsFlow = Flowable                                                //
                .<List<Record>>generate(emitter -> emitter.onNext(nextRecords()))    //                                                                   //
                .subscribeOn(Schedulers.io())                                        //
                .observeOn(Schedulers.computation())                                 //
                .concatMap(Flowable::fromIterable)                                   //
                .map(record -> {
                    BigInteger seekTransactionId = new BigInteger(record.getSequenceNumber());
                    m_seekTransactionId = seekTransactionId;
                    ByteBuffer data = record.getData().asReadOnlyBuffer();
                    ByteBufferBackedInputStream inputStream = new ByteBufferBackedInputStream(data);
                    String storeName = readStoreName(inputStream);
                    return new TransactionInput() {
                        @Override
                        public String storeName() {
                            return storeName;
                        }

                        @Override
                        public InputStream getInputStream() {
                            return inputStream;
                        }

                        @Override
                        public BigInteger getTransactionId() {
                            return seekTransactionId;
                        }
                    };
                });
    }

    @Override
    public OutputStream createTransactionOutput(String storeName) throws IOException {
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream() {
            @Override
            public void close() throws IOException {
                super.close();
                ByteBuffer data = ByteBuffer.wrap(toByteArray());
                performAmazonActionWithRetry("describe stream " + m_streamName, () -> {
                    m_kinesis.putRecord(new PutRecordRequest().withStreamName(m_streamName).withData(data).withPartitionKey(USELESS_PARTITION_KEY));
                    return null;
                }, DEFAULT_RETRY_COUNT, DEFAULT_RETRY_DURATION_IN_MILLIS);
            }
        };
        m_prefixManagement.writeNamePrefix(storeName, outputStream);
        return outputStream;
    }

    @Override
    public Publisher<TransactionInput> allTransactions() {
        return m_transactionsFlow;
    }

    @Override
    public void seekTransaction(BigInteger latestProcessedTransactionId) {
        m_seekTransactionId = latestProcessedTransactionId;
        if (!m_seekTransactionId.equals(ZERO)) { // the snapshot is not a special migration snapshot, some transactions have already been processed
            GetShardIteratorRequest getShardIteratorRequest = new GetShardIteratorRequest().withStreamName(m_streamName)//
                    .withShardIteratorType(ShardIteratorType.AFTER_SEQUENCE_NUMBER)//
                    .withShardId(m_shard.getShardId())//
                    .withStartingSequenceNumber(m_seekTransactionId.toString());
            GetShardIteratorResult getShardIteratorResult = performAmazonActionWithRetry("describe stream " + m_streamName,
                                                                                () -> m_kinesis.getShardIterator(getShardIteratorRequest),
                                                                                DEFAULT_RETRY_COUNT, DEFAULT_RETRY_DURATION_IN_MILLIS);
            m_shardIterator = getShardIteratorResult.getShardIterator();
        }
    }

    @Override
    public void close() {
        m_kinesis.shutdown();
    }

    /**
     * Returns the single created shard (stream has a single shard).
     */
    private Shard getUniqueShard() {
        DescribeStreamRequest describeStreamRequest = new DescribeStreamRequest().withStreamName(m_streamName).withLimit(1);
        DescribeStreamResult describeStreamResult = performAmazonActionWithRetry("describe stream " + m_streamName,
                                                                                () -> m_kinesis.describeStream(describeStreamRequest), DEFAULT_RETRY_COUNT,
                                                                                DEFAULT_RETRY_DURATION_IN_MILLIS);
        String streamStatus = describeStreamResult.getStreamDescription().getStreamStatus();
        if (streamStatus == null || !streamStatus.equals(ACTIVE_STREAM_STATUS)) {
            throw new AwsKinesisException("Stream does not exist", args -> args.add("streamName", m_streamName));
        }
        List<Shard> shards = describeStreamResult.getStreamDescription().getShards();
        checkState(shards.size() == 1, "Kinesis Stream should contain only one shard",
                   args -> args.add("streamName", m_streamName).add("shardCount", shards.size()));
        return shards.get(0);
    }

    @Nullable
    private List<Record> nextRecords() {
        if (!waitTheMinimalDurationToExecuteTheNextProvisioningRequest()) {
            return List.of();
        }
        // Suboptimal request, should store records in a list instead. To be fixed.
        GetRecordsRequest getRecordsRequest = new GetRecordsRequest().withShardIterator(m_shardIterator).withLimit(1);
        return getRecords(getRecordsRequest);
    }

    /**
     * Wait that the minimum duration between two GetShardIteratorRequests has elapsed.
     *
     * @return true if the duration elapsed, false if the thread has been interrupted
     */
    private boolean waitTheMinimalDurationToExecuteTheNextProvisioningRequest() {
        if (m_lastGetShardIteratorRequestTime != null) {
            long delay = m_durationBetweenRequests.get() - (System.currentTimeMillis() - m_lastGetShardIteratorRequestTime);
            if (delay > 0) {
                try {
                    Thread.sleep(delay);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    return false;
                }
            }
        }
        m_lastGetShardIteratorRequestTime = System.currentTimeMillis();
        return true;
    }

    /**
     * Retrieves records corresponding to the request.
     *
     * @param getRecordsRequest the request
     * @return records corresponding to the request
     */
    @Nullable
    private List<Record> getRecords(GetRecordsRequest getRecordsRequest) {
        return tryAmazonAction("", () -> {
            GetRecordsResult getRecordsResult = m_kinesis.getRecords(getRecordsRequest);
            m_shardIterator = getRecordsResult.getNextShardIterator();
            List<Record> records = getRecordsResult.getRecords();
            LOG.trace("Get records", args -> args.add("streamName", m_streamName).add("record number", records.size())
                    .add("millisBehindLatest", getRecordsResult.getMillisBehindLatest()));
            return records;
        }, m_durationBetweenRequests).orElse(List.of());
    }

    /**
     * Simple {@link InputStream} implementation that exposes currently available content of a {@link ByteBuffer}.
     */
    private class ByteBufferBackedInputStream extends InputStream {
        private final ByteBuffer m_byteBuffer;

        private ByteBufferBackedInputStream(ByteBuffer byteBuffer) {
            m_byteBuffer = byteBuffer;
        }

        @Override
        public int available() {
            return m_byteBuffer.remaining();
        }

        @Override
        public int read() {
            return m_byteBuffer.hasRemaining() ? m_byteBuffer.get() & 255 : -1;
        }

        @Override
        public int read(byte[] bytes, int off, int len) {
            if (!m_byteBuffer.hasRemaining()) {
                return -1;
            }
            int newLen = Math.min(len, m_byteBuffer.remaining());
            m_byteBuffer.get(bytes, off, newLen);
            return newLen;
        }
    }
}


package io.axway.iron.spi.aws;

import java.util.*;
import java.util.concurrent.atomic.*;
import java.util.function.*;
import javax.annotation.*;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.client.builder.AwsClientBuilder;
import com.amazonaws.services.kinesis.model.KMSThrottlingException;
import com.amazonaws.services.kinesis.model.LimitExceededException;
import com.amazonaws.services.kinesis.model.ProvisionedThroughputExceededException;
import io.axway.alf.log.Logger;
import io.axway.alf.log.LoggerFactory;

import static java.util.Optional.*;

public final class AwsUtils {

    private static final Logger LOG = LoggerFactory.getLogger(AwsUtils.class);

    private static final Random RANDOM = new Random();

    public static void setAws(AwsClientBuilder builder, //
                              @Nullable String accessKey, @Nullable String secretKey, //
                              @Nullable String endpoint, @Nullable Integer port, @Nullable String region) {
        if (accessKey != null && secretKey != null) {
            builder.setCredentials(new AWSStaticCredentialsProvider(new BasicAWSCredentials(accessKey, secretKey)));
        }
        if (region != null) {
            if (endpoint != null && port != null) {
                String s3EndpointFull = "https://" + endpoint + ":" + port;
                builder.setEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration(s3EndpointFull, region));
            } else {
                builder.setRegion(region);
            }
        }
    }

    /**
     * Handle retry for amazon quotas
     *
     * @param <T> the action return type
     * @param actionLabel action label used for logging purpose only
     * @param action the action to retry
     * @param retryLimit retry number limit
     * @param durationInMillis duration between each retry
     * @return the action result
     * @throws AwsException after retry exhausted
     */
    public static <T> T performAmazonActionWithRetry(String actionLabel, Supplier<T> action, int retryLimit, int durationInMillis) {
        int retryCount = 0;
        do {
            try {
                return action.get();
            } catch (LimitExceededException | ProvisionedThroughputExceededException | KMSThrottlingException e) {
                // We should just wait a little time before trying again
                int remainingRetries = retryLimit - retryCount;
                LOG.debug("Amazon exception caught",
                          args -> args.add("exception", e.getClass().getName()).add("action", actionLabel).add("remainingRetryCount", remainingRetries));
            }
            sleepUntilInterrupted(actionLabel, durationInMillis);
        } while (retryCount++ < retryLimit);
        throw new AwsException("Limit exceeded, all retries failed", args -> args.add("action", actionLabel).add("retryLimit", retryLimit));
    }

    /**
     * Try to perform an Amazon action and increase the duration between requests if some exception is exceeding resource usage exception is thrown.
     *
     * @param <T> the action return type
     * @param actionLabel action label used for logging purpose only
     * @param action the action to retry
     * @param durationBetweenRequests duration between each retry
     * @return the action result
     */
    public static <T> Optional<T> tryAmazonAction(String actionLabel, Supplier<T> action, AtomicLong durationBetweenRequests) {
        try {
            return of(action.get());
        } catch (LimitExceededException | ProvisionedThroughputExceededException | KMSThrottlingException e) {
            int durationRandomModifier = 1 + RANDOM.nextInt(64);// random duration to make readers out of sync, avoiding simultaneous readings
            long updatedDuration = durationBetweenRequests.updateAndGet(duration -> duration * 2 // twice the duration
                    + duration * 2 / durationRandomModifier);// add random duration to avoid simultaneous reads
            LOG.debug("Update of minimal duration between two get shard iterator requests",
                      args -> args.add("actionLabel", actionLabel).add("new minimalDurationBetweenTwoGetShardIteratorRequests", updatedDuration));
        }
        return empty();
    }

    public static void sleepUntilInterrupted(String actionLabel, int durationInMillis) {
        try {
            LOG.debug("Throttling", args -> args.add("action", actionLabel).add("durationMs", durationInMillis));
            Thread.sleep(durationInMillis);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new AwsException("Thread interrupted while performing action", args -> args.add("action", actionLabel), e);
        }
    }

    private AwsUtils() {
        // utility class
    }
}


package io.axway.iron.spi.aws.s3;

import javax.annotation.*;
import com.amazonaws.AmazonServiceException;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import com.amazonaws.services.s3.model.HeadBucketRequest;

import static io.axway.iron.spi.aws.AwsUtils.setAws;

public final class AwsS3Utils {

    /**
     * Create a AwsS3SnapshotStore with some properties set to configure S3 client:
     * (+) to configure the access, both access key and secret key must be provided.
     * (*) to configure the endpoint URL, the endpoint, the port and the region must be provided.
     *
     * @param accessKey aws access key (optional+)
     * @param secretKey aws secret key (optional+)
     * @param endpoint s3 endpoint (optional*)
     * @param port s3 port (optional*)
     * @param region aws region (optional*)
     * @return the s3 client
     */
    public static AmazonS3 buildS3Client(@Nullable String accessKey, @Nullable String secretKey, //
                                         @Nullable String endpoint, @Nullable Integer port, @Nullable String region) {
        AmazonS3ClientBuilder builder = AmazonS3ClientBuilder.standard();
        setAws(builder, accessKey, secretKey, endpoint, port, region);
        return builder.build();
    }

    /**
     * Throws an exception if the bucket does not exist or is not readable.
     *
     * @param amazonS3 Amazon S3 client
     * @param bucketName bucket name
     */
    static String checkBucketIsAccessible(AmazonS3 amazonS3, String bucketName) {
        HeadBucketRequest headBucketRequest = new HeadBucketRequest(bucketName);
        try {
            amazonS3.headBucket(headBucketRequest);
        } catch (AmazonServiceException e) {
            throw new AwsS3Exception("Bucket is not accessible", args -> args.add("bucketName", bucketName), e);
        }
        return bucketName;
    }

    private AwsS3Utils() {
        // utility class
    }
}


package io.axway.iron.core.internal.utils;

import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.*;
import java.util.concurrent.*;
import io.axway.iron.core.internal.utils.proxy.ProxyFactoryBuilder;
import io.axway.iron.error.StoreException;
import io.axway.iron.functional.Accessor;

import static io.axway.alf.assertion.Assertion.checkState;
import static java.util.Map.*;

public class IntrospectionHelper {
    private static final Map<Class<?>, Object> DEFAULT_VALUES = Map.ofEntries( //
                                                                               entry(boolean.class, Boolean.FALSE), //
                                                                               entry(byte.class, (byte) 0), //
                                                                               entry(char.class, '\0'), //
                                                                               entry(short.class, (short) 0), //
                                                                               entry(int.class, 0), //
                                                                               entry(long.class, 0L), //
                                                                               entry(float.class, 0f), //
                                                                               entry(double.class, 0d));

    private final ConcurrentMap<Accessor<?, ?>, String> m_methodReferenceNames = new ConcurrentHashMap<>();

    public Class<?> getParametrizedClass(Type genericReturnType, int index) {
        if (genericReturnType instanceof ParameterizedType) {
            Type[] actualTypeArguments = ((ParameterizedType) genericReturnType).getActualTypeArguments();
            if (actualTypeArguments.length > index) {
                Type typeArg = actualTypeArguments[index];
                if (typeArg instanceof Class) {
                    return (Class<?>) typeArg;
                }
            }
        }
        throw new StoreException("Unable to retrieve the parametrized class", args -> args.add("type", genericReturnType).add("index", index));
    }

    public <T> String getMethodName(Class<T> clazz, Accessor<T, ?> accessor) {
        return m_methodReferenceNames.computeIfAbsent(accessor, ignored -> retrieveMethod(clazz, accessor).getName());
    }

    /**
     * This method don't use cache. Use with care.<br>
     * If caching is required, then use {@link #getMethodName(Class, Accessor)}
     *
     * @param clazz the class where the method is defined
     * @param accessor the method reference
     * @param <T> the type of the class
     * @return the referenced method
     */
    public static <T> Method retrieveMethod(Class<T> clazz, Accessor<T, ?> accessor) {
        MethodCallRecorder methodCallRecorder = new MethodCallRecorder();

        T accessorInstance = ProxyFactoryBuilder.<MethodCallRecorder>newProxyFactoryBuilder() //
                .defaultObjectMethods() //
                .unhandled((context, proxy, method, args) -> {
                    context.setMethod(method);
                    return DEFAULT_VALUES.get(method.getReturnType());
                }) //
                .build(clazz).createProxy(methodCallRecorder);

        accessor.get(accessorInstance);
        Method method = methodCallRecorder.getMethod();
        if (method == null) {
            throw new StoreException("Method reference didn't call any method on the class", args -> args.add("className", clazz.getName()));
        }
        return method;
    }

    private static final class MethodCallRecorder {
        private Method m_method;

        void setMethod(Method method) {
            checkState(m_method == null, "Method has been already set");
            m_method = method;
        }

        Method getMethod() {
            return m_method;
        }
    }
}


package io.axway.iron.core.internal;

import java.io.*;
import java.math.BigInteger;
import java.util.*;
import java.util.function.*;
import java.util.stream.*;
import javax.annotation.*;
import io.axway.alf.log.Logger;
import io.axway.alf.log.LoggerFactory;
import io.axway.iron.Command;
import io.axway.iron.core.internal.command.CommandProxyFactory;
import io.axway.iron.core.internal.definition.command.CommandDefinition;
import io.axway.iron.core.internal.entity.EntityStore;
import io.axway.iron.core.internal.entity.EntityStores;
import io.axway.iron.error.StoreException;
import io.axway.iron.error.UnrecoverableStoreException;
import io.axway.iron.spi.model.snapshot.SerializableSnapshot;
import io.axway.iron.spi.model.transaction.SerializableCommand;
import io.axway.iron.spi.model.transaction.SerializableTransaction;
import io.axway.iron.spi.serializer.SnapshotSerializer;
import io.axway.iron.spi.serializer.TransactionSerializer;
import io.axway.iron.spi.storage.SnapshotStore;
import io.axway.iron.spi.storage.TransactionStore;
import io.reactivex.Flowable;

import static io.axway.alf.assertion.Assertion.checkArgument;
import static io.axway.iron.spi.model.snapshot.SerializableSnapshot.SNAPSHOT_MODEL_VERSION;
import static io.axway.iron.spi.model.transaction.SerializableTransaction.TRANSACTION_MODEL_VERSION;

class StorePersistence {
    private static final Logger LOG = LoggerFactory.getLogger(StorePersistence.class);

    private final CommandProxyFactory m_commandProxyFactory;
    private final TransactionStore m_transactionStore;
    private final TransactionSerializer m_transactionSerializer;
    private final SnapshotStore m_snapshotStore;
    private final SnapshotSerializer m_snapshotSerializer;
    private final Map<String, CommandDefinition<? extends Command<?>>> m_commandDefinitions;

    private long m_applicationModelVersion = 0;

    StorePersistence(CommandProxyFactory commandProxyFactory, TransactionStore transactionStore, TransactionSerializer transactionSerializer,
                     SnapshotStore snapshotStore, SnapshotSerializer snapshotSerializer,
                     Collection<CommandDefinition<? extends Command<?>>> commandDefinitions) {
        m_commandProxyFactory = commandProxyFactory;
        m_transactionStore = transactionStore;
        m_transactionSerializer = transactionSerializer;
        m_snapshotStore = snapshotStore;
        m_snapshotSerializer = snapshotSerializer;
        m_commandDefinitions = commandDefinitions.stream().
                collect(Collectors.toUnmodifiableMap( //
                                                      commandDefinition -> commandDefinition.getCommandClass().getName(),
                                                      commandDefinition -> commandDefinition));
    }

    void persistSnapshot(BigInteger txId, String storeName, Collection<EntityStore<?>> entityStores) {
        SerializableSnapshot serializableSnapshot = new SerializableSnapshot();
        serializableSnapshot.setSnapshotModelVersion(SNAPSHOT_MODEL_VERSION);
        serializableSnapshot.setApplicationModelVersion(m_applicationModelVersion);
        serializableSnapshot.setTransactionId(txId);
        serializableSnapshot.setEntities(entityStores.stream().map(EntityStore::snapshot).collect(Collectors.toList()));

        try (OutputStream out = m_snapshotStore.createSnapshotWriter(storeName, txId)) {
            m_snapshotSerializer.serializeSnapshot(out, serializableSnapshot);
        } catch (IOException e) {
            throw new StoreException("Error when creating the store snapshot", args -> args.add("transactionId", txId), e);
        }
    }

    /**
     * Load the stores.
     *
     * @param entityStoresByStoreName the map store name to entity stores
     * @return latestSnapshotTxId the transaction id of the last snapshot
     */
    Optional<BigInteger> loadStores(Function<String, EntityStores> entityStoresByStoreName,
                                    BiFunction<SerializableSnapshot, String, SerializableSnapshot> snapshotPostProcessor) {
        Optional<BigInteger> latestSnapshotTxId;
        try {
            latestSnapshotTxId = m_snapshotStore.listSnapshots().stream().max(BigInteger::compareTo);
        } catch (IOException e) {
            throw new UnrecoverableStoreException("Error occurred when recovering from latest snapshot", e);
        }

        latestSnapshotTxId.ifPresent(lastTx -> {
            LOG.info("Recovering store from snapshot", args -> args.add("transactionId", lastTx));
            var postProcess = new SnapshotPostProcessor(snapshotPostProcessor);
            try {
                Flowable.fromPublisher(m_snapshotStore.createSnapshotReader(lastTx))  //
                        .blockingForEach(reader -> {
                            String storeName = reader.storeName();
                            EntityStores entityStores = entityStoresByStoreName.apply(storeName);

                            SerializableSnapshot serializableSnapshot;
                            try (InputStream is = reader.inputStream()) {
                                serializableSnapshot = m_snapshotSerializer.deserializeSnapshot(storeName, is);
                            }
                            if (serializableSnapshot.getSnapshotModelVersion() != SNAPSHOT_MODEL_VERSION) {
                                throw new UnrecoverableStoreException("Snapshot serializable model version is not supported",
                                                                      args -> args.add("version", serializableSnapshot.getSnapshotModelVersion())
                                                                              .add("expectedVersion", SNAPSHOT_MODEL_VERSION));
                            }

                            if (!lastTx.equals(serializableSnapshot.getTransactionId())) {
                                throw new UnrecoverableStoreException("Snapshot transaction id  mismatch with request transaction id",
                                                                      args -> args.add("snapshotTransactionId", serializableSnapshot.getTransactionId())
                                                                              .add("requestTransactionId", lastTx));
                            }

                            SerializableSnapshot finalSnapshot = postProcess.apply(storeName, serializableSnapshot);

                            finalSnapshot.getEntities().forEach(serializableEntityInstances -> {
                                String entityName = serializableEntityInstances.getEntityName();
                                EntityStore<?> entityStore = entityStores.getEntityStore(entityName);
                                checkArgument(entityStore != null, "Entity has not be registered in the store", args -> args.add("entityName", entityName));

                                entityStore.recover(serializableEntityInstances);
                            });
                        });
            } catch (Exception e) {
                throw new UnrecoverableStoreException("Error occurred when recovering from latest snapshot", e);
            }

            // update the applicationModelVersion if any consistent load/update
            m_applicationModelVersion = postProcess.getConsistentApplicationModelVersion();
        });

        if (!latestSnapshotTxId.isPresent()) {
            LOG.info("Store has no snapshot, store is empty, creating it's first snapshot");
        }

        return latestSnapshotTxId;
    }

    void persistTransaction(String storeName, String synchronizationId, List<Command<?>> commands) {
        List<SerializableCommand> serializableCommands = commands.stream().map(m_commandProxyFactory::serializeCommand).collect(Collectors.toList());

        SerializableTransaction serializableTransaction = new SerializableTransaction();
        serializableTransaction.setTransactionModelVersion(TRANSACTION_MODEL_VERSION);
        serializableTransaction.setSynchronizationId(synchronizationId);
        serializableTransaction.setCommands(serializableCommands);

        try (OutputStream out = m_transactionStore.createTransactionOutput(storeName)) {
            LOG.debug("Enlisting new transaction", args -> args.add("Store", storeName).add("synchronizationId", synchronizationId));
            m_transactionSerializer.serializeTransaction(out, serializableTransaction);
        } catch (IOException e) {
            throw new StoreException(e);
        }
    }

    Flowable<TransactionToExecute> allTransactions() {
        return Flowable.fromPublisher(m_transactionStore.allTransactions()) //
                .map(this::createTransactionToExecute) //
                .doOnNext(tx -> LOG.debug("Received new transaction", args ->          //
                        args.add("Store", tx.m_storeName).add("synchronizationId", tx.m_synchronizationId).add("txId", tx.m_txId)));
    }

    @Nonnull
    private TransactionToExecute createTransactionToExecute(TransactionStore.TransactionInput transactionInput) throws IOException {
        SerializableTransaction serializableTransaction;
        try (InputStream in = transactionInput.getInputStream()) {
            serializableTransaction = m_transactionSerializer.deserializeTransaction(in);
        }

        if (serializableTransaction.getTransactionModelVersion() != TRANSACTION_MODEL_VERSION) {
            throw new StoreException("Transaction serializable model version is not supported",
                                     args -> args.add("version", serializableTransaction.getTransactionModelVersion())
                                             .add("expectedVersion", TRANSACTION_MODEL_VERSION));
        }

        List<Command<?>> commands = serializableTransaction.getCommands().stream().map(serializableCommand -> {
            String commandName = serializableCommand.getCommandName();
            CommandDefinition<? extends Command<?>> commandDefinition = m_commandDefinitions.get(commandName);
            checkArgument(commandDefinition != null, "Command has not been registered in the store", args -> args.add("commandName", commandName));

            Class<? extends Command<?>> commandClass = commandDefinition.getCommandClass();
            return m_commandProxyFactory.createCommand(commandClass, serializableCommand.getParameters());
        }).collect(Collectors.toList());

        return new TransactionToExecute(transactionInput.storeName(), transactionInput.getTransactionId(), serializableTransaction.getSynchronizationId(),
                                        commands);
    }

    static class TransactionToExecute {
        private final BigInteger m_txId;
        private final String m_synchronizationId;
        private final List<Command<?>> m_commands;
        private final String m_storeName;

        private TransactionToExecute(String storeName, BigInteger txId, String synchronizationId, List<Command<?>> commands) {
            m_storeName = storeName;
            m_txId = txId;
            m_synchronizationId = synchronizationId;
            m_commands = commands;
        }

        BigInteger getTxId() {
            return m_txId;
        }

        String getSynchronizationId() {
            return m_synchronizationId;
        }

        List<Command<?>> getCommands() {
            return m_commands;
        }

        String getStoreName() {
            return m_storeName;
        }
    }
}


/**
 * Copyright Alex Objelean
 */
package ro.isdc.wro.model.resource.locator.wildcard;

import static org.apache.commons.lang3.Validate.notNull;

import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;
import java.util.Comparator;
import java.util.Set;
import java.util.TreeSet;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.filefilter.FalseFileFilter;
import org.apache.commons.io.filefilter.IOFileFilter;
import org.apache.commons.io.filefilter.TrueFileFilter;
import org.apache.commons.io.filefilter.WildcardFileFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ro.isdc.wro.util.Function;

/**
 * Default implementation of {@link WildcardStreamLocator}.
 *
 * @author Alex Objelean
 * @created May 8, 2010
 */
public class DefaultWildcardStreamLocator
    implements WildcardStreamLocator, WildcardExpanderHandlerAware {
  private static final Logger LOG = LoggerFactory.getLogger(DefaultWildcardStreamLocator.class);
  /**
   * Character to distinguish wildcard inside the uri.
   */
  public static final String RECURSIVE_WILDCARD = "**";
  /**
   * Character to distinguish wildcard inside the uri. If the file name contains '*' or '?' character, it is considered
   * a wildcard.
   * <p>
   * A string is considered to contain wildcard if it doesn't start with http(s) and contains at least one of the
   * following characters: [?*].
   */
  private static final String WILDCARD_REGEX = "^(?:(?!http))(.)*[\\*\\?]+(.)*";
  /**
   * Regex used to identify the query path from the provided path.
   */
  private static final String REGEX_QUERY_PATH = "\\?.*";
  /**
   * Ensures File's natural ordering across different platforms.
   */
  private static final Comparator<File> ALPHABETIC_FILE_COMPARATOR = new Comparator<File>() {
    public int compare(final File o1, final File o2) {
      return o1.getPath().compareTo(o2.getPath());
    }
  };

  /**
   * Removes the query path from the path which potentially could be treated as a path containing wildcard special
   * characters.
   *
   * @param path to strip the query path from.
   * @return a path with the query path removed.
   */
  public static String stripQueryPath(final String path) {
    return path.replaceFirst(REGEX_QUERY_PATH, "");
  }

  /**
   * Responsible for expanding wildcards, in other words for replacing one wildcard with a set of associated files.
   */
  private Function<Collection<File>, Void> wildcardExpanderHandler;

  /**
   * {@inheritDoc}
   */
  public boolean hasWildcard(final String uri) {
    return uri.matches(WILDCARD_REGEX);
  }

  /**
   * {@inheritDoc}
   */
  public InputStream locateStream(final String uri, final File folder)
      throws IOException {
    final Collection<File> files = findMatchedFiles(new WildcardContext(uri, folder));
    final ByteArrayOutputStream out = new ByteArrayOutputStream();
    for (final File file : files) {
      if (file.isFile()) {
        final InputStream is = new FileInputStream(file);
        IOUtils.copy(is, out);
        is.close();
      } else {
        LOG.debug("Ignoring folder: " + file);
      }
    }
    return new BufferedInputStream(new ByteArrayInputStream(out.toByteArray()));
  }

  /**
   * Creates a {@link IOFileFilter} which collects found files into a collection and also populates a map with found
   * resources and corresponding files.
   *
   * @param wildcardContext
   * @param allFiles - where all found files and folders are collected.
   */
  @SuppressWarnings("serial")
  private IOFileFilter createWildcardCollectorFileFilter(final WildcardContext wildcardContext, final Collection<File> allFiles) {
    notNull(wildcardContext);
    notNull(allFiles);
    return new WildcardFileFilter(wildcardContext.getWildcard()) {
      @Override
      public boolean accept(final File file) {
        final boolean accept = super.accept(file);
        if (accept) {
          LOG.debug("\tfound resource: {}", file.getPath());
          allFiles.add(file);
        }
        return accept;
      }
    };
  }

  /**
   * @return a collection of files found inside a given folder for a search uri which contains a wildcard.
   */
  private Collection<File> findMatchedFiles(final WildcardContext wildcardContext)
      throws IOException {
    validate(wildcardContext);

    // Holds a set of all files (also folders, not only resources). This is useful for wildcard expander processing.
    final Set<File> allFiles = new TreeSet<File>(ALPHABETIC_FILE_COMPARATOR);
    final IOFileFilter fileFilter = createWildcardCollectorFileFilter(wildcardContext, allFiles);
    FileUtils.listFiles(wildcardContext.getFolder(), fileFilter, getFolderFilter(wildcardContext.getWildcard()));

    triggerWildcardExpander(allFiles, wildcardContext);

    return allFiles;
  }

  /**
   * Validates arguments used by {@link DefaultWildcardStreamLocator#findMatchedFiles(String, File)} method.
   *
   * @throws IOException if supplied arguments are invalid or cannot be handled by this locator.
   */
  private void validate(final WildcardContext wildcardContext)
    throws IOException {
    notNull(wildcardContext);
    final String uri = wildcardContext.getUri();
    final File folder = wildcardContext.getFolder();

    if (uri == null || folder == null || !folder.isDirectory()) {
      final StringBuffer message = new StringBuffer("Invalid folder provided");
      if (folder != null) {
        message.append(", with path: ").append(folder.getPath());
      }
      message.append(", with fileNameWithWildcard: ").append(uri);
      throw new IOException(message.toString());
    }
    if (!hasWildcard(uri)) {
      throw new IOException("No wildcard detected for the uri: " + uri);
    }
    LOG.debug("uri: {}", uri);
    LOG.debug("folder: {}", folder.getPath());
    LOG.debug("wildcard: {}", wildcardContext.getWildcard());
  }

  /**
   * Uses the wildcardExpanderHandler to process all found files and directories.
   *
   * @param files
   *          a collection of all files and folders found during wildcard matching.
   * @VisibleForTestOnly
   */
  void triggerWildcardExpander(final Collection<File> allFiles, final WildcardContext wildcardContext)
      throws IOException {
    LOG.debug("wildcard resources: {}", allFiles);
    if (allFiles.isEmpty()) {
      final String message = String.format("No resource found for wildcard: %s", wildcardContext.getWildcard());
      LOG.warn(message);
      throw new IOException(message);
    }
    if (wildcardExpanderHandler != null) {
      try {
        wildcardExpanderHandler.apply(allFiles);
      } catch(final IOException e) {
        // preserve exception type if the exception is already an IOException
        throw e;
      } catch (final Exception e) {
        LOG.debug("wildcard expanding error. Reporting original exception", e);
        throw new IOException("Exception during expanding wildcard: " + e.getMessage());
      }
    }
  }

  /**
   * @param wildcard
   *          to use to determine if the folder filter should be recursive or not.
   * @return filter to be used for folders.
   */
  private IOFileFilter getFolderFilter(final String wildcard) {
    final boolean recursive = wildcard.contains(RECURSIVE_WILDCARD);
    return recursive ? TrueFileFilter.INSTANCE : FalseFileFilter.INSTANCE;
  }

  /**
   * {@inheritDoc}
   */
  public void setWildcardExpanderHandler(final Function<Collection<File>, Void> handler) {
    this.wildcardExpanderHandler = handler;
  }
}


/*
 * Copyright (c) 2009. All rights reserved.
 */
package ro.isdc.wro.util;

import static org.apache.commons.lang3.StringUtils.EMPTY;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

/**
 * StringUtils Utility class. This class contains Utility methods for
 * manipulating strings. Inspired from commons & spring.
 *
 * @author Alex Objelean
 * @created Created on Feb 27, 2009
 */
public final class StringUtils {
  /**
   * Folder separator.
   */
  private static final String FOLDER_SEPARATOR = "/";

  /**
   * Top path separator.
   */
  private static final String TOP_PATH = "..";

  /**
   * Current path character.
   */
  private static final String CURRENT_PATH = ".";

  /**
   * Windows folder separator.
   */
  private static final String WINDOWS_FOLDER_SEPARATOR = "\\";

  /**
   * Private constructor to prevent instantiation.
   */
  private StringUtils() {}

  /**
   * Normalize the path by suppressing sequences like "path/.." and inner simple
   * dots.
   *

   * The result is convenient for path comparison. For other uses, notice that
   * Windows separators ("\") are replaced by simple slashes.
   *
   * @param path
   *          the original path
   * @return the normalized path
   */
  public static String cleanPath(final String path) {
    if (path == null) {
      return null;
    }
    String pathToUse = replace(path, WINDOWS_FOLDER_SEPARATOR, FOLDER_SEPARATOR);

    // Strip prefix from path to analyze, to not treat it as part of the
    // first path element. This is necessary to correctly parse paths like
    // "file:core/../core/io/Resource.class", where the ".." should just
    // strip the first "core" directory while keeping the "file:" prefix.
    final int prefixIndex = pathToUse.indexOf(":");
    String prefix = EMPTY;
    if (prefixIndex != -1) {
      prefix = pathToUse.substring(0, prefixIndex + 1);
      pathToUse = pathToUse.substring(prefixIndex + 1);
    }
    if (pathToUse.startsWith(FOLDER_SEPARATOR)) {
      prefix = prefix + FOLDER_SEPARATOR;
      pathToUse = pathToUse.substring(1);
    }

    final String[] pathArray = delimitedListToStringArray(pathToUse,
        FOLDER_SEPARATOR);
    final List<String> pathElements = new LinkedList<String>();
    int tops = 0;

    for (int i = pathArray.length - 1; i >= 0; i--) {
      final String element = pathArray[i];
      if (CURRENT_PATH.equals(element)) {
        // Points to current directory - drop it.
      } else if (TOP_PATH.equals(element)) {
        // Registering top path found.
        tops++;
      } else {
        if (tops > 0) {
          // Merging path element with element corresponding to top path.
          tops--;
        } else {
          // Normal path element found.
          pathElements.add(0, element);
        }
      }
    }

    // Remaining top paths need to be retained.
    for (int i = 0; i < tops; i++) {
      pathElements.add(0, TOP_PATH);
    }

    return prefix + collectionToDelimitedString(pathElements, FOLDER_SEPARATOR);
  }

  /**
   * Replace all occurrences of a substring within a string with another string.
   *
   * @param inString
   *          String to examine
   * @param oldPattern
   *          String to replace
   * @param newPattern
   *          String to insert
   * @return a String with the replacements
   */
  private static String replace(final String inString, final String oldPattern,
      final String newPattern) {
    if (!hasLength(inString) || !hasLength(oldPattern) || newPattern == null) {
      return inString;
    }
    final StringBuffer sbuf = new StringBuffer();
    // output StringBuffer we'll build up
    int pos = 0; // our position in the old string
    int index = inString.indexOf(oldPattern);
    // the index of an occurrence we've found, or -1
    final int patLen = oldPattern.length();
    while (index >= 0) {
      sbuf.append(inString.substring(pos, index));
      sbuf.append(newPattern);
      pos = index + patLen;
      index = inString.indexOf(oldPattern, pos);
    }
    sbuf.append(inString.substring(pos));
    // remember to append any characters to the right of a match
    return sbuf.toString();
  }

  /**
   * Convenience method to return a Collection as a delimited (e.g. CSV) String.
   * E.g. useful for toString() implementations.
   *
   * @param coll
   *          the Collection to display
   * @param delim
   *          the delimiter to use (probably a ",")
   * @return the delimited String
   */
  private static String collectionToDelimitedString(final Collection<String> coll,
      final String delim) {
    return collectionToDelimitedString(coll, delim, EMPTY, EMPTY);
  }

  /**
   * Convenience method to return a Collection as a delimited (e.g. CSV) String.
   * E.g. useful for toString() implementations.
   *
   * @param coll
   *          the Collection to display
   * @param delim
   *          the delimiter to use (probably a ",")
   * @param prefix
   *          the String to start each element with
   * @param suffix
   *          the String to end each element with
   * @return the delimited String
   */
  private static String collectionToDelimitedString(final Collection<String> coll,
      final String delim, final String prefix, final String suffix) {
    if (coll == null || coll.isEmpty()) {
      return EMPTY;
    }
    final StringBuffer sb = new StringBuffer();
    final Iterator<String> it = coll.iterator();
    while (it.hasNext()) {
      sb.append(prefix).append(it.next()).append(suffix);
      if (it.hasNext()) {
        sb.append(delim);
      }
    }
    return sb.toString();
  }

  /**
   * Take a String which is a delimited list and convert it to a String array.
   *

   * A single delimiter can consists of more than one character: It will still
   * be considered as single delimiter string, rather than as bunch of potential
   * delimiter characters - in contrast to tokenizeToStringArray.
   *
   * @param str
   *          the input String
   * @param delimiter
   *          the delimiter between elements (this is a single delimiter, rather
   *          than a bunch individual delimiter characters)
   * @return an array of the tokens in the list
   * @see #tokenizeToStringArray
   */
  private static String[] delimitedListToStringArray(final String str,
      final String delimiter) {
    return delimitedListToStringArray(str, delimiter, null);
  }

  /**
   * Take a String which is a delimited list and convert it to a String array.
   *

   * A single delimiter can consists of more than one character: It will still
   * be considered as single delimiter string, rather than as bunch of potential
   * delimiter characters - in contrast to tokenizeToStringArray.
   *
   * @param str
   *          the input String
   * @param delimiter
   *          the delimiter between elements (this is a single delimiter, rather
   *          than a bunch individual delimiter characters)
   * @param charsToDelete
   *          a set of characters to delete. Useful for deleting unwanted line
   *          breaks: e.g. "\r\n\f" will delete all new lines and line feeds in
   *          a String.
   * @return an array of the tokens in the list
   * @see #tokenizeToStringArray
   */
  private static String[] delimitedListToStringArray(final String str,
      final String delimiter, final String charsToDelete) {
    if (str == null) {
      return new String[0];
    }
    if (delimiter == null) {
      return new String[] { str };
    }
    final List<String> result = new ArrayList<String>();
    if ("".equals(delimiter)) {
      for (int i = 0; i < str.length(); i++) {
        result.add(deleteAny(str.substring(i, i + 1), charsToDelete));
      }
    } else {
      int pos = 0;
      int delPos = 0;
      while ((delPos = str.indexOf(delimiter, pos)) != -1) {
        result.add(deleteAny(str.substring(pos, delPos), charsToDelete));
        pos = delPos + delimiter.length();
      }
      if (str.length() > 0 && pos <= str.length()) {
        // Add rest of String, but not in case of empty input.
        result.add(deleteAny(str.substring(pos), charsToDelete));
      }
    }
    return toStringArray(result);
  }

  /**
   * Copy the given Collection into a String array. The Collection must contain
   * String elements only.
   *
   * @param collection
   *          the Collection to copy
   * @return the String array (null if the passed-in Collection
   *         was null)
   */
  private static String[] toStringArray(final Collection<?> collection) {
    if (collection == null) {
      return null;
    }
    return collection.toArray(new String[collection.size()]);
  }

  /**
   * Delete any character in a given String.
   *
   * @param inString
   *          the original String
   * @param charsToDelete
   *          a set of characters to delete. E.g. "az\n" will delete 'a's, 'z's
   *          and new lines.
   * @return the resulting String
   */
  private static String deleteAny(final String inString,
      final String charsToDelete) {
    if (!hasLength(inString) || !hasLength(charsToDelete)) {
      return inString;
    }
    final StringBuffer out = new StringBuffer();
    for (int i = 0; i < inString.length(); i++) {
      final char c = inString.charAt(i);
      if (charsToDelete.indexOf(c) == -1) {
        out.append(c);
      }
    }
    return out.toString();
  }

  /**
   * Check that the given CharSequence is neither null nor of
   * length 0. Note: Will return true for a CharSequence that
   * purely consists of whitespace.
   *

   *
   *

   * StringUtils.hasLength(null) = false
   * StringUtils.hasLength("") = false
   * StringUtils.hasLength(" ") = true
   * StringUtils.hasLength("Hello") = true
   *

   *
   * @param str
   *          the CharSequence to check (may be null)
   * @return true if the CharSequence is not null and has length
   * @see #hasText(String)
   */
  private static boolean hasLength(final CharSequence str) {
    return (str != null && str.length() > 0);
  }

  /**
   * Check that the given String is neither null nor of length 0.
   * Note: Will return true for a String that purely consists of
   * whitespace.
   *
   * @param str
   *          the String to check (may be null)
   * @return true if the String is not null and has length
   * @see #hasLength(CharSequence)
   */
  private static boolean hasLength(final String str) {
    return hasLength((CharSequence) str);
  }
}


/*
 * Copyright (C) 2010. All rights reserved.
 */
package ro.isdc.wro.extensions.processor.js;

import java.io.IOException;
import java.io.Reader;
import java.io.Writer;

import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ro.isdc.wro.WroRuntimeException;
import ro.isdc.wro.extensions.processor.css.RhinoLessCssProcessor;
import ro.isdc.wro.extensions.processor.support.ObjectPoolHelper;
import ro.isdc.wro.extensions.processor.support.typescript.TypeScriptCompiler;
import ro.isdc.wro.model.resource.Resource;
import ro.isdc.wro.model.resource.ResourceType;
import ro.isdc.wro.model.resource.SupportedResourceType;
import ro.isdc.wro.model.resource.processor.Destroyable;
import ro.isdc.wro.model.resource.processor.ResourcePostProcessor;
import ro.isdc.wro.model.resource.processor.ResourcePreProcessor;
import ro.isdc.wro.util.ObjectFactory;

/**
 * Compiles TypeScript into javascript in a cross platform manner. Uses rhino to interpret javascript implementation of
 * the compiler.
 *
 * @author Alex Objelean
 * @since 1.6.3
 * @created 21 Jan 2013
 */
@SupportedResourceType(ResourceType.JS)
public class RhinoTypeScriptProcessor
    implements ResourcePreProcessor, ResourcePostProcessor, Destroyable {
  private static final Logger LOG = LoggerFactory.getLogger(RhinoLessCssProcessor.class);

  public static final String ALIAS = "rhinoTypeScript";

  private ObjectPoolHelper<TypeScriptCompiler> enginePool;

  public RhinoTypeScriptProcessor() {
    enginePool = new ObjectPoolHelper<TypeScriptCompiler>(new ObjectFactory<TypeScriptCompiler>() {
      @Override
      public TypeScriptCompiler create() {
        return newCompiler();
      }
    });
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void process(final Resource resource, final Reader reader, final Writer writer)
      throws IOException {
    LOG.debug("processing resource: {}", resource);
    final String content = IOUtils.toString(reader);
    final TypeScriptCompiler compiler = enginePool.getObject();
    try {
      writer.write(compiler.compile(content));
    } catch (final Exception e) {
      onException(e, content);
    } finally {
      // return for later reuse
      enginePool.returnObject(compiler);
      reader.close();
      writer.close();
    }
  }

  /**
   * Invoked when a processing exception occurs.
   */
  protected void onException(final Exception e, final String content) {
    throw WroRuntimeException.wrap(e);
  }

  /**
   * @return the {@link TypeScriptCompiler} engine implementation. Override it to provide a different version of the typescript.js
   *         library. Useful for upgrading the processor outside the wro4j release.
   */
  protected TypeScriptCompiler newCompiler() {
    return new TypeScriptCompiler();
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void process(final Reader reader, final Writer writer)
      throws IOException {
    process(null, reader, writer);
  }

  @Override
  public void destroy() throws Exception {
    enginePool.destroy();
  }
}


package ro.isdc.wro.extensions.processor.js;

import org.apache.commons.io.FilenameUtils;

import ro.isdc.wro.extensions.processor.support.emberjs.EmberJs;
import ro.isdc.wro.extensions.processor.support.template.AbstractJsTemplateCompiler;
import ro.isdc.wro.model.resource.Resource;
import ro.isdc.wro.model.resource.ResourceType;
import ro.isdc.wro.model.resource.SupportedResourceType;

/**
 * Compiles EmberJS templates to javascript. The processor loads emberJs library and all its dependencies from the webjar.
 *
 * @author blemoine
 */
@SupportedResourceType(ResourceType.JS)
public class EmberJsProcessor
    extends JsTemplateCompilerProcessor {

  public static final String ALIAS = "emberJs";

  /**
   * {@inheritDoc}
   */
  @Override
  protected AbstractJsTemplateCompiler createCompiler() {
    return new EmberJs();
  }

  /**
   * {@inheritDoc}
   */
  @Override
  protected String getArgument(final Resource resource) {
    final String name = resource == null ? "" : FilenameUtils.getBaseName(resource.getUri());
    return String.format("'%s'", name);
  }
}


/**
 * Copyright@2011 wro4j
 */
package ro.isdc.wro.http.handler.factory;

import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ro.isdc.wro.http.handler.RequestHandler;
import ro.isdc.wro.http.handler.spi.RequestHandlerProvider;
import ro.isdc.wro.model.resource.support.AbstractConfigurableMultipleStrategy;

/**
 * A {@link RequestHandler} implementation which is easy to configure using a {@link Properties} object.
 *
 * @author Alex Objelean
 * @created 30 Jul 2011
 * @since 1.5.0
 */
public class ConfigurableRequestHandlerFactory
    extends AbstractConfigurableMultipleStrategy<RequestHandler, RequestHandlerProvider>
    implements RequestHandlerFactory {
  private static final Logger LOG = LoggerFactory.getLogger(ConfigurableRequestHandlerFactory.class);
  /**
   * Name of the property used to configure requestHandlers.
   */
  public static final String KEY = "requestHandlers";

  private final RequestHandlerFactory requestHandlerFactory = newRequestHandlerFactory();

  /**
   * {@inheritDoc}
   */
  @Override
  protected String getStrategyKey() {
    return KEY;
  }

  /**
   * {@inheritDoc}
   */
  @Override
  protected Map<String, RequestHandler> getStrategies(final RequestHandlerProvider provider) {
    return provider.provideRequestHandlers();
  }

  /**
   * {@inheritDoc}
   */
  public Collection<RequestHandler> create() {
    return requestHandlerFactory.create();
  }

  /**
   * {@inheritDoc}
   */
  private RequestHandlerFactory newRequestHandlerFactory() {
    final SimpleRequestHandlerFactory factory = new SimpleRequestHandlerFactory();
    final List<RequestHandler> requestHandlers = getConfiguredStrategies();
    for (final RequestHandler requestHandler : requestHandlers) {
      factory.addHandler(requestHandler);
    }
    // use default when none provided
    if (requestHandlers.isEmpty()) {
      LOG.debug("No locators configured. Using Default locator factory.");
      return new DefaultRequestHandlerFactory();
    }
    return factory;
  }

  /**
   * {@inheritDoc}
   */
  @Override
  protected Class<RequestHandlerProvider> getProviderClass() {
    return RequestHandlerProvider.class;
  }
}


package ro.isdc.wro.extensions.support.lint;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.builder.ToStringBuilder;

/**
 * Encapsulates information about lint errors associated with a collection of resources. This class is generic, the type
 * <T> describe the type of lint errors.
 *
 * @author Alex Objelean
 * @created 16 Sep 2012
 * @since 1.5.0
 */
public final class LintReport<T> {
  private final List<ResourceLintReport<T>> reports;

  public LintReport() {
    reports = new ArrayList<ResourceLintReport<T>>() {};
  }

  /**
   * @return a readonly collection of resource
   */
  public List<ResourceLintReport<T>> getReports() {
    return Collections.unmodifiableList(reports);
  }

  /**
   * Add a single lint report to underlying collection.
   *
   * @param resourceLintReport
   *          {@link ResourceLintReport} to add.
   * @return reference to this {@link LintReport} object (fluent interface).
   */
  public LintReport<T> addReport(final ResourceLintReport<T> resourceLintReport) {
    Validate.notNull(resourceLintReport);
    reports.add(resourceLintReport);
    return this;
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public String toString() {
    return ToStringBuilder.reflectionToString(this);
  }
}


/*
 * Copyright wro4j@2011
 */
package ro.isdc.wro.extensions.processor.support.linter;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Type;
import java.util.List;

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

import ro.isdc.wro.WroRuntimeException;
import ro.isdc.wro.extensions.locator.WebjarUriLocator;
import ro.isdc.wro.extensions.processor.support.csslint.CssLint;
import ro.isdc.wro.extensions.script.RhinoScriptBuilder;
import ro.isdc.wro.util.StopWatch;
import ro.isdc.wro.util.WroUtil;

import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;

/**
 * Encapsulates common code for utilities like jsHint or jsLint.
 *
 * @author Alex Objelean
 * @created 19 Sept 2011
 * @since 1.4.2
 */
public abstract class AbstractLinter {
  private static final Logger LOG = LoggerFactory.getLogger(AbstractLinter.class);
  /**
   * Required to make jshint not complain.
   */
  private static final String DEFINE_WINDOW = "var window = {};";
  private WebjarUriLocator webjarLocator;
  private final OptionsBuilder optionsBuilder = new OptionsBuilder();
  /**
   * Options to apply to js hint processing
   */
  private String options;
  private RhinoScriptBuilder builder;

  /**
   * Initialize script builder for evaluation.
   */
  private RhinoScriptBuilder initScriptBuilder() {
    try {
      if (builder == null) {
        builder = RhinoScriptBuilder.newChain().evaluateChain(DEFINE_WINDOW, "window").evaluateChain(
            getScriptAsStream(), "linter.js");
      }
      return builder;
    } catch (final IOException e) {
      throw new WroRuntimeException("Failed reading init script", e);
    }
  }

  /**
   * @return {@link WebjarUriLocator} instance to retrieve webjars.
   */
  protected final WebjarUriLocator getWebjarLocator() {
    if (webjarLocator == null) {
      webjarLocator = new WebjarUriLocator();
    }
    return webjarLocator;
  }

  /**
   * @return the stream of the linter script. Override this method to provide a different script version.
   * @throws IOException
   *           if the stream is invalid or unavailable.
   */
  protected abstract InputStream getScriptAsStream()
      throws IOException;

  /**
   * Validates a js using jsHint and throws {@link LinterException} if the js is invalid. If no exception is thrown, the
   * js is valid.
   *
   * @param data
   *          js content to process.
   */
  public void validate(final String data)
      throws LinterException {
    final StopWatch watch = new StopWatch();
    watch.start("init");
    final RhinoScriptBuilder builder = initScriptBuilder();
    watch.stop();
    watch.start("lint");
    final String packIt = buildLinterScript(WroUtil.toJSMultiLineString(data), getOptions());
    final boolean valid = Boolean.parseBoolean(builder.evaluate(packIt, "check").toString());
    if (!valid) {
      final String json = builder.addJSON().evaluate(
          String.format("JSON.stringify(JSON.decycle(%s.errors))", getLinterName()), "stringify errors").toString();
      LOG.debug("json {}", json);
      final Type type = new TypeToken<List<LinterError>>() {}.getType();
      final List<LinterError> errors = new Gson().fromJson(json, type);
      LOG.debug("errors {}", errors);
      throw new LinterException().setErrors(errors);
    }
    LOG.debug("result: {}", valid);
    watch.stop();
    LOG.debug(watch.prettyPrint());
  }

  /**
   * @return the name of the function used to perform the lint operation.
   */
  protected abstract String getLinterName();

  /**
   * TODO this method is duplicated in {@link CssLint}. Extract and reuse it.
   *
   * @param data
   *          script to process.
   * @param options
   *          options to set as true
   * @return Script used to pack and return the packed result.
   */
  private String buildLinterScript(final String data, final String options) {
    return String.format("%s(%s,%s);", getLinterName(), data, optionsBuilder.buildFromCsv(options));
  }

  /**
   * @param options
   *          the options to set
   */
  public AbstractLinter setOptions(final String... options) {
    this.options = StringUtils.join(options, ',');
    LOG.debug("setOptions: {}", this.options);
    return this;
  }

  /**
   * @return an options as CSV.
   */
  private String getOptions() {
    if (options == null) {
      options = createDefaultOptions();
    }
    return options;
  }

  /**
   * @return default options to use for linting.
   */
  protected String createDefaultOptions() {
    return "";
  }
}


/*
 * Copyright (c) 2009.
 */
package ro.isdc.wro.manager.factory;

import java.util.Map;
import java.util.Properties;

import javax.servlet.FilterConfig;

import org.apache.commons.lang3.Validate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ro.isdc.wro.cache.CacheKey;
import ro.isdc.wro.cache.CacheStrategy;
import ro.isdc.wro.cache.CacheValue;
import ro.isdc.wro.cache.ConfigurableCacheStrategy;
import ro.isdc.wro.config.Context;
import ro.isdc.wro.config.factory.FilterConfigWroConfigurationFactory;
import ro.isdc.wro.config.factory.ServletContextPropertyWroConfigurationFactory;
import ro.isdc.wro.model.factory.ConfigurableModelFactory;
import ro.isdc.wro.model.factory.WroModelFactory;
import ro.isdc.wro.model.resource.locator.UriLocator;
import ro.isdc.wro.model.resource.locator.factory.ConfigurableLocatorFactory;
import ro.isdc.wro.model.resource.locator.factory.UriLocatorFactory;
import ro.isdc.wro.model.resource.locator.support.LocatorProvider;
import ro.isdc.wro.model.resource.processor.ResourcePostProcessor;
import ro.isdc.wro.model.resource.processor.ResourcePreProcessor;
import ro.isdc.wro.model.resource.processor.factory.ConfigurableProcessorsFactory;
import ro.isdc.wro.model.resource.processor.factory.ProcessorsFactory;
import ro.isdc.wro.model.resource.processor.support.ProcessorProvider;
import ro.isdc.wro.model.resource.support.hash.ConfigurableHashStrategy;
import ro.isdc.wro.model.resource.support.hash.HashStrategy;
import ro.isdc.wro.model.resource.support.naming.ConfigurableNamingStrategy;
import ro.isdc.wro.model.resource.support.naming.NamingStrategy;

/**
 * Allow configuring uriLocators & processors to add using initParams of the filter.
 *
 * @author Alex Objelean
 * @created Created on Dec 31, 2009
 */
public class ConfigurableWroManagerFactory extends BaseWroManagerFactory {
  private static final Logger LOG = LoggerFactory.getLogger(ConfigurableWroManagerFactory.class);
  private Properties configProperties;
  private Properties additionalConfigProperties;

  /**
   * Allow subclasses to contribute with it's own locators.
   *
   * @param map containing locator mappings.
   */
  protected void contributeLocators(final Map<String, UriLocator> map) {}

  /**
   * Allow subclasses to contribute with it's own pre processors.
   *

   * It is implementor responsibility to add a {@link ResourcePreProcessor} instance.
   *
   * @param map containing processor mappings.
   */
  protected void contributePreProcessors(final Map<String, ResourcePreProcessor> map) {}

  /**
   * Allow subclasses to contribute with it's own processors.
   *

   * It is implementor responsibility to add a {@link ResourcePostProcessor} instance.
   *
   * @param map containing processor mappings.
   */
  protected void contributePostProcessors(final Map<String, ResourcePostProcessor> map) {}

  @Override
  protected UriLocatorFactory newUriLocatorFactory() {
    return new ConfigurableLocatorFactory() {
      @Override
      protected Properties newProperties() {
        final Properties props = new Properties();
        updatePropertiesWithConfiguration(props, ConfigurableLocatorFactory.PARAM_URI_LOCATORS);
        return props;
      }
      @Override
      protected Map<String, UriLocator> getStrategies(final LocatorProvider provider) {
        final Map<String, UriLocator> map = super.getStrategies(provider);
        contributeLocators(map);
        return map;
      }
    };
  }

  /**
   * Reuse {@link ConfigurableProcessorsFactory} for processors lookup.
   */
  @Override
  protected ProcessorsFactory newProcessorsFactory() {
    return new ConfigurableProcessorsFactory() {
      @Override
      protected Properties newProperties() {
        final Properties props = new Properties();
        updatePropertiesWithConfiguration(props, ConfigurableProcessorsFactory.PARAM_PRE_PROCESSORS);
        updatePropertiesWithConfiguration(props, ConfigurableProcessorsFactory.PARAM_POST_PROCESSORS);
        return props;
      }
      @Override
      protected Map<String, ResourcePostProcessor> getPostProcessorStrategies(final ProcessorProvider provider) {
        final Map<String, ResourcePostProcessor> map = super.getPostProcessorStrategies(provider);
        contributePostProcessors(map);
        return map;
      }
      @Override
      protected Map<String, ResourcePreProcessor> getPreProcessorStrategies(final ProcessorProvider provider) {
        final Map<String, ResourcePreProcessor> map = super.getPreProcessorStrategies(provider);
        contributePreProcessors(map);
        return map;
      }
    };
  }

  @Override
  protected NamingStrategy newNamingStrategy() {
    return new ConfigurableNamingStrategy() {
      @Override
      protected Properties newProperties() {
        final Properties props = new Properties();
        updatePropertiesWithConfiguration(props, ConfigurableNamingStrategy.KEY);
        return props;
      }
    };
  }

  @Override
  protected HashStrategy newHashStrategy() {
    return new ConfigurableHashStrategy() {
      @Override
      protected Properties newProperties() {
        final Properties props = new Properties();
        updatePropertiesWithConfiguration(props, ConfigurableHashStrategy.KEY);
        return props;
      }
    };
  }

  @Override
  protected CacheStrategy<CacheKey, CacheValue> newCacheStrategy() {
    return new ConfigurableCacheStrategy() {
      @Override
      protected Properties newProperties() {
        final Properties props = new Properties();
        updatePropertiesWithConfiguration(props, ConfigurableCacheStrategy.KEY);
        return props;
      }
    };
  }

  @Override
  protected WroModelFactory newModelFactory() {
    return new ConfigurableModelFactory() {
      @Override
      protected Properties newProperties() {
        final Properties props = new Properties();
        updatePropertiesWithConfiguration(props, ConfigurableModelFactory.KEY);
        return props;
      }
    };
  }

  /**
   * Add to properties a new key with value extracted either from filterConfig or from configurable properties file.
   * This method helps to ensure backward compatibility of the filterConfig vs configProperties configuration.
   *
   * @param props
   *          the {@link Properties} which will be populated with the value extracted from filterConfig or
   *          configProperties for the provided key.
   * @param key
   *          to read from filterConfig or configProperties and put into props.
   */
  private void updatePropertiesWithConfiguration(final Properties props, final String key) {
    final FilterConfig filterConfig = Context.get().getFilterConfig();
    // first, retrieve value from init-param for backward compatibility
    final String valuesAsString = filterConfig.getInitParameter(key);
    if (valuesAsString != null) {
      props.setProperty(key, valuesAsString);
    } else {
      // retrieve value from configProperties file
      final String value = getConfigProperties().getProperty(key);
      if (value != null) {
        props.setProperty(key, value);
      }
    }
  }

  /**
   * Use this method rather than accessing the field directly, because it will create a default one if none is provided.
   */
  private Properties getConfigProperties() {
    if (configProperties == null) {
      configProperties = newConfigProperties();
      if (additionalConfigProperties != null) {
        configProperties.putAll(additionalConfigProperties);
      }
    }
    return configProperties;
  }

  /**
   * Override this method to provide a different config properties file location. It is very likely that you would like
   * it to be the same as the one used by the {@link FilterConfigWroConfigurationFactory}. The default properties file
   * location is /WEB-INF/wro.properties.
   *
   * @return a not null properties object used as a secondary configuration option for processors if these are not
   *         configured in init-param.
   */
  protected Properties newConfigProperties() {
    // default location is /WEB-INF/wro.properties
    final Properties props = new Properties();
    try {
      return new ServletContextPropertyWroConfigurationFactory(Context.get().getServletContext()).createProperties();
    } catch (final Exception e) {
      LOG.warn("No configuration property file found. Using default values.", e);
    }
    return props;
  }

  ConfigurableWroManagerFactory addConfigProperties(final Properties configProperties) {
    Validate.notNull(configProperties);
    this.additionalConfigProperties = configProperties;
    return this;
  }

  /**
   * Setter is useful for unit tests.
   */
  ConfigurableWroManagerFactory setConfigProperties(final Properties configProperties) {
    Validate.notNull(configProperties);
    this.configProperties = configProperties;
    return this;
  }
}