@JsonIgnore
/** Creates an id for this def, composed of 'name:version' */
public static String createId(String name, String version) {
    return name + SEPARATOR + (version == null ? "null" : version);
} public static String getName(String id) {
    return id.split(SEPARATOR)[0];
} public static String getVersion(String id) {
    return id.split(SEPARATOR)[1];
} public static final String LOCAL_REPO_PATH = RUNTIME_DIR_PATH
        + "local_repo/"; public static void validateArgument(boolean expression, String errorMessage)
        throws SherlokException {
    if (!expression) {
        throw new SherlokException(errorMessage);
    }
} public static <T> T validateNotNull(T reference, String variableName)
        throws SherlokException {
    if (reference == null) {
        throw new SherlokException(variableName + " should not be null");
    }
    return reference;
} public static <E> ArrayList<E> list() {
    return new ArrayList<E>();
} public static <K, V> HashMap<K, V> map() {
    return new HashMap<K, V>();
} public static <E> HashSet<E> set() {
    return new HashSet<E>();
} @JsonPropertyOrder(value = { "name", "version", "description", "domain",
        "dependencies", "repositories", "engines", "config" }, alphabetic = true)
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
public class BundleDef extends Def {

    /** a list of all library dependencies. */
    private List<BundleDependency> dependencies = list();

    /** additional maven repositories. */
    private Map<String, String> repositories = map();

    private List<EngineDef> engines = list();

    /** A Maven dependency to some external UIMA code. */
    @JsonInclude(JsonInclude.Include.NON_DEFAULT)
    public static class BundleDependency {

        /** Dependencies can only have these formats. */
        public enum DependencyType {
            /** Default value, corresponds to a released maven artifact. */
            mvn, //
            /** any accessible git repository that contains a Maven project. */
            git, // TODO xLATER git protocol not implemented
            /** corresponds to a local or remote jar. */
            jar // TODO xLATER jar protocol not implemented
        }

        private DependencyType type = DependencyType.mvn;
        /** Format: {group id}:{artifact id}:{version} */
        private String value;

        public BundleDependency() {
        }

        public BundleDependency(DependencyType type, String value) {
            this.type = type;
            this.value = value;
        }

        public DependencyType getType() {
            return type;
        }

        public void setType(DependencyType type) {
            this.type = type;
        }

        public String getValue() {
            return value;
        }

        public void setValue(String value) {
            this.value = value;
        }

        @JsonIgnore
        public String getGroupId() {
            return value.split(SEPARATOR)[0];
        }

        @JsonIgnore
        public String getArtifactId() {
            return value.split(SEPARATOR)[1];
        }

        @JsonIgnore
        public String getVersion() {
            return value.split(SEPARATOR)[2];
        }

        @JsonIgnore
        public int hashCode() {
            return value.hashCode();
        }

        @JsonIgnore
        public boolean equals(Object obj) {
            if (obj instanceof BundleDependency) {
                BundleDependency o = (BundleDependency) obj;
                if (o.value.equals(value) && o.type == type) {
                    return true;
                }
            }
            return false;
        }

        @JsonIgnore
        public String toString() {
            return value + " (" + type + ")";
        };

        /** @see {@link DefaultModelValidator} */
        private static final Pattern VALIDATE_ID = compile("[A-Za-z0-9_\\-.]+");

        @JsonIgnore
        public void validate() throws SherlokException {

            validateArgument(isLetterOrDigit(value.charAt(0)),
                    "Dependency value should start with a letter or a digit, but was '"
                            + value + "'", "", "");
            validateArgument(value.split(SEPARATOR).length == 3,
                    "Dependency value should contain exactly 3 columns (':'), but was '"
                            + value + "'", "", "");
            validateArgument(VALIDATE_ID.matcher(getGroupId()).matches(),
                    "Dependency '" + value + "'has an invalid group id '"
                            + getGroupId() + "', allowed characters are "
                            + VALIDATE_ID.toString(), "", "");
            validateArgument(VALIDATE_ID.matcher(getArtifactId()).matches(),
                    "Dependency '" + value + "'has an invalid artifact id '"
                            + getArtifactId() + "', allowed characters are "
                            + VALIDATE_ID.toString(), "", "");
            validateArgument(VALIDATE_ID.matcher(getVersion()).matches(),
                    "Dependency '" + value + "' has an invalid version id '"
                            + getVersion() + "', allowed characters are "
                            + VALIDATE_ID.toString(), "", "");
        }
    }

    @JsonPropertyOrder({ "name", "class", "description", "parameters" })
    @JsonInclude(JsonInclude.Include.NON_DEFAULT)
    public static class EngineDef {

