/*
 * Copyright (C) 2016 Airbnb, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.airbnb.rxgroups;

import android.app.Activity;
import android.app.Fragment;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;

import java.util.UUID;

import javax.annotation.Nullable;

import io.reactivex.ObservableTransformer;
import io.reactivex.Observer;

/**
 * Manges unlocking, locking, and destroying observables based on the lifecycle of an activity or
 * fragment. An Activity or fragment must call:
 *

 *
{@link #onCreate(ObservableManager, Bundle, Object)}

 *
{@link #onResume()}

 *
{@link #onPause()}

 *
{@link #onDestroy(Activity)}

 *
{@link #onSaveInstanceState(Bundle)}

 *

 * in corresponding methods.
 */
@SuppressWarnings("WeakerAccess")
public class GroupLifecycleManager {
  private static final String KEY_STATE = "KEY_GROUPLIFECYCLEMANAGER_STATE";

  private final ObservableManager observableManager;
  private final ObservableGroup group;
  private boolean hasSavedState;

  private GroupLifecycleManager(ObservableManager observableManager, ObservableGroup group) {
    this.observableManager = observableManager;
    this.group = group;
  }

  /** Call this method from your Activity or Fragment's onCreate method */
  public static GroupLifecycleManager onCreate(ObservableManager observableManager,
      @Nullable Bundle savedState, @Nullable Object target) {

    ObservableGroup group;
    if (savedState != null) {
      State state = savedState.getParcelable(KEY_STATE);

      Preconditions.checkState(state != null, "Must call onSaveInstanceState() first");

      // First check the instance ID before restoring state. If it's not the same instance,
      // then we have to create a new group since the previous one is already destroyed.
      // Android can sometimes reuse the same instance after saving state and we can't reliably
      // determine when that happens. This is a workaround for that behavior.
      if (state.managerId != observableManager.id()) {
        group = observableManager.newGroup();
      } else {
        group = observableManager.getGroup(state.groupId);
      }
    } else {
      group = observableManager.newGroup();
    }

    group.lock();

    GroupLifecycleManager manager = new GroupLifecycleManager(observableManager, group);

    if (target != null) {
      manager.initializeAutoTaggingAndResubscription(target);
    }

    return manager;
  }

  /** @return the {@link ObservableGroup} associated to this instance */
  public ObservableGroup group() {
    return group;
  }

  /**
   * Calls {@link ObservableGroup#transform(Observer)}  for the group managed by
   * this instance.
   */
  public <T> ObservableTransformer<? super T, T> transform(Observer<? super T> observer) {
    return group.transform(observer);
  }

  /**
   * Calls {@link ObservableGroup#transform(Observer, String)}  for the group managed by
   * this instance.
   */
  public <T> ObservableTransformer<? super T, T> transform(Observer<? super T> observer,
          String observableTag) {
    return group.transform(observer, observableTag);
  }

  /**
   * Call {@link ObservableGroup#hasObservables(Observer)} for the group managed by
   * this instance.
   */
  public boolean hasObservables(Observer<?> observer) {
    return group.hasObservables(observer);
  }

  /**
   * Call {@link ObservableGroup#hasObservable(Observer, String)} for the group managed by
   * this instance.
   */
  public boolean hasObservable(Observer<?> observer, String observableTag) {
    return group.hasObservable(observer, observableTag);
  }

  /**
   * Calls
   * {@link ObservableGroup#initializeAutoTaggingAndResubscription(Object)} (Object, Class)}
   * for the group managed by this instance.
   */
  public void initializeAutoTaggingAndResubscription(Object target) {
    Preconditions.checkNotNull(target, "Target cannot be null");
    group.initializeAutoTaggingAndResubscription(target);
  }

  /**
   * Calls
   * {@link ObservableGroup#initializeAutoTaggingAndResubscriptionInTargetClassOnly(Object, Class)}
   * for the group managed by this instance.
   */
  public <T> void initializeAutoTaggingAndResubscriptionInTargetClassOnly(T target,
          Class<T> targetClass) {
   group.initializeAutoTaggingAndResubscriptionInTargetClassOnly(target, targetClass);
  }

  /**
   * Calls {@link ObservableGroup#cancelAllObservablesForObserver(Observer)} (Observer)}
   * for the group associated with this instance.
   */
  public void cancelAllObservablesForObserver(Observer<?> observer) {
    group.cancelAllObservablesForObserver(observer);
  }

  /**
   * Calls {@link ObservableGroup#cancelAndRemove(Observer, String)} for the group
   * associated with this instance.
   */
  public void cancelAndRemove(Observer<?> observer, String observableTag) {
    group.cancelAndRemove(observer, observableTag);
  }

  private void onDestroy(boolean isFinishing) {
    if (isFinishing) {
      observableManager.destroy(group);
    } else {
      group().removeNonResubscribableObservers();
      group.dispose();
    }
  }

  /** Call this method from your Activity or Fragment's onDestroy method */
  public void onDestroy(@Nullable Activity activity) {
    // We need to track whether the current Activity is finishing or not in order to decide if we
    // should destroy the ObservableGroup. If the Activity is not finishing, then we should not
    // destroy it, since we know that we're probably restoring state at some point and reattaching
    // to the existing ObservableGroup. If saveState() was not called, then it's likely that we're
    // being destroyed and are not ever coming back. However, this isn't perfect, especially
    // when using fragments in a ViewPager. We might want to allow users to explicitly destroy it
    // instead, in order to mitigate this issue.
    // Also in some situations it seems like an Activity can be recreated even though its
    // isFinishing() property is set to true. For this reason, we must also check
    // isChangingConfigurations() to make sure it's safe to destroy the group.
    onDestroy(!hasSavedState
        || (activity != null && activity.isFinishing() && !activity.isChangingConfigurations()));
  }

  /** Call this method from your Activity or Fragment's onDestroy method */
  public void onDestroy(Fragment fragment) {
    onDestroy(fragment.getActivity());
  }

  /** Call this method from your Activity or Fragment's onResume method */
  public void onResume() {
    hasSavedState = false;
    unlock();
  }

  /** Call this method from your Activity or Fragment's onPause method */
  public void onPause() {
    lock();
  }

  private void lock() {
    group.lock();
  }

  private void unlock() {
    group.unlock();
  }

  /** Call this method from your Activity or Fragment's onSaveInstanceState method */
  public void onSaveInstanceState(Bundle outState) {
    hasSavedState = true;
    outState.putParcelable(KEY_STATE, new State(observableManager.id(), group.id()));
  }

  static class State implements Parcelable {
    final UUID managerId;
    final long groupId;

    State(UUID managerId, long groupId) {
      this.managerId = managerId;
      this.groupId = groupId;
    }

