content: package ro.isdc.wro.maven.plugin.support;
  
  import static org.apache.commons.lang3.Validate.notNull;
  
  import java.io.File;
  import java.io.IOException;
  import java.io.InputStreamReader;
  import java.io.Reader;
  import java.io.StringWriter;
  import java.util.HashSet;
  import java.util.List;
  import java.util.Set;
  import java.util.concurrent.atomic.AtomicBoolean;
  
  import org.apache.maven.plugin.logging.Log;
  import org.sonatype.plexus.build.incremental.BuildContext;
  
  import ro.isdc.wro.manager.WroManager;
  import ro.isdc.wro.manager.factory.WroManagerFactory;
  import ro.isdc.wro.model.group.processor.InjectorBuilder;
  import ro.isdc.wro.model.resource.Resource;
  import ro.isdc.wro.model.resource.ResourceType;
  import ro.isdc.wro.model.resource.locator.factory.UriLocatorFactory;
  import ro.isdc.wro.model.resource.processor.ResourcePreProcessor;
  import ro.isdc.wro.model.resource.processor.decorator.ExceptionHandlingProcessorDecorator;
  import ro.isdc.wro.model.resource.processor.impl.css.AbstractCssImportPreProcessor;
  import ro.isdc.wro.model.resource.processor.impl.css.CssImportPreProcessor;
  import ro.isdc.wro.model.resource.support.hash.HashStrategy;
  import ro.isdc.wro.util.Function;
  
  import com.google.common.annotations.VisibleForTesting;
  
  
  /**
   * Encapsulates the details about resource change detection and persist the change information in build context.
   *
   * @author Alex Objelean
   * @created 2 Oct 2013
   * @since 1.7.2
   */
  public class ResourceChangeHandler {
    private enum ChangeStatus {
      CHANGED, NOT_CHANGED
    }
  
    private WroManagerFactory managerFactory;
    private Log log;
    /**
     * Responsible for build storage persistence. Uses configured {@link BuildContext} as a primary storage object.
     */
    private BuildContextHolder buildContextHolder;
    private BuildContext buildContext;
    private File buildDirectory;
    private boolean incrementalBuildEnabled;
    /**
     * Contains the set of already remembered resources. Used to avoid duplicate hash computation.
     */
    private final Set<String> rememberedSet = new HashSet<String>();
  
    /**
     * Factory method which requires all mandatory fields.
     */
    public static ResourceChangeHandler create(final WroManagerFactory managerFactory, final Log log) {
      notNull(managerFactory, "WroManagerFactory was not set");
      notNull(log, "Log was not set");
      return new ResourceChangeHandler().setManagerFactory(managerFactory).setLog(log);
    }
  
    private ResourceChangeHandler() {
    }
  
  
    public boolean isResourceChanged(final Resource resource) {
      notNull(resource, "Invalid resource provided");
  
      final WroManager manager = getManagerFactory().create();
      final HashStrategy hashStrategy = manager.getHashStrategy();
      final UriLocatorFactory locatorFactory = manager.getUriLocatorFactory();
      // using AtomicBoolean because we need to mutate this variable inside an anonymous class.
      final AtomicBoolean changeDetected = new AtomicBoolean(false);
      try {
        final String fingerprint = hashStrategy.getHash(locatorFactory.locate(resource.getUri()));
        final String previousFingerprint = getBuildContextHolder().getValue(resource.getUri());
  
        final boolean newValue = fingerprint != null && !fingerprint.equals(previousFingerprint);
        changeDetected.set(newValue);
  
        if (!changeDetected.get() && resource.getType() == ResourceType.CSS) {
          final Reader reader = new InputStreamReader(locatorFactory.locate(resource.getUri()));
          getLog().debug("Check @import directive from " + resource);
          // detect changes in imported resources.
          detectChangeForCssImports(resource, reader, changeDetected);
        }
        return changeDetected.get();
      } catch (final IOException e) {
        getLog().error("failed to check for delta resource: " + resource, e);
      }
      return false;
    }
  
    private void detectChangeForCssImports(final Resource resource, final Reader reader,
        final AtomicBoolean changeDetected)
        throws IOException {
      forEachCssImportApply(new Function<String, ChangeStatus>() {
        public ChangeStatus apply(final String importedUri) throws Exception {
          final boolean isImportChanged = isResourceChanged(Resource.create(importedUri, ResourceType.CSS));
          getLog().debug("\tisImportChanged: " + isImportChanged);
          if (isImportChanged) {
            changeDetected.set(true);
            return ChangeStatus.CHANGED;
          }
          return ChangeStatus.NOT_CHANGED;
        }
      }, resource, reader);
    }
  
    /**
     * Will persist the information regarding the provided resource in some internal store. This information will be used
     * later to check if the resource is changed.
     *
     * @param resource
     *          {@link Resource} to touch.
     */
    public void remember(final Resource resource) {
      final WroManager manager = getManagerFactory().create();
      final HashStrategy hashStrategy = manager.getHashStrategy();
      final UriLocatorFactory locatorFactory = manager.getUriLocatorFactory();
  
      if (rememberedSet.contains(resource.getUri())) {
        // only calculate fingerprints and check imports if not already done
        getLog().debug("Resource with uri '" + resource.getUri() + "' has already been updated in this run.");
      } else {
        try {
          final String fingerprint = hashStrategy.getHash(locatorFactory.locate(resource.getUri()));
          getBuildContextHolder().setValue(resource.getUri(), fingerprint);
          rememberedSet.add(resource.getUri());
          getLog().debug("Persist fingerprint for resource '" + resource.getUri() + "' : " + fingerprint);
          if (resource.getType() == ResourceType.CSS) {
            final Reader reader = new InputStreamReader(locatorFactory.locate(resource.getUri()));
            getLog().debug("Check @import directive from " + resource);
            // persist fingerprints in imported resources.
            persistFingerprintsForCssImports(resource, reader);
          }
        } catch (final IOException e) {
          getLog().debug("could not check fingerprint of resource: " + resource);
        }
      }
    }
  
    private void persistFingerprintsForCssImports(final Resource resource, final Reader reader)
        throws IOException {
      forEachCssImportApply(new Function<String, ChangeStatus>() {
        public ChangeStatus apply(final String importedUri) throws Exception {
          remember(Resource.create(importedUri, ResourceType.CSS));
          return ChangeStatus.NOT_CHANGED;
        }
      }, resource, reader);
    }
  
    /**
     * Invokes the provided function for each detected css import.
     *
     * @param func
     *          a function (closure) invoked for each found import. It will be provided as argument the uri of imported
     *          css.
     */
    private void forEachCssImportApply(final Function<String, ChangeStatus> func, final Resource resource, final Reader reader)
        throws IOException {
      final ResourcePreProcessor processor = createCssImportProcessor(func);
      InjectorBuilder.create(getManagerFactory()).build().inject(processor);
      processor.process(resource, reader, new StringWriter());
    }
  
    private ResourcePreProcessor createCssImportProcessor(final Function<String, ChangeStatus> func) {
      final ResourcePreProcessor cssImportProcessor = new AbstractCssImportPreProcessor() {
        @Override
        protected void onImportDetected(final String importedUri) {
          getLog().debug("Found @import " + importedUri);
          try {
            final ChangeStatus status = func.apply(importedUri);
            getLog().debug("ChangeStatus for " + importedUri + ": " + status);
            if (ChangeStatus.NOT_CHANGED.equals(status)) {
          	  remember(Resource.create(importedUri, ResourceType.CSS));
            }
          } catch (final Exception e) {
            getLog().error("Cannot apply a function on @import resource: " + importedUri + ". Ignoring it.", e);
          }
          remember(Resource.create(importedUri, ResourceType.CSS));
        }
  
        @Override
        protected String doTransform(final String cssContent, final List<Resource> foundImports)
            throws IOException {
          // no need to build the content, since we are interested in finding imported resources only
          return "";
        }
  
        @Override
        public String toString() {
          return CssImportPreProcessor.class.getSimpleName();
        }
      };
      final ResourcePreProcessor processor = new ExceptionHandlingProcessorDecorator(cssImportProcessor) {
        @Override
        protected boolean isIgnoreFailingProcessor() {
          return true;
        }
      };
      return processor;
    }
  
    private BuildContextHolder getBuildContextHolder() {
      if (buildContextHolder == null) {
        buildContextHolder = new BuildContextHolder(buildContext, buildDirectory);
        buildContextHolder.setIncrementalBuildEnabled(incrementalBuildEnabled);
      }
      return buildContextHolder;
    }
  
    @VisibleForTesting
    void setBuildContextHolder(final BuildContextHolder buildContextHolder) {
      this.buildContextHolder = buildContextHolder;
    }
  
    private WroManagerFactory getManagerFactory() {
      return managerFactory;
    }
  
    public Log getLog() {
      return log;
    }
  
    public ResourceChangeHandler setManagerFactory(final WroManagerFactory wroManagerFactory) {
      this.managerFactory = wroManagerFactory;
      return this;
    }
  
    public ResourceChangeHandler setLog(final Log log) {
      this.log = log;
      return this;
    }
  
    public ResourceChangeHandler setBuildContext(final BuildContext buildContext) {
      this.buildContext = buildContext;
      return this;
    }
  
    public ResourceChangeHandler setBuildDirectory(final File buildDirectory) {
      this.buildDirectory = buildDirectory;
      return this;
    }
  
    public ResourceChangeHandler setIncrementalBuildEnabled(final boolean incrementalBuildEnabled) {
      this.incrementalBuildEnabled = incrementalBuildEnabled;
      return this;
    }
  
    public boolean isIncrementalBuild() {
      return getBuildContextHolder().isIncrementalBuild();
    }
  
    /**
     * Destroys all information about resource tracked for changes.
     */
    public void destroy() {
      getBuildContextHolder().destroy();
      rememberedSet.clear();
    }
  
    /**
     * After invoking this method on a resource, the next invocation of {@link #isResourceChanged(Resource)} will return
     * true.
     *
     * @param resourceUri
     *          uri of the resource to clear from persisted storage.
     */
    public void forget(final Resource resource) {
      if (resource != null) {
        getBuildContextHolder().setValue(resource.getUri(), null);
        rememberedSet.remove(resource.getUri());
      }
    }
  
    /**
     * Persist the values stored in BuildContext(Holder)
     */
    public void persist() {
      getBuildContextHolder().persist();
    }
  }
  