        /**
         * an optional unique name for this bundle. Letters, numbers and
         * underscore only
         */
        private String name,

        /** (optional) */
        description;

        /** the Java UIMA class name of this engine. */
        @JsonProperty("class")
        private String classz;

        /** UIMA parameters. To overwrite default parameters */
        // TODO serialize without [ ], by creating custom
        // serializer, see Def.LineSerializer
        private Map<String, List<String>> parameters = map();

        /** TRANSITIVE (JsonIgnore), dynamically set by the bundle. */
        @JsonIgnore
        private BundleDef bundle;

        // get/set

        /** @return the engine name. Falls back on {@link #classz's simple name}. */
        public String getName() {
            if (name != null) {
                return name;
            } else if (classz != null) {
                if (classz.contains(".")) {
                    return classz.substring(classz.lastIndexOf(".") + 1,
                            classz.length());
                } else { // no dot in class name --> just return it
                    return classz;
                }
            }
            return null;
        }

        public EngineDef setName(String name) {
            this.name = name;
            return this;
        }

        public String getDescription() {
            return description;
        }

        public EngineDef setDescription(String description) {
            this.description = description;
            return this;
        }

        public String getClassz() {
            return classz;
        }

        public EngineDef setClassz(String classz) {
            this.classz = classz;
            return this;
        }

        public Map<String, List<String>> getParameters() {
            return parameters;
        }

        public EngineDef setParameters(Map<String, List<String>> parameters) {
            this.parameters = parameters;
            return this;
        }

        public EngineDef addParameter(String key, List<String> value) {
            this.parameters.put(key, value);
            return this;
        }

        public List<String> getParameter(String key) {
            return this.parameters.get(key);
        }

        public boolean validate() throws SherlokException {
            checkOnlyAlphanumDotUnderscore(name,
                    "EngineDef '" + this.toString() + "'' id");
            return true;
        }

        public BundleDef getBundle() {
            return bundle;
        }

        /** Is set at load time by {@link Controller#_load()}. */
        public EngineDef setBundle(final BundleDef bundle) {
            this.bundle = bundle;
            return this;
        }

        /** Needs bundle to be set (see {@link #setBundle()}). */
        @JsonIgnore
        public String getId() {
            return createId(name, bundle.getVersion());
        }

        /** @return the id without non-alphanumeric, separated by separator */
        public String getIdForDescriptor(final String separator) {
            return getName().replaceAll("[^A-Za-z0-9]", "_") + separator
                    + bundle.getVersion().replaceAll("[^A-Za-z0-9]", "_");
        }

        @Override
        public String toString() {
            return name;
        }
    }

    public List<BundleDependency> getDependencies() {
        return dependencies;
    }

    public BundleDef setDependencies(List<BundleDependency> dependencies) {
        this.dependencies = dependencies;
        return this;
    }

    public BundleDef addDependency(BundleDependency dependency) {
        this.dependencies.add(dependency);
        return this;
    }

    public Map<String, String> getRepositories() {
        return repositories;
    }

    public BundleDef setRepositories(Map<String, String> repositories) {
        this.repositories = repositories;
        return this;
    }

    public BundleDef addRepository(String id, String url) {
        this.repositories.put(id, url);
        return this;
    }

    public List<EngineDef> getEngines() {
        return engines;
    }

    public BundleDef setEngines(List<EngineDef> engines) {
        this.engines = engines;
        return this;
    }

    public BundleDef addEngine(EngineDef engine) {
        this.engines.add(engine);
        return this;
    }

    public void validate(String context) throws SherlokException {
        try {
            super.validate();
            for (BundleDependency bd : getDependencies()) {
                bd.validate();
            }
            for (EngineDef e : getEngines()) {
                e.validate();
            }

            // TODO validate usage of variables in engines and make sure no
            // unknown variable are used
        } catch (SherlokException e) {
            throw e.setObject(this.toString()).setWhen(context);
        }
    }
} @JsonPropertyOrder({ "name", "class", "description", "parameters" })
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
public static class EngineDef {

    /**
     * an optional unique name for this bundle. Letters, numbers and
     * underscore only
     */
    private String name,

    /** (optional) */
    description;

    /** the Java UIMA class name of this engine. */
    @JsonProperty("class")
    private String classz;

    /** UIMA parameters. To overwrite default parameters */
    // TODO serialize without [ ], by creating custom
    // serializer, see Def.LineSerializer
    private Map<String, List<String>> parameters = map();

    /** TRANSITIVE (JsonIgnore), dynamically set by the bundle. */
    @JsonIgnore
    private BundleDef bundle;

    // get/set