    @Override public int describeContents() {
      return 0;
    }

    @Override public void writeToParcel(Parcel dest, int flags) {
      dest.writeSerializable(managerId);
      dest.writeLong(groupId);
    }

    public static final Parcelable.Creator<State> CREATOR = new Parcelable.Creator<State>() {
      @Override
      public State[] newArray(int size) {
        return new State[size];
      }

      @Override
      public State createFromParcel(Parcel source) {
        return new State((UUID) source.readSerializable(), source.readLong());
      }
    };
  }
}


/*
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package io.methvin.watchservice;

import java.nio.file.Path;
import java.nio.file.WatchEvent;
import java.nio.file.WatchEvent.Kind;
import java.nio.file.WatchKey;
import java.nio.file.Watchable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;

import io.methvin.watchservice.AbstractWatchService.Event;

import static java.nio.file.StandardWatchEventKinds.OVERFLOW;
import static java.util.Objects.requireNonNull;

/** Implementation of {@link WatchKey} for an {@link AbstractWatchService}. */
class AbstractWatchKey implements WatchKey {

  private static WatchEvent<Object> overflowEvent(int count) {
    return new Event<>(OVERFLOW, count, null);
  }

  private final AbstractWatchService watcher;
  private final Watchable watchable;
  private final Set<Kind<?>> subscribedTypes;

  private final AtomicReference<AbstractWatchKey.State> state = new AtomicReference<>(State.READY);
  private final AtomicBoolean valid = new AtomicBoolean(true);
  private final AtomicInteger overflow = new AtomicInteger();

  private final BlockingQueue<WatchEvent<?>> events;

  public AbstractWatchKey(
      AbstractWatchService watcher,
      Watchable watchable,
      Iterable<? extends WatchEvent.Kind<?>> subscribedTypes,
      int queueSize) {
    this.watcher = requireNonNull(watcher);
    this.watchable = watchable; // nullable for Watcher poison
    this.events = new ArrayBlockingQueue<>(queueSize);

    Set<Kind<?>> types = new HashSet<Kind<?>>();
    subscribedTypes.forEach(types::add);
    this.subscribedTypes = Collections.unmodifiableSet(types);
  }

  /** Gets the current state of this key, State.READY or SIGNALLED. */
  AbstractWatchKey.State state() {
    return state.get();
  }

  /** Gets whether or not this key is subscribed to the given type of event. */
  public boolean subscribesTo(WatchEvent.Kind<?> eventType) {
    return subscribedTypes.contains(eventType);
  }

  /**
   * Posts the given event to this key. After posting one or more events, {@link #signal()} must be
   * called to cause the key to be enqueued with the watch service.
   */
  public void post(WatchEvent<?> event) {
    if (!events.offer(event)) {
      overflow.incrementAndGet();
    }
  }

  /**
   * Sets the state to SIGNALLED and enqueues this key with the watcher if it was previously in the
   * READY state.
   */
  public void signal() {
    if (state.getAndSet(State.SIGNALLED) == State.READY) {
      watcher.enqueue(this);
    }
  }

  @Override
  public boolean isValid() {
    return watcher.isOpen() && valid.get();
  }

  @Override
  public List<WatchEvent<?>> pollEvents() {
    // note: it's correct to be able to retrieve more events from a key without calling reset()
    // reset() is ONLY for "returning" the key to the watch service to potentially be retrieved by
    // another thread when you're finished with it
    List<WatchEvent<?>> result = new ArrayList<>(events.size());
    events.drainTo(result);
    int overflowCount = overflow.getAndSet(0);
    if (overflowCount != 0) {
      result.add(overflowEvent(overflowCount));
    }
    return Collections.unmodifiableList(result);
  }

  @Override
  public boolean reset() {
    // calling reset() multiple times without polling events would cause key to be placed in
    // watcher queue multiple times, but not much that can be done about that
    if (isValid() && state.compareAndSet(State.SIGNALLED, State.READY)) {
      // requeue if events are pending
      if (!events.isEmpty()) {
        signal();
      }
    }

    return isValid();
  }

  @Override
  public void cancel() {
    valid.set(false);
    watcher.cancelled(this);
  }

  @Override
  public Watchable watchable() {
    return watchable;
  }

  enum State {
    READY,
    SIGNALLED
  }

  // WatchEvent not WatchEvent.Kind
  final void signalEvent(WatchEvent.Kind<Path> kind, Path context) {
    post(new Event<>(kind, 1, context));
    signal();
  }
}