    /** @return the engine name. Falls back on {@link #classz's simple name}. */
    public String getName() {
        if (name != null) {
            return name;
        } else if (classz != null) {
            if (classz.contains(".")) {
                return classz.substring(classz.lastIndexOf(".") + 1,
                        classz.length());
            } else { // no dot in class name --> just return it
                return classz;
            }
        }
        return null;
    }

    public EngineDef setName(String name) {
        this.name = name;
        return this;
    }

    public String getDescription() {
        return description;
    }

    public EngineDef setDescription(String description) {
        this.description = description;
        return this;
    }

    public String getClassz() {
        return classz;
    }

    public EngineDef setClassz(String classz) {
        this.classz = classz;
        return this;
    }

    public Map<String, List<String>> getParameters() {
        return parameters;
    }

    public EngineDef setParameters(Map<String, List<String>> parameters) {
        this.parameters = parameters;
        return this;
    }

    public EngineDef addParameter(String key, List<String> value) {
        this.parameters.put(key, value);
        return this;
    }

    public List<String> getParameter(String key) {
        return this.parameters.get(key);
    }

    public boolean validate() throws SherlokException {
        checkOnlyAlphanumDotUnderscore(name,
                "EngineDef '" + this.toString() + "'' id");
        return true;
    }

    public BundleDef getBundle() {
        return bundle;
    }

    /** Is set at load time by {@link Controller#_load()}. */
    public EngineDef setBundle(final BundleDef bundle) {
        this.bundle = bundle;
        return this;
    }

    /** Needs bundle to be set (see {@link #setBundle()}). */
    @JsonIgnore
    public String getId() {
        return createId(name, bundle.getVersion());
    }

    /** @return the id without non-alphanumeric, separated by separator */
    public String getIdForDescriptor(final String separator) {
        return getName().replaceAll("[^A-Za-z0-9]", "_") + separator
                + bundle.getVersion().replaceAll("[^A-Za-z0-9]", "_");
    }

    @Override
    public String toString() {
        return name;
    }
} @JsonPropertyOrder(value = { "name", "version", "description", "language",
        "domain", "loadOnStartup", "scriptLines", "config", "output", "tests" }, alphabetic = true)
@JsonInclude(NON_DEFAULT)
public class PipelineDef extends Def {

    /** Which language this pipeline works for (ISO code). Defaults to 'en' */
    private String language = "en";

    /** The list of engine definitions */
    @JsonProperty("script")
    @JsonSerialize(using = ListSerializer.class)
    private List<String> scriptLines = list();

    /** Controls the output of this pipeline */
    private PipelineOutput output = new PipelineOutput();

    /** Embedded (integration) tests */
    @JsonInclude(ALWAYS)
    private List<PipelineTest> tests = list();

    /** Defines what kind of annotation this pipeline outputs */
    @JsonInclude(JsonInclude.Include.NON_DEFAULT)
    public static class PipelineOutput {

        @JsonProperty("filter_annotations")
        private List<String> annotationFilters = list();

        @JsonProperty("include_annotations")
        private List<String> annotationIncludes = list();

        public List<String> getAnnotationFilters() {
            return annotationFilters;
        }

        public PipelineOutput setAnnotationFilters(
                List<String> annotationFilters) {
            this.annotationFilters = annotationFilters;
            return this;
        }

        public List<String> getAnnotationIncludes() {
            return annotationIncludes;
        }

        public PipelineOutput setAnnotationIncludes(
                List<String> annotationIncludes) {
            this.annotationIncludes = annotationIncludes;
            return this;
        }
    }

    /**
     * Tests one single input string against a list of expected
     * {@link JsonAnnotation}s
     */
    @JsonIgnoreProperties(ignoreUnknown = true)
    @JsonInclude(NON_DEFAULT)
    @JsonPropertyOrder(value = { "comment", "input", "expected", "comparison" }, alphabetic = true)
    public static class PipelineTest {

        public enum Comparison {
            /** all expected {@link JsonAnnotation}s are present in system */
            atLeast,
            /** expected and system {@link JsonAnnotation}s are exactly equals */
            exact;
        }

        private String comment;
        private String input;
        private Map<String, List<JsonAnnotation>> expected = map();
        private Comparison comparison = Comparison.atLeast; // default

        public String getComment() {
            return comment;
        }

        public PipelineTest setComment(String comment) {
            this.comment = comment;
            return this;
        }

        public String getInput() {
            return input;
        }

        public PipelineTest setInput(String input) {
            this.input = input;
            return this;
        }

        public Map<String, List<JsonAnnotation>> getExpected() {
            return expected;
        }

        public PipelineTest setExpected(
                Map<String, List<JsonAnnotation>> expected) {
            this.expected = expected;
            return this;
        }

        public Comparison getComparison() {
            return comparison;
        }

        public void setComparison(Comparison comparison) {
            this.comparison = comparison;
        }

        @Override
        public String toString() {
            return input + "::" + expected;
        }

    }

    // Get/Set ////////////////////////////////////////////////////////////////

    public String getLanguage() {
        return language;
    }

    public PipelineDef setLanguage(String language) {
        this.language = language;
        return this;
    }

    public List<String> getScriptLines() {
        return scriptLines;
    }

    public PipelineDef setScriptLines(List<String> scriptLines) {
        this.scriptLines = scriptLines;
        return this;
    }

    public PipelineDef addScriptLine(String scriptLine) {
        this.scriptLines.add(scriptLine);
        return this;
    }

    public PipelineOutput getOutput() {
        return output;
    }

    public PipelineDef setOutput(PipelineOutput output) {
        this.output = output;
        return this;
    }

    public List<PipelineTest> getTests() {
        return tests;
    }

    public PipelineDef setTests(List<PipelineTest> tests) {
        this.tests = tests;
        return this;
    }

    public PipelineDef addTest(PipelineTest test) {
        this.tests.add(test);
        return this;
    }

    // Utilities //////////////////////////////////////////////////////////////

    public void validate(String context) throws SherlokException {
        try {
            super.validate();
            validateArgument(language != null, "'language' can not be null");
            validateArgument(language.length() > 0,
                    "'language' can not be empty");
            validateDomain(domain);

            // output
            validateArgument(
                    !(!output.annotationFilters.isEmpty() && !output.annotationIncludes
                            .isEmpty()),
                    "either provide 'filter_annotations' or 'include_annotations', but not both for pipeline '"
                            + getId() + "'");

            String typeMsg = "It should be a list of valid identifiers, separated by dots, "
                    + "and optionally ending with '.*' to match all subsequent types. "
                    + "Identifiers must be composed of letters, numbers, the underscore"
                    + " _ and the dollar sign $. Identifiers may only begin with a letter, "
                    + "the underscore or a dollar sign.";
            for (String type : output.annotationFilters) {
                validateTypeIdentifier(type, "output annotation filter '"
                        + type + "' is not valid." + typeMsg);
            }
            for (String type : output.annotationIncludes) {
                validateTypeIdentifier(type, "output annotation includes '"
                        + type + "' is not valid." + typeMsg);
            }
            if (tests.isEmpty()) {
                LOG.warn("no tests for pipeline '{}'", getId());
            }

            // TODO validate usage of variables in script and make sure no
            // unknown variable is used
        } catch (SherlokException e) {
            throw e.setObject(this.toString()).setWhen(context);
        }
    }

    /**
     * Ensures that this pipeline only uses engines defined in
     * {@link Controller}'s engines
     * 
     * @param engineIds
     *            the engine ids of the controller
     */
    public void validateEngines(Set<String> engineIds) throws SherlokException {
        for (String engineFromScript : getEnginesFromScript()) {
            if (!engineIds.contains(engineFromScript)) {
                throw new SherlokException("engine declared in pipeline '"
                        + getId() + "' but not found", engineFromScript);
            }
        }
    }

    /**
     * Extract all {@link EngineDef}s from theses script lines, by scanning for
     * lines starting with "ENGINE"
     */
    @JsonIgnore
    public List<String> getEnginesFromScript() {
        return getEnginesFromScript(getScriptLines());
    }

    /**
     * Extract all {@link EngineDef}s from theses script lines, by scanning for
     * lines starting with "ENGINE"
     */
    public static List<String> getEnginesFromScript(List<String> scriptLines) {
        List<String> engineIds = list();
        for (String scriptLine : scriptLines) {
            if (scriptLine.startsWith("ENGINE")) {
                String pengineId = scriptLine.trim()
                        .substring("ENGINE".length()).trim()
                        .replaceAll(";", "");
                engineIds.add(pengineId);
            }
        }
        return engineIds;
    }

    /** Adds newlines between entries */
    @SuppressWarnings("rawtypes")
    public static class ListSerializer extends JsonSerializer<List> {

        @Override
        public boolean isEmpty(final List value) {
            return value.isEmpty();
        }

        @Override
        public Class<List> handledType() {
            return List.class;
        }

        @Override
        public void serialize(final List value, final JsonGenerator jgen,
                final SerializerProvider provider) throws IOException,
                JsonProcessingException {
            jgen.writeStartArray();
            for (Object item : value) {
                jgen.writeRaw('\n');
                jgen.writeObject(item);
            }
            jgen.writeRaw('\n');
            jgen.writeEndArray();
        }
    }

} @JsonPropertyOrder(value = { "when", "message", "object", "remedy", "details" })
@JsonIgnoreProperties(value = { "stackTrace", "localizedMessage" })
@JsonInclude(NON_DEFAULT)
public class SherlokException extends Exception {
    private static final long serialVersionUID = 2101833257950070947L;