/*
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.methvin.watcher;

import com.sun.nio.file.ExtendedWatchEventModifier;
import io.methvin.watcher.DirectoryChangeEvent.EventType;
import io.methvin.watcher.hashing.FileHasher;
import io.methvin.watcher.hashing.HashCode;
import io.methvin.watchservice.MacOSXListeningWatchService;
import io.methvin.watchservice.WatchablePath;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.nio.file.*;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.ForkJoinPool;

import static java.nio.file.LinkOption.NOFOLLOW_LINKS;
import static java.nio.file.StandardWatchEventKinds.*;

public class DirectoryWatcher {

  /**
   * A builder for a {@link DirectoryWatcher}. Use {@code DirectoryWatcher.builder()} to get a new
   * instance.
   */
  public static final class Builder {
    private List<Path> paths = Collections.emptyList();
    private DirectoryChangeListener listener = (event -> {});
    private Logger logger = null;
    private FileHasher fileHasher = FileHasher.DEFAULT_FILE_HASHER;
    private WatchService watchService = null;

    private Builder() {}

    /** Set multiple paths to watch. */
    public Builder paths(List<Path> paths) {
      this.paths = paths;
      return this;
    }

    /** Set a single path to watch. */
    public Builder path(Path path) {
      return paths(Collections.singletonList(path));
    }

    /** Set a listener that will be called when a directory change event occurs. */
    public Builder listener(DirectoryChangeListener listener) {
      this.listener = listener;
      return this;
    }

    /**
     * Set a {@link WatchService} implementation that will be used by the watcher.
     *
     *
By default, this detects your OS and either uses the native JVM watcher or the macOS
     * watcher.
     */
    public Builder watchService(WatchService watchService) {
      this.watchService = watchService;
      return this;
    }

    /**
     * Set a logger to be used by the watcher. This defaults to {@code
     * LoggerFactory.getLogger(DirectoryWatcher.class)}
     */
    public Builder logger(Logger logger) {
      this.logger = logger;
      return this;
    }

    /**
     * Defines whether file hashing should be used to catch duplicate events. Defaults to {@code
     * true}.
     */
    public Builder fileHashing(boolean enabled) {
      this.fileHasher = enabled ? FileHasher.DEFAULT_FILE_HASHER : null;
      return this;
    }

    /**
     * Defines the file hasher to be used by the watcher.
     *
     *
Note: will implicitly enable file hashing. Setting to null is equivalent to {@code
     * fileHashing(false)}
     */
    public Builder fileHasher(FileHasher fileHasher) {
      this.fileHasher = fileHasher;
      return this;
    }

    public DirectoryWatcher build() throws IOException {
      if (watchService == null) {
        osDefaultWatchService();
      }
      if (logger == null) {
        staticLogger();
      }
      return new DirectoryWatcher(paths, listener, watchService, fileHasher, logger);
    }

    private Builder osDefaultWatchService() throws IOException {
      boolean isMac = System.getProperty("os.name").toLowerCase().contains("mac");
      if (isMac) {
        return watchService(
            new MacOSXListeningWatchService(
                new MacOSXListeningWatchService.Config() {
                  @Override
                  public FileHasher fileHasher() {
                    return fileHasher;
                  }
                }));
      } else {
        return watchService(FileSystems.getDefault().newWatchService());
      }
    }

    private Builder staticLogger() {
      return logger(LoggerFactory.getLogger(DirectoryWatcher.class));
    }
  }

  /** Get a new builder for a {@link DirectoryWatcher}. */
  public static Builder builder() {
    return new Builder();
  }

  private final Logger logger;

  private final WatchService watchService;
  private final List<Path> paths;
  private final boolean isMac;
  private final DirectoryChangeListener listener;
  private final Map<Path, HashCode> pathHashes;
  private final Map<WatchKey, Path> keyRoots;

  // this is set to true/false depending on whether recursive watching is supported natively
  private Boolean fileTreeSupported = null;
  private FileHasher fileHasher;

  public DirectoryWatcher(
      List<Path> paths,
      DirectoryChangeListener listener,
      WatchService watchService,
      FileHasher fileHasher,
      Logger logger)
      throws IOException {
    this.paths = paths;
    this.listener = listener;
    this.watchService = watchService;
    this.isMac = watchService instanceof MacOSXListeningWatchService;
    this.pathHashes = PathUtils.createHashCodeMap(paths, fileHasher);
    this.keyRoots = PathUtils.createKeyRootsMap();
    this.fileHasher = fileHasher;
    this.logger = logger;

    for (Path path : paths) {
      registerAll(path);
    }
  }

  /**
   * Asynchronously watch the directories using {@code ForkJoinPool.commonPool()} as the executor
   */
  public CompletableFuture<Void> watchAsync() {
    return watchAsync(ForkJoinPool.commonPool());
  }

  /**
   * Asynchronously watch the directories.
   *
   * @param executor the executor to use to watch asynchronously
   */
  public CompletableFuture<Void> watchAsync(Executor executor) {
    return CompletableFuture.supplyAsync(
        () -> {
          watch();
          return null;
        },
        executor);
  }

  /**
   * Watch the directories. Block until either the listener stops watching or the DirectoryWatcher
   * is closed.
   */
  public void watch() {
    for (; ; ) {
      if (!listener.isWatching()) {
        return;
      }
      // wait for key to be signalled
      WatchKey key;
      try {
        key = watchService.take();
      } catch (InterruptedException x) {
        return;
      }
      for (WatchEvent<?> event : key.pollEvents()) {
        try {
          WatchEvent.Kind<?> kind = event.kind();
          // Context for directory entry event is the file name of entry
          WatchEvent<Path> ev = PathUtils.cast(event);
          int count = ev.count();
          Path eventPath = ev.context();
          if (!keyRoots.containsKey(key)) {
            throw new IllegalStateException(
                "WatchService returned key [" + key + "] but it was not found in keyRoots!");
          }
          Path childPath = eventPath == null ? null : keyRoots.get(key).resolve(eventPath);
          logger.debug("{} [{}]", kind, childPath);
          /*
           * If a directory is created, and we're watching recursively, then register it and its sub-directories.
           */
          if (kind == OVERFLOW) {
            listener.onEvent(new DirectoryChangeEvent(EventType.OVERFLOW, childPath, count));
          } else if (eventPath == null) {
            throw new IllegalStateException("WatchService returned a null path for " + kind.name());
          } else if (kind == ENTRY_CREATE) {
            if (Files.isDirectory(childPath, NOFOLLOW_LINKS)) {
              if (!Boolean.TRUE.equals(fileTreeSupported)) {
                registerAll(childPath);
              }
              /*
               * Our custom Mac service sends subdirectory changes but the Windows/Linux do not.
               * Walk the file tree to make sure we send create events for any files that were created.
               */
              if (!isMac) {
                PathUtils.recursiveVisitFiles(
                    childPath,
                    dir -> notifyCreateEvent(dir, count),
                    file -> notifyCreateEvent(file, count));
              }
            }
            notifyCreateEvent(childPath, count);
          } else if (kind == ENTRY_MODIFY) {
            if (fileHasher != null || Files.isDirectory(childPath)) {
              /*
               * Note that existingHash may be null due to the file being created before we start listening
               * It's important we don't discard the event in this case
               */
              HashCode existingHash = pathHashes.get(childPath);

              /*
               * newHash can be null when using File#delete() on windows - it generates MODIFY and DELETE in succession.
               * In this case the MODIFY event can be safely ignored
               */
              HashCode newHash = PathUtils.hash(fileHasher, childPath);

              if (newHash != null && !newHash.equals(existingHash)) {
                pathHashes.put(childPath, newHash);
                listener.onEvent(new DirectoryChangeEvent(EventType.MODIFY, childPath, count));
              } else if (newHash == null) {
                logger.debug(
                    "Failed to hash modified file [{}]. It may have been deleted.", childPath);
              }
            } else {
              listener.onEvent(new DirectoryChangeEvent(EventType.MODIFY, childPath, count));
            }
          } else if (kind == ENTRY_DELETE) {
            // we cannot tell if the deletion was on file or folder because path points nowhere
            // (file/folder was deleted)
            pathHashes.entrySet().removeIf(e -> e.getKey().startsWith(childPath));
            listener.onEvent(new DirectoryChangeEvent(EventType.DELETE, childPath, count));
          }
        } catch (Exception e) {
          logger.debug("DirectoryWatcher got an exception while watching!", e);
          listener.onException(e);
        }
      }
      boolean valid = key.reset();
      if (!valid) {
        logger.debug("WatchKey for [{}] no longer valid; removing.", key.watchable());
        // remove the key from the keyRoots
        keyRoots.remove(key);
        // if there are no more keys left to watch, we can break out
        if (keyRoots.isEmpty()) {
          logger.debug("No more directories left to watch; terminating watcher.");
          break;
        }
      }
    }
  }

  public DirectoryChangeListener getListener() {
    return listener;
  }

  public void close() throws IOException {
    watchService.close();
  }

  private void registerAll(final Path start) throws IOException {
    if (!Boolean.FALSE.equals(fileTreeSupported)) {
      // Try using FILE_TREE modifier since we aren't certain that it's unsupported
      try {
        register(start, true);
        // We didn't get an UnsupportedOperationException so assume FILE_TREE is supported
        fileTreeSupported = true;
      } catch (UnsupportedOperationException e) {
        // UnsupportedOperationException should only happen if FILE_TREE is unsupported
        logger.debug("Assuming ExtendedWatchEventModifier.FILE_TREE is not supported", e);
        fileTreeSupported = false;
        // If we failed to use the FILE_TREE modifier, try again without
        registerAll(start);
      }
    } else {
      // Since FILE_TREE is unsupported, register root directory and sub-directories
      PathUtils.recursiveVisitFiles(start, dir -> register(dir, false), file -> {});
    }
  }

  // Internal method to be used by registerAll
  private void register(Path directory, boolean useFileTreeModifier) throws IOException {
    logger.debug("Registering [{}].", directory);
    Watchable watchable = isMac ? new WatchablePath(directory) : directory;
    WatchEvent.Modifier[] modifiers =
        useFileTreeModifier
            ? new WatchEvent.Modifier[] {ExtendedWatchEventModifier.FILE_TREE}
            : new WatchEvent.Modifier[] {};
    WatchEvent.Kind<?>[] kinds =
        new WatchEvent.Kind<?>[] {ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY};
    WatchKey watchKey = watchable.register(watchService, kinds, modifiers);
    keyRoots.put(watchKey, directory);
  }

  private void notifyCreateEvent(Path path, int count) throws IOException {
    if (fileHasher != null || Files.isDirectory(path)) {
      HashCode newHash = PathUtils.hash(fileHasher, path);
      if (newHash == null) {
        logger.debug("Failed to hash created file [{}]. It may have been deleted.", path);
        return;
      }
      // Notify for the file create if not already notified
      if (!pathHashes.containsKey(path)) {
        logger.debug("{} [{}]", EventType.CREATE, path);
        listener.onEvent(new DirectoryChangeEvent(EventType.CREATE, path, count));
        pathHashes.put(path, newHash);
      }
    } else {
      logger.debug("{} [{}]", EventType.CREATE, path);
      listener.onEvent(new DirectoryChangeEvent(EventType.CREATE, path, count));
    }
  }
}