    public SherlokException() {
    }

    public SherlokException(String message) {
        this._message = message;
    }

    public SherlokException(String message, String object) {
        this._message = message;
        this.object = object;
    }

    /** In which circumstances did something go wrong? */
    @JsonProperty("when")
    private String when;

    /** What went wrong? */
    @JsonProperty("message")
    private String _message;

    /** More details about what went wrong? */
    private Object details;

    /** What was the object causing the error? */
    private String object;

    /** Suggestion on how to fix the error, if available */
    private String remedy;
    /** The REST route, if available */
    private String route;

    public String getWhen() {
        return when;
    }

    public SherlokException setWhen(String when) {
        this.when = when;
        return this;
    }

    public String getMessage() {
        return _message;
    }

    public SherlokException setMessage(String message) {
        this._message = message;
        return this;
    }

    public Object getDetails() {
        return details;
    }

    public SherlokException setDetails(String details) {
        this.details = details;
        return this;
    }

    public SherlokException setDetails(StackTraceElement[] elements) {

        StringBuilder msg = new StringBuilder();
        for (int i = 0, n = elements.length; i < n && n < 5; i++) {
            msg.append(elements[i].getFileName() + ":"
                    + elements[i].getLineNumber() + ">> "
                    + elements[i].getMethodName() + "(); ");
        }
        setDetails(msg.toString().trim());
        return this;
    }

    /**
     * @param detailsObj
     *            must be JSON seralizable
     */
    public SherlokException setDetails(Object detailsObj) {
        this.details = detailsObj;
        return this;
    }

    public String getObject() {
        return object;
    }

    public SherlokException setObject(String object) {
        this.object = object;
        return this;
    }

    public String getRemedy() {
        return remedy;
    }

    public SherlokException setRemedy(String remedy) {
        this.remedy = remedy;
        return this;
    }

    public SherlokException setRoute(String route) {
        this.route = route;
        return this;
    }

    public String getRoute() {
        return route;
    }

    /** To display errors nicely as JSON */
    public Object toJson() {
        return this;
    }

    @Override
    public String toString() {
        return (getMessage() != null ? getMessage() : "") //
                + (getObject() != null ? " ON OBJECT " + getObject() : "")//
                + (getWhen() != null ? " WHEN " + getWhen() : "")//
                + (getRoute() != null ? " and ROUTE " + getRoute() : "")//
                + (getDetails() != null ? "; DETAILS: " + getDetails() : "")//
                + (getRemedy() != null ? "; REMEDY: " + getRemedy() : "");
    }
} public class AetherResolver {
    private static final Logger LOG = getLogger(AetherResolver.class);

    /** Directory where to store the local artifacts */
    public static final String LOCAL_REPO_PATH = RUNTIME_DIR_PATH
            + "local_repo/";

    public static RepositorySystem newRepositorySystem() {
        DefaultServiceLocator locator = MavenRepositorySystemUtils
                .newServiceLocator();
        locator.addService(RepositoryConnectorFactory.class,
                BasicRepositoryConnectorFactory.class);
        locator.addService(TransporterFactory.class,
                FileTransporterFactory.class);
        locator.addService(TransporterFactory.class,
                HttpTransporterFactory.class);

        locator.setErrorHandler(new DefaultServiceLocator.ErrorHandler() {
            @Override
            public void serviceCreationFailed(Class<?> type, Class<?> impl,
                    Throwable exception) {
                LOG.error("Aether serviceCreation failed", exception);
            }
        });

        return locator.getService(RepositorySystem.class);
    }

    public static DefaultRepositorySystemSession newRepositorySystemSession(
            RepositorySystem system, String localRepoPath) {
        DefaultRepositorySystemSession session = MavenRepositorySystemUtils
                .newSession();

        LocalRepository localRepo = new LocalRepository(localRepoPath);
        session.setLocalRepositoryManager(system.newLocalRepositoryManager(
                session, localRepo));

        session.setTransferListener(new ConsoleTransferListener());
        // session.setRepositoryListener(new ConsoleRepositoryListener());
        // uncomment to generate dirty trees
        // session.setDependencyGraphTransformer( null );
        return session;
    }

    // local repo (added as remote)
    public static File localRepo = new File(System.getProperty("user.home")
            + "/.m2/repository/");

    public static List<RemoteRepository> newRepositories(
            RepositorySystem system, RepositorySystemSession session,
            Map<String, String> otherRepos) throws MalformedURLException {

        List<RemoteRepository> repos = list();
        if (localRepo.exists()) {
            repos.add(new RemoteRepository.Builder("local_default", "default",
                    localRepo.toURI().toURL().toString()).build());
        }
        // Maven central
        repos.add(new RemoteRepository.Builder("central", "default",
                "http://central.maven.org/maven2/").build());

        for (Entry<String, String> id_url : otherRepos.entrySet()) {
            repos.add(new RemoteRepository.Builder(id_url.getKey(), "default",
                    id_url.getValue()).build());
        }
        return repos;
    }

    /** A dependency visitor that logs the graph */
    public static class ConsoleDependencyGraphDumper implements
            DependencyVisitor {

        private String currentIndent = "";

        public boolean visitEnter(DependencyNode node) {
            LOG.trace(currentIndent + node);
            if (currentIndent.length() <= 0) {
                currentIndent = "+- ";
            } else {
                currentIndent = "|  " + currentIndent;
            }
            return true;
        }

        public boolean visitLeave(DependencyNode node) {
            currentIndent = currentIndent.substring(3, currentIndent.length());
            return true;
        }
    }
} public class MavenPom {
    private static final Logger LOG = getLogger(MavenPom.class);

    public static class RepoDefDTO {
        public String id, url;

        public RepoDefDTO(String id, String url) {
            this.id = id;
            this.url = url;
        }

        public String getId() {
            return id;
        }

        public String getUrl() {
            return url;
        }

        @Override
        public boolean equals(Object obj) {
            if (obj instanceof RepoDefDTO && ((RepoDefDTO) obj).id.equals(id)) {
                return true;
            }
            return false;
        }

        @Override
        public int hashCode() {
            return id.hashCode();
        }
    }

    /**
     * Generates and writes a Maven pom.xml in local_repo
     * 
     * @param bundleDefs
     * @param pipelineName
     * @param version
     * @return pom id for aether
     */
    public static String writePom(Set<BundleDef> bundleDefs,
            String pipelineName, String version) throws IOException,
            TemplateException {

        // Freemarker configuration object
        Configuration cfg = new Configuration();
        cfg.setClassForTemplateLoading(MavenPom.class, "/");

        Template template = cfg.getTemplate("fakePom.ftl");

        // Bind variables
        Map<String, Object> data = new HashMap<String, Object>();

        data.put("now", System.currentTimeMillis());
        data.put("pipelineName", pipelineName);
        data.put("pipelineVersion", version);

        List<BundleDependency> deps = list();
        for (BundleDef bundleDef : bundleDefs) {
            deps.addAll(bundleDef.getDependencies());
        }
        data.put("deps", deps);

        // order matters, so that sherlok github repo can be inserted first
        Set<RepoDefDTO> repoDefs = new LinkedHashSet<RepoDefDTO>();
        for (BundleDef bundleDef : bundleDefs) {
            for (Entry<String, String> id_url : bundleDef.getRepositories()
                    .entrySet()) {
                RepoDefDTO rd = new RepoDefDTO(id_url.getKey(),
                        id_url.getValue());
                if (repoDefs.add(rd))
                    LOG.trace("adding repo id '{}', url '{}' to pom", rd.id,
                            rd.url);
            }
        }
        data.put("repos", repoDefs);

        // File output
        File dir = new File(AetherResolver.LOCAL_REPO_PATH + "/org/sherlok/"
                + pipelineName + "/" + version);
        dir.mkdirs();

        File pomFile = new File(dir, pipelineName + "-" + version + ".pom");
        Writer writer = new FileWriter(pomFile);
        template.process(data, writer);
        writer.flush();
        writer.close();
        LOG.trace("pomFile written to '{}'", pomFile.getAbsolutePath());

        return "org.sherlok:" + pipelineName + ":pom:" + version;
    }
} public final class Strings {

    /**
     * This is a utility class (static methods only), don't instantiate.
     */
    private Strings() {
    }

    /**
     * <p>
     * Compares two strings using the current locale's rules and comparing
     * contained numbers based on their numeric values.
     * </p>
     * <p>
     * This is probably the best default comparison to use.
     * </p>
     * <p>
     * If you know that the texts to be compared are in a certain language that
     * differs from the default locale's langage, then get a collator for the
     * desired locale ({@link java.text.Collator#getInstance(java.util.Locale)})
     * and pass it to
     * {@link #compareNatural(java.text.Collator, String, String)}
     * </p>
     *
     * @param s
     *            first string
     * @param t
     *            second string
     * @return zero iff <code>s</code> and <code>t</code> are equal, a value
     *         less than zero iff <code>s</code> lexicographically precedes
     *         <code>t</code> and a value larger than zero iff <code>s</code>
     *         lexicographically follows <code>t</code>
     */
    public static int compareNatural(String s, String t) {
        return compareNatural(s, t, false, Collator.getInstance());
    }

    /**
     * @param s
     *            first string
     * @param t
     *            second string
     * @param caseSensitive
     *            treat characters differing in case only as equal - will be
     *            ignored if a collator is given
     * @param collator
     *            used to compare subwords that aren't numbers - if null,
     *            characters will be compared individually based on their
     *            Unicode value
     * @return zero iff <code>s</code> and <code>t</code> are equal, a value
     *         less than zero iff <code>s</code> lexicographically precedes
     *         <code>t</code> and a value larger than zero iff <code>s</code>
     *         lexicographically follows <code>t</code>
     */
    private static int compareNatural(String s, String t,
            boolean caseSensitive, Collator collator) {
        int sIndex = 0;
        int tIndex = 0;

        int sLength = s.length();
        int tLength = t.length();

        while (true) {
            // both character indices are after a subword (or at zero)

            // Check if one string is at end
            if (sIndex == sLength && tIndex == tLength) {
                return 0;
            }
            if (sIndex == sLength) {
                return -1;
            }
            if (tIndex == tLength) {
                return 1;
            }

            // Compare sub word
            char sChar = s.charAt(sIndex);
            char tChar = t.charAt(tIndex);

            boolean sCharIsDigit = Character.isDigit(sChar);
            boolean tCharIsDigit = Character.isDigit(tChar);

            if (sCharIsDigit && tCharIsDigit) {
                // Compare numbers

                // skip leading 0s
                int sLeadingZeroCount = 0;
                while (sChar == '0') {
                    ++sLeadingZeroCount;
                    ++sIndex;
                    if (sIndex == sLength) {
                        break;
                    }
                    sChar = s.charAt(sIndex);
                }
                int tLeadingZeroCount = 0;
                while (tChar == '0') {
                    ++tLeadingZeroCount;
                    ++tIndex;
                    if (tIndex == tLength) {
                        break;
                    }
                    tChar = t.charAt(tIndex);
                }
                boolean sAllZero = sIndex == sLength
                        || !Character.isDigit(sChar);
                boolean tAllZero = tIndex == tLength
                        || !Character.isDigit(tChar);
                if (sAllZero && tAllZero) {
                    continue;
                }
                if (sAllZero && !tAllZero) {
                    return -1;
                }
                if (tAllZero) {
                    return 1;
                }

                int diff = 0;
                do {
                    if (diff == 0) {
                        diff = sChar - tChar;
                    }
                    ++sIndex;
                    ++tIndex;
                    if (sIndex == sLength && tIndex == tLength) {
                        return diff != 0 ? diff : sLeadingZeroCount
                                - tLeadingZeroCount;
                    }
                    if (sIndex == sLength) {
                        if (diff == 0) {
                            return -1;
                        }
                        return Character.isDigit(t.charAt(tIndex)) ? -1 : diff;
                    }
                    if (tIndex == tLength) {
                        if (diff == 0) {
                            return 1;
                        }
                        return Character.isDigit(s.charAt(sIndex)) ? 1 : diff;
                    }
                    sChar = s.charAt(sIndex);
                    tChar = t.charAt(tIndex);
                    sCharIsDigit = Character.isDigit(sChar);
                    tCharIsDigit = Character.isDigit(tChar);
                    if (!sCharIsDigit && !tCharIsDigit) {
                        // both number sub words have the same length
                        if (diff != 0) {
                            return diff;
                        }
                        break;
                    }
                    if (!sCharIsDigit) {
                        return -1;
                    }
                    if (!tCharIsDigit) {
                        return 1;
                    }
                } while (true);
            } else {
                // Compare words
                if (collator != null) {
                    // To use the collator the whole subwords have to be
                    // compared - character-by-character comparision
                    // is not possible. So find the two subwords first
                    int aw = sIndex;
                    int bw = tIndex;
                    do {
                        ++sIndex;
                    } while (sIndex < sLength
                            && !Character.isDigit(s.charAt(sIndex)));
                    do {
                        ++tIndex;
                    } while (tIndex < tLength
                            && !Character.isDigit(t.charAt(tIndex)));

                    String as = s.substring(aw, sIndex);
                    String bs = t.substring(bw, tIndex);
                    int subwordResult = collator.compare(as, bs);
                    if (subwordResult != 0) {
                        return subwordResult;
                    }
                } else {
                    // No collator specified. All characters should be ascii
                    // only. Compare character-by-character.
                    do {
                        if (sChar != tChar) {
                            if (caseSensitive) {
                                return sChar - tChar;
                            }
                            sChar = Character.toUpperCase(sChar);
                            tChar = Character.toUpperCase(tChar);
                            if (sChar != tChar) {
                                sChar = Character.toLowerCase(sChar);
                                tChar = Character.toLowerCase(tChar);
                                if (sChar != tChar) {
                                    return sChar - tChar;
                                }
                            }
                        }
                        ++sIndex;
                        ++tIndex;
                        if (sIndex == sLength && tIndex == tLength) {
                            return 0;
                        }
                        if (sIndex == sLength) {
                            return -1;
                        }
                        if (tIndex == tLength) {
                            return 1;
                        }
                        sChar = s.charAt(sIndex);
                        tChar = t.charAt(tIndex);
                        sCharIsDigit = Character.isDigit(sChar);
                        tCharIsDigit = Character.isDigit(tChar);
                    } while (!sCharIsDigit && !tCharIsDigit);
                }
            }
        }
    }
}
import static org.apache.commons.io.FileUtils.copyFile;
import static org.apache.commons.io.FilenameUtils.getExtension;
import static org.apache.commons.lang3.StringUtils.join;
import static org.sherlok.mappings.Def.createId;
import static org.sherlok.mappings.Def.getName;
import static org.sherlok.mappings.Def.getVersion;
import static org.sherlok.utils.AetherResolver.LOCAL_REPO_PATH;
import static org.sherlok.utils.CheckThat.validateArgument;
import static org.sherlok.utils.CheckThat.validateNotNull;
import static org.sherlok.utils.Create.list;
import static org.sherlok.utils.Create.map;
import static org.sherlok.utils.Create.set;
import static org.slf4j.LoggerFactory.getLogger;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.uima.UIMAException;
import org.apache.uima.ruta.engine.RutaEngine;
import org.apache.uima.ruta.extensions.RutaParseRuntimeException;
import org.eclipse.aether.RepositorySystem;
import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.artifact.Artifact;
import org.eclipse.aether.artifact.DefaultArtifact;
import org.eclipse.aether.collection.CollectRequest;
import org.eclipse.aether.collection.CollectResult;
import org.eclipse.aether.collection.DependencyCollectionException;
import org.eclipse.aether.graph.Dependency;
import org.eclipse.aether.repository.RemoteRepository;
import org.eclipse.aether.resolution.ArtifactRequest;
import org.eclipse.aether.resolution.ArtifactResolutionException;
import org.eclipse.aether.util.graph.visitor.PreorderNodeListGenerator;
import org.sherlok.mappings.BundleDef;
import org.sherlok.mappings.BundleDef.EngineDef;
import org.sherlok.mappings.PipelineDef;
import org.sherlok.mappings.SherlokException;
import org.sherlok.utils.AetherResolver;
import org.sherlok.utils.MavenPom;
import org.sherlok.utils.Strings;
import org.slf4j.Logger;
import freemarker.template.TemplateException;
/**
 * Copyright (C) 2014-2015 Renaud Richardet
 *
 * 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 org.sherlok;





/**
 * Resolves, loads and caches {@link UimaPipeline}s.
 * 
 * @author renaud@apache.org
 */