package org.codehaus.mojo.exec;

import java.io.IOException;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.InvocationTargetException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Properties;
import java.util.Set;

import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.factory.ArtifactFactory;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope;
import org.apache.maven.project.MavenProject;
import org.apache.maven.project.ProjectBuilder;
import org.apache.maven.project.ProjectBuildingRequest;
import org.apache.maven.shared.transfer.artifact.resolve.ArtifactResult;
import org.apache.maven.shared.transfer.dependencies.DefaultDependableCoordinate;
import org.apache.maven.shared.transfer.dependencies.resolve.DependencyResolver;

/**
 * Executes the supplied java class in the current VM with the enclosing project's dependencies as classpath.
 *
 * @author Kaare Nilsen (kaare.nilsen@gmail.com), David Smiley (dsmiley@mitre.org)
 * @since 1.0
 */
@Mojo( name = "java", threadSafe = true, requiresDependencyResolution = ResolutionScope.TEST )
public class ExecJavaMojo
    extends AbstractExecMojo
{
    @Component
    private DependencyResolver dependencyResolver;

    /**
     * @since 1.0
     */
    @Component
    private ProjectBuilder projectBuilder;

    /**
     * @since 1.1-beta-1
     */
    @Parameter( readonly = true, defaultValue = "${plugin.artifacts}" )
    private List<Artifact> pluginDependencies;

    /**
     * The main class to execute.

     * With Java 9 and above you can prefix it with the modulename, e.g. com.greetings/com.greetings.Main
     * Without modulename the classpath will be used, with modulename a new modulelayer will be created.
     *
     * @since 1.0
     */
    @Parameter( required = true, property = "exec.mainClass" )
    private String mainClass;

    /**
     * The class arguments.
     *
     * @since 1.0
     */
    @Parameter( property = "exec.arguments" )
    private String[] arguments;

    /**
     * A list of system properties to be passed. Note: as the execution is not forked, some system properties required
     * by the JVM cannot be passed here. Use MAVEN_OPTS or the exec:exec instead. See the user guide for more
     * information.
     *
     * @since 1.0
     */
    @Parameter
    private Property[] systemProperties;

    /**
     * Indicates if mojo should be kept running after the mainclass terminates. Use full for server like apps with
     * daemon threads.
     *
     * @deprecated since 1.1-alpha-1
     * @since 1.0
     */
    @Parameter( property = "exec.keepAlive", defaultValue = "false" )
    @Deprecated
    private boolean keepAlive;

    /**
     * Indicates if the project dependencies should be used when executing the main class.
     *
     * @since 1.1-beta-1
     */
    @Parameter( property = "exec.includeProjectDependencies", defaultValue = "true" )
    private boolean includeProjectDependencies;

    /**
     * Indicates if this plugin's dependencies should be used when executing the main class.
     *

     * This is useful when project dependencies are not appropriate. Using only the plugin dependencies can be
     * particularly useful when the project is not a java project. For example a mvn project using the csharp plugins
     * only expects to see dotnet libraries as dependencies.
     *
     * @since 1.1-beta-1
     */
    @Parameter( property = "exec.includePluginsDependencies", defaultValue = "false" )
    private boolean includePluginDependencies;

    /**
     * Whether to interrupt/join and possibly stop the daemon threads upon quitting.

     * If this is false, maven does nothing about the daemon threads. When maven has no more work to do,
     * the VM will normally terminate any remaining daemon threads.
     *

     * In certain cases (in particular if maven is embedded), you might need to keep this enabled to make sure threads
     * are properly cleaned up to ensure they don't interfere with subsequent activity. In that case, see
     * {@link #daemonThreadJoinTimeout} and {@link #stopUnresponsiveDaemonThreads} for further tuning.
     *

     *
     * @since 1.1-beta-1
     */
    @Parameter( property = "exec.cleanupDaemonThreads", defaultValue = "true" )
    private boolean cleanupDaemonThreads;

    /**
     * This defines the number of milliseconds to wait for daemon threads to quit following their interruption.

     * This is only taken into account if {@link #cleanupDaemonThreads} is true. A value <=0 means to
     * not timeout (i.e. wait indefinitely for threads to finish). Following a timeout, a warning will be logged.
     *

     * Note: properly coded threads should terminate upon interruption but some threads may prove problematic: as
     * the VM does interrupt daemon threads, some code may not have been written to handle interruption properly. For
     * example java.util.Timer is known to not handle interruptions in JDK <= 1.6. So it is not possible for us to
     * infinitely wait by default otherwise maven could hang. A sensible default value has been chosen, but this default
     * value may change in the future based on user feedback.
     *

     *
     * @since 1.1-beta-1
     */
    @Parameter( property = "exec.daemonThreadJoinTimeout", defaultValue = "15000" )
    private long daemonThreadJoinTimeout;

    /**
     * Wether to call {@link Thread#stop()} following a timing out of waiting for an interrupted thread to finish. This
     * is only taken into account if {@link #cleanupDaemonThreads} is true and the
     * {@link #daemonThreadJoinTimeout} threshold has been reached for an uncooperative thread. If this is
     * false, or if {@link Thread#stop()} fails to get the thread to stop, then a warning is logged and
     * Maven will continue on while the affected threads (and related objects in memory) linger on. Consider setting
     * this to true if you are invoking problematic code that you can't fix. An example is
     * {@link java.util.Timer} which doesn't respond to interruption. To have Timer fixed, vote for
     * this bug.
     *
     * @since 1.1-beta-1
     */
    @Parameter( property = "exec.stopUnresponsiveDaemonThreads", defaultValue = "false" )
    private boolean stopUnresponsiveDaemonThreads;

    /**
     * Deprecated this is not needed anymore.
     *
     * @deprecated since 1.1-alpha-1
     * @since 1.0
     */
    @Parameter( property = "exec.killAfter", defaultValue = "-1" )
    @Deprecated
    private long killAfter;

    private Properties originalSystemProperties;

    /**
     * Additional elements to be appended to the classpath.
     *
     * @since 1.3
     */
    @Parameter
    private List<String> additionalClasspathElements;

    /**
     * Execute goal.
     *
     * @throws MojoExecutionException execution of the main class or one of the threads it generated failed.
     * @throws MojoFailureException something bad happened...
     */
    public void execute()
        throws MojoExecutionException, MojoFailureException
    {
        if ( isSkip() )
        {
            getLog().info( "skipping execute as per configuration" );
            return;
        }
        if ( killAfter != -1 )
        {
            getLog().warn( "Warning: killAfter is now deprecated. Do you need it ? Please comment on MEXEC-6." );
        }

        if ( null == arguments )
        {
            arguments = new String[0];
        }

        if ( getLog().isDebugEnabled() )
        {
            StringBuffer msg = new StringBuffer( "Invoking : " );
            msg.append( mainClass );
            msg.append( ".main(" );
            for ( int i = 0; i < arguments.length; i++ )
            {
                if ( i > 0 )
                {
                    msg.append( ", " );
                }
                msg.append( arguments[i] );
            }
            msg.append( ")" );
            getLog().debug( msg );
        }

        IsolatedThreadGroup threadGroup = new IsolatedThreadGroup( mainClass /* name */ );
        Thread bootstrapThread = new Thread( threadGroup, new Runnable()
        {
            public void run()
            {
                int sepIndex = mainClass.indexOf( '/' );

                final String bootClassName;
                if ( sepIndex >= 0 )
                {
                    bootClassName = mainClass.substring( sepIndex + 1 );
                }
                else
                {
                    bootClassName = mainClass;
                }

                try
                {
                    Class<?> bootClass = Thread.currentThread().getContextClassLoader().loadClass( bootClassName );

                    MethodHandles.Lookup lookup = MethodHandles.lookup();

                    MethodHandle mainHandle =
                        lookup.findStatic( bootClass, "main",
                                                 MethodType.methodType( void.class, String[].class ) );

                    mainHandle.invoke( arguments );
                }
                catch ( IllegalAccessException e )
                { // just pass it on
                    Thread.currentThread().getThreadGroup().uncaughtException( Thread.currentThread(),
                                                                               new Exception( "The specified mainClass doesn't contain a main method with appropriate signature.",
                                                                                e ) );
                }
                catch ( InvocationTargetException e )
                { // use the cause if available to improve the plugin execution output
                   Throwable exceptionToReport = e.getCause() != null ? e.getCause() : e;
                   Thread.currentThread().getThreadGroup().uncaughtException( Thread.currentThread(), exceptionToReport );
                }
                catch ( Throwable e )
                { // just pass it on
                    Thread.currentThread().getThreadGroup().uncaughtException( Thread.currentThread(), e );
                }
            }
        }, mainClass + ".main()" );
        bootstrapThread.setContextClassLoader( getClassLoader() );
        setSystemProperties();

        bootstrapThread.start();
        joinNonDaemonThreads( threadGroup );
        // It's plausible that spontaneously a non-daemon thread might be created as we try and shut down,
        // but it's too late since the termination condition (only daemon threads) has been triggered.
        if ( keepAlive )
        {
            getLog().warn( "Warning: keepAlive is now deprecated and obsolete. Do you need it? Please comment on MEXEC-6." );
            waitFor( 0 );
        }

        if ( cleanupDaemonThreads )
        {

            terminateThreads( threadGroup );

            try
            {
                threadGroup.destroy();
            }
            catch ( IllegalThreadStateException e )
            {
                getLog().warn( "Couldn't destroy threadgroup " + threadGroup, e );
            }
        }

        if ( originalSystemProperties != null )
        {
            System.setProperties( originalSystemProperties );
        }

        synchronized ( threadGroup )
        {
            if ( threadGroup.uncaughtException != null )
            {
                throw new MojoExecutionException( "An exception occured while executing the Java class. "
                    + threadGroup.uncaughtException.getMessage(), threadGroup.uncaughtException );
            }
        }

        registerSourceRoots();
    }

    /**
     * a ThreadGroup to isolate execution and collect exceptions.
     */
    class IsolatedThreadGroup
        extends ThreadGroup
    {
        private Throwable uncaughtException; // synchronize access to this

        public IsolatedThreadGroup( String name )
        {
            super( name );
        }

        public void uncaughtException( Thread thread, Throwable throwable )
        {
            if ( throwable instanceof ThreadDeath )
            {
                return; // harmless
            }
            synchronized ( this )
            {
                if ( uncaughtException == null ) // only remember the first one
                {
                    uncaughtException = throwable; // will be reported eventually
                }
            }
            getLog().warn( throwable );
        }
    }

    private void joinNonDaemonThreads( ThreadGroup threadGroup )
    {
        boolean foundNonDaemon;
        do
        {
            foundNonDaemon = false;
            Collection<Thread> threads = getActiveThreads( threadGroup );
            for ( Thread thread : threads )
            {
                if ( thread.isDaemon() )
                {
                    continue;
                }
                foundNonDaemon = true; // try again; maybe more threads were created while we were busy
                joinThread( thread, 0 );
            }
        }
        while ( foundNonDaemon );
    }

    private void joinThread( Thread thread, long timeoutMsecs )
    {
        try
        {
            getLog().debug( "joining on thread " + thread );
            thread.join( timeoutMsecs );
        }
        catch ( InterruptedException e )
        {
            Thread.currentThread().interrupt(); // good practice if don't throw
            getLog().warn( "interrupted while joining against thread " + thread, e ); // not expected!
        }
        if ( thread.isAlive() ) // generally abnormal
        {
            getLog().warn( "thread " + thread + " was interrupted but is still alive after waiting at least "
                + timeoutMsecs + "msecs" );
        }
    }

    private void terminateThreads( ThreadGroup threadGroup )
    {
        long startTime = System.currentTimeMillis();
        Set<Thread> uncooperativeThreads = new HashSet<Thread>(); // these were not responsive to interruption
        for ( Collection<Thread> threads = getActiveThreads( threadGroup ); !threads.isEmpty(); threads =
            getActiveThreads( threadGroup ), threads.removeAll( uncooperativeThreads ) )
        {
            // Interrupt all threads we know about as of this instant (harmless if spuriously went dead (! isAlive())
            // or if something else interrupted it ( isInterrupted() ).
            for ( Thread thread : threads )
            {
                getLog().debug( "interrupting thread " + thread );
                thread.interrupt();
            }
            // Now join with a timeout and call stop() (assuming flags are set right)
            for ( Thread thread : threads )
            {
                if ( !thread.isAlive() )
                {
                    continue; // and, presumably it won't show up in getActiveThreads() next iteration
                }
                if ( daemonThreadJoinTimeout <= 0 )
                {
                    joinThread( thread, 0 ); // waits until not alive; no timeout
                    continue;
                }
                long timeout = daemonThreadJoinTimeout - ( System.currentTimeMillis() - startTime );
                if ( timeout > 0 )
                {
                    joinThread( thread, timeout );
                }
                if ( !thread.isAlive() )
                {
                    continue;
                }
                uncooperativeThreads.add( thread ); // ensure we don't process again
                if ( stopUnresponsiveDaemonThreads )
                {
                    getLog().warn( "thread " + thread + " will be Thread.stop()'ed" );
                    thread.stop();
                }
                else
                {
                    getLog().warn( "thread " + thread + " will linger despite being asked to die via interruption" );
                }
            }
        }
        if ( !uncooperativeThreads.isEmpty() )
        {
            getLog().warn( "NOTE: " + uncooperativeThreads.size() + " thread(s) did not finish despite being asked to "
                + " via interruption. This is not a problem with exec:java, it is a problem with the running code."
                + " Although not serious, it should be remedied." );
        }
        else
        {
            int activeCount = threadGroup.activeCount();
            if ( activeCount != 0 )
            {
                // TODO this may be nothing; continue on anyway; perhaps don't even log in future
                Thread[] threadsArray = new Thread[1];
                threadGroup.enumerate( threadsArray );
                getLog().debug( "strange; " + activeCount + " thread(s) still active in the group " + threadGroup
                    + " such as " + threadsArray[0] );
            }
        }
    }

    private Collection<Thread> getActiveThreads( ThreadGroup threadGroup )
    {
        Thread[] threads = new Thread[threadGroup.activeCount()];
        int numThreads = threadGroup.enumerate( threads );
        Collection<Thread> result = new ArrayList<Thread>( numThreads );
        for ( int i = 0; i < threads.length && threads[i] != null; i++ )
        {
            result.add( threads[i] );
        }
        return result; // note: result should be modifiable
    }

    /**
     * Pass any given system properties to the java system properties.
     */
    private void setSystemProperties()
    {
        if ( systemProperties != null )
        {
            originalSystemProperties = System.getProperties();
            for ( Property systemProperty : systemProperties )
            {
                String value = systemProperty.getValue();
                System.setProperty( systemProperty.getKey(), value == null ? "" : value );
            }
        }
    }

    /**
     * Set up a classloader for the execution of the main class.
     *
     * @return the classloader
     * @throws MojoExecutionException if a problem happens
     */
    private ClassLoader getClassLoader()
        throws MojoExecutionException
    {
        List<Path> classpathURLs = new ArrayList<>();
        this.addRelevantPluginDependenciesToClasspath( classpathURLs );
        this.addRelevantProjectDependenciesToClasspath( classpathURLs );
        this.addAdditionalClasspathElements( classpathURLs );

        try
        {
            return LoaderFinder.find( classpathURLs, mainClass );
        }
        catch ( NullPointerException | IOException e )
        {
            throw new MojoExecutionException( e.getMessage(), e );
        }

    }

    private void addAdditionalClasspathElements( List<Path> path )
    {
        if ( additionalClasspathElements != null )
        {
            for ( String classPathElement : additionalClasspathElements )
            {
                Path file = Paths.get( classPathElement );
                if ( !file.isAbsolute() )
                {
                    file = project.getBasedir().toPath().resolve( file );
                }
                getLog().debug( "Adding additional classpath element: " + file + " to classpath" );
                path.add( file );
            }
        }
    }

    /**
     * Add any relevant project dependencies to the classpath. Indirectly takes includePluginDependencies and
     * ExecutableDependency into consideration.
     *
     * @param path classpath of {@link java.net.URL} objects
     * @throws MojoExecutionException if a problem happens
     */
    private void addRelevantPluginDependenciesToClasspath( List<Path> path )
        throws MojoExecutionException
    {
        if ( hasCommandlineArgs() )
        {
            arguments = parseCommandlineArgs();
        }

        for ( Artifact classPathElement : this.determineRelevantPluginDependencies() )
        {
            getLog().debug( "Adding plugin dependency artifact: " + classPathElement.getArtifactId()
                + " to classpath" );
            path.add( classPathElement.getFile().toPath() );
        }
    }

    /**
     * Add any relevant project dependencies to the classpath. Takes includeProjectDependencies into consideration.
     *
     * @param path classpath of {@link java.net.URL} objects
     * @throws MojoExecutionException if a problem happens
     */
    private void addRelevantProjectDependenciesToClasspath( List<Path> path )
        throws MojoExecutionException
    {
        if ( this.includeProjectDependencies )
        {
            getLog().debug( "Project Dependencies will be included." );

            List<Artifact> artifacts = new ArrayList<>();
            List<Path> theClasspathFiles = new ArrayList<>();

            collectProjectArtifactsAndClasspath( artifacts, theClasspathFiles );

            for ( Path classpathFile : theClasspathFiles )
            {
                getLog().debug( "Adding to classpath : " + classpathFile );
                path.add( classpathFile );
            }

            for ( Artifact classPathElement : artifacts )
            {
                getLog().debug( "Adding project dependency artifact: " + classPathElement.getArtifactId()
                    + " to classpath" );
                path.add( classPathElement.getFile().toPath() );
            }
        }
        else
        {
            getLog().debug( "Project Dependencies will be excluded." );
        }

    }

    /**
     * Determine all plugin dependencies relevant to the executable. Takes includePlugins, and the executableDependency
     * into consideration.
     *
     * @return a set of Artifact objects. (Empty set is returned if there are no relevant plugin dependencies.)
     * @throws MojoExecutionException if a problem happens resolving the plufin dependencies
     */
    private Set<Artifact> determineRelevantPluginDependencies()
        throws MojoExecutionException
    {
        Set<Artifact> relevantDependencies;
        if ( this.includePluginDependencies )
        {
            if ( this.executableDependency == null )
            {
                getLog().debug( "All Plugin Dependencies will be included." );
                relevantDependencies = new HashSet<Artifact>( this.pluginDependencies );
            }
            else
            {
                getLog().debug( "Selected plugin Dependencies will be included." );
                Artifact executableArtifact = this.findExecutableArtifact();
                relevantDependencies = this.resolveExecutableDependencies( executableArtifact );
            }
        }
        else
        {
            relevantDependencies = Collections.emptySet();
            getLog().debug( "Plugin Dependencies will be excluded." );
        }
        return relevantDependencies;
    }

    /**
     * Resolve the executable dependencies for the specified project
     *
     * @param executablePomArtifact the project's POM
     * @return a set of Artifacts
     * @throws MojoExecutionException if a failure happens
     */
    private Set<Artifact> resolveExecutableDependencies( Artifact executablePomArtifact )
        throws MojoExecutionException
    {

        Set<Artifact> executableDependencies = new LinkedHashSet<>();
        try
        {
            ProjectBuildingRequest buildingRequest = getSession().getProjectBuildingRequest();

            MavenProject executableProject =
                this.projectBuilder.build( executablePomArtifact, buildingRequest ).getProject();

            for ( ArtifactResult artifactResult : dependencyResolver.resolveDependencies( buildingRequest, executableProject.getModel(), null ) )
            {
                executableDependencies.add( artifactResult.getArtifact() );
            }
        }
        catch ( Exception ex )
        {
            throw new MojoExecutionException( "Encountered problems resolving dependencies of the executable "
                + "in preparation for its execution.", ex );
        }

        return executableDependencies;
    }

    /**
     * Stop program execution for nn millis.
     *
     * @param millis the number of millis-seconds to wait for, 0 stops program forever.
     */
    private void waitFor( long millis )
    {
        Object lock = new Object();
        synchronized ( lock )
        {
            try
            {
                lock.wait( millis );
            }
            catch ( InterruptedException e )
            {
                Thread.currentThread().interrupt(); // good practice if don't throw
                getLog().warn( "Spuriously interrupted while waiting for " + millis + "ms", e );
            }
        }
    }

}