public class PipelineLoader {
    private static final Logger LOG = getLogger(PipelineLoader.class);

    private final Controller controller;
    /** caches resolved {@link UimaPipeline}s so can be reused for annotating */
    private Map<String, UimaPipeline> uimaPipelinesCache = map();

    public PipelineLoader(Controller controller) {
        this.controller = controller;
    }

    /**
     * Checks if such a {@link PipelineDef} exists, and returns the
     * corresponding {@link UimaPipeline} (if it is in cache) or else
     * instantiates it. <br/>
     * Instantiation involves resolving the pipeline engines and the engines
     * bundles, then loading the bundles Maven artifacts into the current
     * classpath. <br/>
     * This method is synchronized, so that no two threads can access it during
     * initialization. TODO release the lock earlier
     * 
     * @param pipelineName
     * @param version
     *            the version id, or 'null' / {@link null} to try to fallback on
     *            the 'highest' version (see
     *            {@link Strings#compareNatural(String, String)}
     * @return the {@link UimaPipeline}
     */
    public synchronized UimaPipeline resolvePipeline(String pipelineName,
            final String originalVersion) throws SherlokException {

        String version = originalVersion;
        // 0. resolve version (fallback) if version=null
        if (version == null || version.equals("null")) {
            // init with a lexicographically lowsest value
            String highestVersion = "0000000000000000000000000000000";
            // find highest version
            for (String pId : controller.listPipelineDefNames()) {
                if (getName(pId).equals(pipelineName)) {
                    String pVersion = getVersion(pId);