package org.codehaus.mojo.exec;

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

import org.apache.maven.artifact.Artifact;

/**
 *

 * ExecutableDependency class.
 *

 */
public class ExecutableDependency
{
    private String groupId;

    private String artifactId;

    /**
     *

     * Getter for the field groupId.
     *

     *
     * @return a {@link java.lang.String} object.
     */
    public String getGroupId()
    {
        return this.groupId;
    }

    /**
     *

     * Setter for the field groupId.
     *

     *
     * @param groupId a {@link java.lang.String} object.
     */
    public void setGroupId( String groupId )
    {
        this.groupId = groupId;
    }

    /**
     *

     * Getter for the field artifactId.
     *

     *
     * @return a {@link java.lang.String} object.
     */
    public String getArtifactId()
    {
        return this.artifactId;
    }

    /**
     *

     * Setter for the field artifactId.
     *

     *
     * @param artifactId a {@link java.lang.String} object.
     */
    public void setArtifactId( String artifactId )
    {
        this.artifactId = artifactId;
    }

    /**
     *

     * Matches the groupId and artifactId.
     *

     *
     * @param artifact a {@link org.apache.maven.artifact.Artifact} object.
     * @return true if both math, false otherwise.
     */
    public boolean matches( Artifact artifact )
    {
        return artifact.getGroupId().equals( this.getGroupId() )
            && artifact.getArtifactId().equals( this.getArtifactId() );
    }

    /** {@inheritDoc} */
    public String toString()
    {
        return this.groupId + ":" + this.artifactId;
    }

    /** {@inheritDoc} */
    public boolean equals( Object o )
    {
        if ( this == o )
        {
            return true;
        }
        if ( !( o instanceof ExecutableDependency ) )
        {
            return false;
        }

        final ExecutableDependency that = (ExecutableDependency) o;

        if ( artifactId != null ? !artifactId.equals( that.artifactId ) : that.artifactId != null )
        {
            return false;
        }
        if ( groupId != null ? !groupId.equals( that.groupId ) : that.groupId != null )
        {
            return false;
        }

        return true;
    }

    /** {@inheritDoc} */
    public int hashCode()
    {
        int result;
        result = ( groupId != null ? groupId.hashCode() : 0 );
        result = 29 * result + ( artifactId != null ? artifactId.hashCode() : 0 );
        return result;
    }
}


package org.codehaus.mojo.exec;

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

import java.io.File;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;

import org.apache.maven.artifact.Artifact;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.model.Resource;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.descriptor.PluginDescriptor;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import org.apache.maven.project.ProjectBuilder;
import org.apache.maven.shared.transfer.artifact.resolve.ArtifactResolver;
import org.codehaus.plexus.util.cli.CommandLineUtils;

/**
 * This class is used for unifying functionality between the 2 mojo exec plugins ('java' and 'exec'). It handles parsing
 * the arguments and adding source/test folders.
 *
 * @author Philippe Jacot (PJA)
 * @author Jerome Lacoste
 */
public abstract class AbstractExecMojo
    extends AbstractMojo
{
    /**
     * The enclosing project.
     */
    @Parameter( defaultValue = "${project}", readonly = true )
    protected MavenProject project;

    @Parameter( defaultValue = "${session}", readonly = true, required = true )
    private MavenSession session;

    @Component
    private ArtifactResolver artifactResolver;

    @Component
    private ProjectBuilder projectBuilder;

    @Parameter( defaultValue = "${plugin}", readonly = true ) // Maven 3 only
    private PluginDescriptor plugin;

    @Parameter( readonly = true, defaultValue = "${plugin.artifacts}" )
    private List<Artifact> pluginDependencies;

    /**
     * If provided the ExecutableDependency identifies which of the plugin dependencies contains the executable class.
     * This will have the affect of only including plugin dependencies required by the identified ExecutableDependency.
     *
     *

     * If includeProjectDependencies is set to true, all of the project dependencies will be included on
     * the executable's classpath. Whether a particular project dependency is a dependency of the identified
     * ExecutableDependency will be irrelevant to its inclusion in the classpath.
     *

     *
     * @since 1.1-beta-1
     */
    @Parameter
    protected ExecutableDependency executableDependency;

    /**
     * This folder is added to the list of those folders containing source to be compiled. Use this if your plugin
     * generates source code.
     */
    @Parameter( property = "sourceRoot" )
    private File sourceRoot;

    /**
     * This folder is added to the list of those folders containing source to be compiled for testing. Use this if your
     * plugin generates test source code.
     */
    @Parameter( property = "testSourceRoot" )
    private File testSourceRoot;

    /**
     * Arguments separated by space for the executed program. For example: "-j 20"
     */
    @Parameter( property = "exec.args" )
    private String commandlineArgs;

    /**
     * Defines the scope of the classpath passed to the plugin. Set to compile,test,runtime or system depending on your
     * needs. Since 1.1.2, the default value is 'runtime' instead of 'compile'.
     */
    @Parameter( property = "exec.classpathScope", defaultValue = "runtime" )
    protected String classpathScope;

    /**
     * Skip the execution. Starting with version 1.4.0 the former name skip has been changed into
     * exec.skip.
     *
     * @since 1.0.1
     */
    // TODO: Remove the alias in version 1.5.0 of the plugin.
    @Parameter( property = "exec.skip", defaultValue = "false", alias = "skip" )
    private boolean skip;

    /**
     * Add project resource directories to classpath. This is especially useful if the exec plugin is used for a code
     * generator that reads its settings from the classpath.
     *
     * @since 1.5.1
     */
    @Parameter( property = "addResourcesToClasspath", defaultValue = "false" )
    private boolean addResourcesToClasspath;

    /**
     * Add project output directory to classpath. This might be undesirable when the exec plugin is run before the
     * compile step. Default is true.
     *
     * @since 1.5.1
     */
    @Parameter( property = "addOutputToClasspath", defaultValue = "true" )
    private boolean addOutputToClasspath;

    /**
     * Collects the project artifacts in the specified List and the project specific classpath (build output and build
     * test output) Files in the specified List, depending on the plugin classpathScope value.
     *
     * @param artifacts the list where to collect the scope specific artifacts
     * @param theClasspathFiles the list where to collect the scope specific output directories
     */
    protected void collectProjectArtifactsAndClasspath( List<Artifact> artifacts, List<Path> theClasspathFiles )
    {
        if ( addResourcesToClasspath )
        {
            for ( Resource r : project.getBuild().getResources() )
            {
                theClasspathFiles.add( Paths.get( r.getDirectory() ) );
            }
        }

        if ( "compile".equals( classpathScope ) )
        {
            artifacts.addAll( project.getCompileArtifacts() );
            if ( addOutputToClasspath )
            {
                theClasspathFiles.add( Paths.get( project.getBuild().getOutputDirectory() ) );
            }
        }
        else if ( "test".equals( classpathScope ) )
        {
            artifacts.addAll( project.getTestArtifacts() );
            if ( addOutputToClasspath )
            {
                theClasspathFiles.add( Paths.get( project.getBuild().getTestOutputDirectory() ) );
                theClasspathFiles.add( Paths.get( project.getBuild().getOutputDirectory() ) );
            }
        }
        else if ( "runtime".equals( classpathScope ) )
        {
            artifacts.addAll( project.getRuntimeArtifacts() );
            if ( addOutputToClasspath )
            {
                theClasspathFiles.add( Paths.get( project.getBuild().getOutputDirectory() ) );
            }
        }
        else if ( "system".equals( classpathScope ) )
        {
            artifacts.addAll( project.getSystemArtifacts() );
        }
        else
        {
            throw new IllegalStateException( "Invalid classpath scope: " + classpathScope );
        }

        getLog().debug( "Collected project artifacts " + artifacts );
        getLog().debug( "Collected project classpath " + theClasspathFiles );
    }

    /**
     * Parses the argument string given by the user. Strings are recognized as everything between STRING_WRAPPER.
     * PARAMETER_DELIMITER is ignored inside a string. STRING_WRAPPER and PARAMETER_DELIMITER can be escaped using
     * ESCAPE_CHAR.
     *
     * @return Array of String representing the arguments
     * @throws MojoExecutionException for wrong formatted arguments
     */
    protected String[] parseCommandlineArgs()
        throws MojoExecutionException
    {
        if ( commandlineArgs == null )
        {
            return null;
        }
        else
        {
            try
            {
                return CommandLineUtils.translateCommandline( commandlineArgs );
            }
            catch ( Exception e )
            {
                throw new MojoExecutionException( e.getMessage() );
            }
        }
    }

    /**
     * @return true of the mojo has command line arguments
     */
    protected boolean hasCommandlineArgs()
    {
        return ( commandlineArgs != null );
    }

    /**
     * Register compile and compile tests source roots if necessary
     */
    protected void registerSourceRoots()
    {
        if ( sourceRoot != null )
        {
            getLog().info( "Registering compile source root " + sourceRoot );
            project.addCompileSourceRoot( sourceRoot.toString() );
        }

        if ( testSourceRoot != null )
        {
            getLog().info( "Registering compile test source root " + testSourceRoot );
            project.addTestCompileSourceRoot( testSourceRoot.toString() );
        }
    }

    /**
     * Check if the execution should be skipped
     *
     * @return true to skip
     */
    protected boolean isSkip()
    {
        return skip;
    }

    protected final MavenSession getSession()
    {
        return session;
    }

    /**
     * Examine the plugin dependencies to find the executable artifact.
     *
     * @return an artifact which refers to the actual executable tool (not a POM)
     * @throws MojoExecutionException if no executable artifact was found
     */
    protected Artifact findExecutableArtifact()
        throws MojoExecutionException
    {
        // ILimitedArtifactIdentifier execToolAssembly = this.getExecutableToolAssembly();

        Artifact executableTool = null;
        for ( Artifact pluginDep : this.pluginDependencies )
        {
            if ( this.executableDependency.matches( pluginDep ) )
            {
                executableTool = pluginDep;
                break;
            }
        }

        if ( executableTool == null )
        {
            throw new MojoExecutionException( "No dependency of the plugin matches the specified executableDependency."
                + "  Specified executableToolAssembly is: " + executableDependency.toString() );
        }

        return executableTool;
    }
}