org.dkpro.core
dkpro-core-io-conll-asl
diff --git a/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v1/ExternalRecommenderFactory.java b/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v1/ExternalRecommenderFactory.java
index 53a1855f9be..eb00cba3ff1 100644
--- a/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v1/ExternalRecommenderFactory.java
+++ b/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v1/ExternalRecommenderFactory.java
@@ -32,7 +32,7 @@
/**
*
* This class is exposed as a Spring Component via
- * {@link ExternalRecommenderAutoConfiguration#externalRecommenderFactory}.
+ * {@link ExternalRecommenderAutoConfiguration#externalRecommenderFactoryV1}.
*
*/
public class ExternalRecommenderFactory
@@ -65,7 +65,7 @@ public RecommendationEngine build(Recommender aRecommender)
@Override
public String getName()
{
- return "Remote classifier";
+ return "Remote classifier V1";
}
@Override
diff --git a/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v1/config/ExternalRecommenderAutoConfiguration.java b/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v1/config/ExternalRecommenderAutoConfiguration.java
index 9a11506314e..ba7a472ea45 100644
--- a/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v1/config/ExternalRecommenderAutoConfiguration.java
+++ b/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v1/config/ExternalRecommenderAutoConfiguration.java
@@ -25,12 +25,13 @@
import de.tudarmstadt.ukp.inception.recommendation.imls.external.v1.ExternalRecommenderFactory;
@Configuration
-@ConditionalOnProperty(prefix = "recommender.external", name = "enabled", havingValue = "true", matchIfMissing = true)
+@ConditionalOnProperty(prefix = "recommender.external.v1", name = "enabled", havingValue = "true", //
+ matchIfMissing = true)
@EnableConfigurationProperties(ExternalRecommenderPropertiesImpl.class)
public class ExternalRecommenderAutoConfiguration
{
@Bean
- public ExternalRecommenderFactory externalRecommenderFactory(
+ public ExternalRecommenderFactory externalRecommenderFactoryV1(
ExternalRecommenderProperties aProperties)
{
return new ExternalRecommenderFactory(aProperties);
diff --git a/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v2/ExternalRecommender.java b/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v2/ExternalRecommender.java
new file mode 100644
index 00000000000..2d70f16ea34
--- /dev/null
+++ b/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v2/ExternalRecommender.java
@@ -0,0 +1,260 @@
+/*
+ * Licensed to the Technische Universität Darmstadt under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The Technische Universität Darmstadt
+ * 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.
+ *
+ * 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 de.tudarmstadt.ukp.inception.recommendation.imls.external.v2;
+
+import static de.tudarmstadt.ukp.inception.recommendation.api.recommender.TrainingCapability.TRAINING_NOT_SUPPORTED;
+import static de.tudarmstadt.ukp.inception.recommendation.api.recommender.TrainingCapability.TRAINING_REQUIRED;
+import static de.tudarmstadt.ukp.inception.support.uima.WebAnnoCasUtil.getDocumentTitle;
+import static de.tudarmstadt.ukp.inception.support.uima.WebAnnoCasUtil.getRealCas;
+
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.uima.cas.CAS;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import de.tudarmstadt.ukp.clarin.webanno.model.Project;
+import de.tudarmstadt.ukp.clarin.webanno.security.model.User;
+import de.tudarmstadt.ukp.inception.annotation.storage.CasMetadataUtils;
+import de.tudarmstadt.ukp.inception.recommendation.api.evaluation.DataSplitter;
+import de.tudarmstadt.ukp.inception.recommendation.api.evaluation.EvaluationResult;
+import de.tudarmstadt.ukp.inception.recommendation.api.model.Recommender;
+import de.tudarmstadt.ukp.inception.recommendation.api.recommender.PredictionContext;
+import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommendationEngine;
+import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommendationException;
+import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommenderContext;
+import de.tudarmstadt.ukp.inception.recommendation.api.recommender.TrainingCapability;
+import de.tudarmstadt.ukp.inception.recommendation.imls.external.v2.api.Document;
+import de.tudarmstadt.ukp.inception.recommendation.imls.external.v2.api.DocumentList;
+import de.tudarmstadt.ukp.inception.recommendation.imls.external.v2.api.ExternalRecommenderApiException;
+import de.tudarmstadt.ukp.inception.recommendation.imls.external.v2.api.ExternalRecommenderV2Api;
+import de.tudarmstadt.ukp.inception.recommendation.imls.external.v2.api.FormatConverter;
+import de.tudarmstadt.ukp.inception.recommendation.imls.external.v2.config.ExternalRecommenderProperties;
+import de.tudarmstadt.ukp.inception.rendering.model.Range;
+
+public class ExternalRecommender
+ extends RecommendationEngine
+{
+ private final static Logger LOG = LoggerFactory.getLogger(ExternalRecommender.class);
+
+ public static final RecommenderContext.Key KEY_TRAINING_COMPLETE = new RecommenderContext.Key<>(
+ "training_complete");
+
+ private final ExternalRecommenderProperties properties;
+ private final ExternalRecommenderTraits traits;
+ private final ExternalRecommenderV2Api api;
+
+ public ExternalRecommender(ExternalRecommenderProperties aProperties, Recommender aRecommender,
+ ExternalRecommenderTraits aTraits)
+ {
+ super(aRecommender);
+
+ properties = aProperties;
+ traits = aTraits;
+ api = new ExternalRecommenderV2Api(URI.create(traits.getRemoteUrl()),
+ properties.getConnectTimeout());
+ }
+
+ @Override
+ public void train(RecommenderContext aContext, List aCasses) throws RecommendationException
+ {
+ if (aContext.getUser().isEmpty()) {
+ LOG.warn("No user found in context, skipping training...");
+ return;
+ }
+
+ User user = aContext.getUser().get();
+ Project project = recommender.getProject();
+ String datasetName = buildDatasetName(project, user);
+ String classifierName = buildClassifierName();
+ String modelName = buildModelName(project, user);
+
+ api.createDataset(datasetName);
+ synchronizeDocuments(aCasses, datasetName);
+ api.trainOnDataset(classifierName, modelName, datasetName);
+
+ aContext.put(KEY_TRAINING_COMPLETE, true);
+ }
+
+ private void synchronizeDocuments(List aCasses, String aDatasetName)
+ throws RecommendationException
+ {
+ LOG.debug("Synchronizing documents for [{}]", aDatasetName);
+
+ // Get info about remote documents
+ DocumentList documentList = api.listDocumentsInDataset(aDatasetName);
+ Map remoteVersions = new HashMap<>();
+ if (documentList.getNames().size() != documentList.getVersions().size()) {
+ throw new RecommendationException(
+ "Names and versions in document list have unequal size");
+ }
+
+ for (int i = 0; i < documentList.getNames().size(); i++) {
+ remoteVersions.put(documentList.getNames().get(i), documentList.getVersions().get(i));
+ }
+
+ // Sync documents
+ List documentsToSend = new ArrayList<>();
+ Set seenLocalDocuments = new HashSet<>();
+
+ for (CAS cas : aCasses) {
+ String documentName = getDocumentName(cas);
+ seenLocalDocuments.add(documentName);
+
+ // If the document is not known to the remote server, then we need to update it
+ if (!remoteVersions.containsKey(documentName)) {
+ documentsToSend.add(cas);
+ continue;
+ }
+
+ // If the version is unclear or our local version is newer, then we need to update it
+ long localVersion = getVersion(cas);
+ long remoteVersion = remoteVersions.getOrDefault(documentName, -1L);
+ if (localVersion == -1L || remoteVersion == -1L || localVersion > remoteVersion) {
+ documentsToSend.add(cas);
+ continue;
+ }
+ }
+
+ // All documents that have not been seen locally but are listed remotely need to be deleted
+ Set documentsToDelete = remoteVersions.keySet();
+ documentsToDelete.removeAll(seenLocalDocuments);
+
+ for (String name : documentsToDelete) {
+ api.deleteDocumentFromDataset(aDatasetName, name);
+ }
+
+ LOG.debug("Deleted [{}] documents", documentsToDelete.size());
+
+ // Finally, send new/updated documents
+ FormatConverter converter = new FormatConverter();
+ for (CAS cas : documentsToSend) {
+ var documentName = getDocumentName(cas);
+ var version = getVersion(cas);
+ var document = converter.documentFromCas(cas, recommender.getLayer().getName(),
+ recommender.getFeature().getName(), version);
+
+ api.addDocumentToDataset(aDatasetName, documentName, document);
+ }
+
+ LOG.debug("Sent [{}] documents", documentsToSend.size());
+ }
+
+ @Override
+ public Range predict(PredictionContext aContext, CAS aCas, int aBegin, int aEnd)
+ throws RecommendationException
+ {
+ CAS cas = getRealCas(aCas);
+
+ if (aContext.getUser().isEmpty()) {
+ LOG.warn("No user found in context, skipping predictions...");
+ return Range.UNDEFINED;
+ }
+
+ long version = 0;
+
+ var converter = new FormatConverter();
+ var layerName = recommender.getLayer().getName();
+ var featureName = recommender.getFeature().getName();
+
+ var request = converter.documentFromCas(cas, layerName, featureName, version);
+
+ var modelName = buildModelName(recommender.getProject(), aContext.getUser().get());
+ var classifierName = traits.getClassifierInfo().getName();
+
+ try {
+ Document response = api.predict(classifierName, modelName, request);
+ converter.loadIntoCas(response, layerName, featureName, cas);
+ }
+ catch (ExternalRecommenderApiException e) {
+ LOG.error("Could not obtain predictions, skipping...");
+ }
+
+ return new Range(aBegin, aEnd);
+ }
+
+ @Override
+ public EvaluationResult evaluate(List aCasses, DataSplitter aDataSplitter)
+ throws RecommendationException
+ {
+ return null;
+ }
+
+ @Override
+ public boolean isReadyForPrediction(RecommenderContext aContext)
+ {
+ if (traits.isTrainable()) {
+ return aContext.get(KEY_TRAINING_COMPLETE).orElse(false);
+ }
+ else {
+ return true;
+ }
+ }
+
+ @Override
+ public int estimateSampleCount(List aCasses)
+ {
+ return 0;
+ }
+
+ @Override
+ public TrainingCapability getTrainingCapability()
+ {
+ if (traits.isTrainable()) {
+ //
+ // return TRAINING_SUPPORTED;
+ // We need to get at least one training CAS because we need to extract the type system
+ return TRAINING_REQUIRED;
+ }
+ else {
+ return TRAINING_NOT_SUPPORTED;
+ }
+ }
+
+ private String buildClassifierName()
+ {
+ return traits.getClassifierInfo().getName();
+ }
+
+ private String buildModelName(Project aProject, User aUser)
+ {
+ return aProject.getId() + "_" + aUser.getUsername() + "_" + recommender.getId();
+ }
+
+ private String buildDatasetName(Project aProject, User aUser)
+ {
+ return aProject.getId() + "_" + aUser.getUsername();
+ }
+
+ private String getDocumentName(CAS aCas)
+ {
+ return CasMetadataUtils.getSourceDocumentName(aCas).orElse(getDocumentTitle(aCas));
+ }
+
+ private long getVersion(CAS aCas)
+ {
+ return CasMetadataUtils.getLastChanged(aCas);
+ }
+
+}
diff --git a/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v2/ExternalRecommenderFactory.java b/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v2/ExternalRecommenderFactory.java
new file mode 100644
index 00000000000..82be1cdd813
--- /dev/null
+++ b/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v2/ExternalRecommenderFactory.java
@@ -0,0 +1,110 @@
+/*
+ * Licensed to the Technische Universität Darmstadt under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The Technische Universität Darmstadt
+ * 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.
+ *
+ * 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 de.tudarmstadt.ukp.inception.recommendation.imls.external.v2;
+
+import org.apache.wicket.model.IModel;
+
+import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationFeature;
+import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer;
+import de.tudarmstadt.ukp.inception.annotation.layer.relation.RelationLayerSupport;
+import de.tudarmstadt.ukp.inception.annotation.layer.span.SpanLayerSupport;
+import de.tudarmstadt.ukp.inception.recommendation.api.model.Recommender;
+import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommendationEngine;
+import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommendationEngineFactoryImplBase;
+import de.tudarmstadt.ukp.inception.recommendation.imls.external.v2.config.ExternalRecommenderAutoConfiguration;
+import de.tudarmstadt.ukp.inception.recommendation.imls.external.v2.config.ExternalRecommenderProperties;
+
+/**
+ *
+ * This class is exposed as a Spring Component via
+ * {@link ExternalRecommenderAutoConfiguration#externalRecommenderFactoryV2}.
+ *
+ */
+public class ExternalRecommenderFactory
+ extends RecommendationEngineFactoryImplBase
+{
+ // This is a string literal so we can rename/refactor the class without it changing its ID
+ // and without the database starting to refer to non-existing recommendation tools.
+ public static final String ID = "de.tudarmstadt.ukp.inception.recommendation.imls.external.v2.ExternalClassificationTool";
+
+ private final ExternalRecommenderProperties properties;
+
+ public ExternalRecommenderFactory(ExternalRecommenderProperties aProperties)
+ {
+ properties = aProperties;
+ }
+
+ @Override
+ public String getId()
+ {
+ return ID;
+ }
+
+ @Override
+ public RecommendationEngine build(Recommender aRecommender)
+ {
+ ExternalRecommenderTraits traits = readTraits(aRecommender);
+ return new ExternalRecommender(properties, aRecommender, traits);
+ }
+
+ @Override
+ public String getName()
+ {
+ return "Remote classifier V2";
+ }
+
+ @Override
+ public boolean accepts(AnnotationFeature aFeature)
+ {
+ if (aFeature == null) {
+ return false;
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean accepts(AnnotationLayer aLayer)
+ {
+ if (aLayer == null) {
+ return false;
+ }
+
+ return SpanLayerSupport.TYPE.equals(aLayer.getType())
+ || RelationLayerSupport.TYPE.equals(aLayer.getType());
+ }
+
+ @Override
+ public ExternalRecommenderTraitsEditor createTraitsEditor(String aId,
+ IModel aModel)
+ {
+ return new ExternalRecommenderTraitsEditor(aId, aModel);
+ }
+
+ @Override
+ public ExternalRecommenderTraits createTraits()
+ {
+ return new ExternalRecommenderTraits();
+ }
+
+ @Override
+ public boolean isEvaluable()
+ {
+ return false;
+ }
+}
diff --git a/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v2/ExternalRecommenderTraits.java b/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v2/ExternalRecommenderTraits.java
new file mode 100644
index 00000000000..72be1fa2c04
--- /dev/null
+++ b/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v2/ExternalRecommenderTraits.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Technische Universität Darmstadt under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The Technische Universität Darmstadt
+ * 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.
+ *
+ * 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 de.tudarmstadt.ukp.inception.recommendation.imls.external.v2;
+
+import java.io.Serializable;
+
+import de.tudarmstadt.ukp.inception.recommendation.imls.external.v2.api.ClassifierInfo;
+
+public class ExternalRecommenderTraits
+ implements Serializable
+{
+ private static final long serialVersionUID = -3109239605741337123L;
+
+ private String remoteUrl = "http://localhost:8000";
+ private ClassifierInfo classifierInfo;
+ private boolean trainable;
+
+ public String getRemoteUrl()
+ {
+ return remoteUrl;
+ }
+
+ public void setRemoteUrl(String aRemoteUrl)
+ {
+ remoteUrl = aRemoteUrl;
+ }
+
+ public ClassifierInfo getClassifierInfo()
+ {
+ return classifierInfo;
+ }
+
+ public void setClassifierInfo(ClassifierInfo aClassifierInfo)
+ {
+ classifierInfo = aClassifierInfo;
+ }
+
+ public boolean isTrainable()
+ {
+ return trainable;
+ }
+
+ public void setTrainable(boolean aTrainable)
+ {
+ trainable = aTrainable;
+ }
+}
diff --git a/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v2/ExternalRecommenderTraitsEditor.html b/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v2/ExternalRecommenderTraitsEditor.html
new file mode 100644
index 00000000000..68339718f81
--- /dev/null
+++ b/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v2/ExternalRecommenderTraitsEditor.html
@@ -0,0 +1,69 @@
+
+
+
+
+
+
diff --git a/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v2/ExternalRecommenderTraitsEditor.java b/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v2/ExternalRecommenderTraitsEditor.java
new file mode 100644
index 00000000000..ba85c964742
--- /dev/null
+++ b/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v2/ExternalRecommenderTraitsEditor.java
@@ -0,0 +1,163 @@
+/*
+ * Licensed to the Technische Universität Darmstadt under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The Technische Universität Darmstadt
+ * 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.
+ *
+ * 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 de.tudarmstadt.ukp.inception.recommendation.imls.external.v2;
+
+import static de.tudarmstadt.ukp.inception.support.lambda.LambdaBehavior.visibleWhen;
+import static org.apache.commons.lang3.StringUtils.isNotEmpty;
+
+import java.net.URI;
+import java.util.Collections;
+
+import org.apache.wicket.AttributeModifier;
+import org.apache.wicket.ajax.AjaxRequestTarget;
+import org.apache.wicket.feedback.IFeedback;
+import org.apache.wicket.markup.html.form.CheckBox;
+import org.apache.wicket.markup.html.form.DropDownChoice;
+import org.apache.wicket.markup.html.form.Form;
+import org.apache.wicket.markup.html.form.LambdaChoiceRenderer;
+import org.apache.wicket.markup.html.form.TextField;
+import org.apache.wicket.model.CompoundPropertyModel;
+import org.apache.wicket.model.IModel;
+import org.apache.wicket.model.Model;
+import org.apache.wicket.spring.injection.annot.SpringBean;
+import org.apache.wicket.validation.validator.UrlValidator;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import de.tudarmstadt.ukp.inception.recommendation.api.model.Recommender;
+import de.tudarmstadt.ukp.inception.recommendation.api.recommender.DefaultTrainableRecommenderTraitsEditor;
+import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommendationEngineFactory;
+import de.tudarmstadt.ukp.inception.recommendation.imls.external.v2.api.ClassifierInfo;
+import de.tudarmstadt.ukp.inception.recommendation.imls.external.v2.api.ExternalRecommenderApiException;
+import de.tudarmstadt.ukp.inception.recommendation.imls.external.v2.api.ExternalRecommenderV2Api;
+import de.tudarmstadt.ukp.inception.support.lambda.LambdaAjaxButton;
+import de.tudarmstadt.ukp.inception.support.lambda.LambdaAjaxFormComponentUpdatingBehavior;
+
+public class ExternalRecommenderTraitsEditor
+ extends DefaultTrainableRecommenderTraitsEditor
+{
+ private static final Logger LOG = LoggerFactory.getLogger(ExternalRecommenderV2Api.class);
+
+ private static final long serialVersionUID = 1677442652521110324L;
+
+ private static final String MID_FORM = "form";
+
+ private @SpringBean RecommendationEngineFactory toolFactory;
+
+ private final ExternalRecommenderTraits traits;
+ private final LambdaAjaxButton> checkServerConnectionButton;
+ private final LambdaAjaxButton> checkClassifierButton;
+ private final DropDownChoice classifierInfoSelect;
+
+ public ExternalRecommenderTraitsEditor(String aId, IModel aRecommender)
+ {
+ super(aId, aRecommender);
+
+ traits = toolFactory.readTraits(aRecommender.getObject());
+
+ Form form = new Form<>(MID_FORM,
+ CompoundPropertyModel.of(Model.of(traits)))
+ {
+ private static final long serialVersionUID = -3109239605742291123L;
+
+ @Override
+ protected void onSubmit()
+ {
+ super.onSubmit();
+ toolFactory.writeTraits(aRecommender.getObject(), traits);
+ }
+ };
+
+ TextField remoteUrl = new TextField<>("remoteUrl");
+ remoteUrl.setRequired(true);
+ remoteUrl.add(new UrlValidator());
+ form.add(remoteUrl);
+
+ checkServerConnectionButton = new LambdaAjaxButton<>("checkServerConnection",
+ this::checkServerConnection);
+ form.add(checkServerConnectionButton);
+
+ // TODO: Make combo box
+ classifierInfoSelect = new DropDownChoice<>("classifierInfo");
+ classifierInfoSelect.setChoiceRenderer(new LambdaChoiceRenderer<>(ClassifierInfo::getName));
+ classifierInfoSelect.setOutputMarkupId(true);
+ form.add(classifierInfoSelect);
+
+ checkClassifierButton = new LambdaAjaxButton<>("checkClassifier", this::checkClassifier);
+ form.add(checkClassifierButton);
+
+ CheckBox trainable = new CheckBox("trainable");
+ trainable.setOutputMarkupId(true);
+ trainable.add(new LambdaAjaxFormComponentUpdatingBehavior("change",
+ _target -> _target.add(getTrainingStatesChoice())));
+ form.add(trainable);
+
+ getTrainingStatesChoice().add(visibleWhen(() -> trainable.getModelObject() == true));
+
+ add(form);
+ }
+
+ private void checkServerConnection(AjaxRequestTarget aTarget, Form> aForm)
+ throws ExternalRecommenderApiException
+ {
+ URI uri = URI.create(traits.getRemoteUrl());
+ ExternalRecommenderV2Api api = new ExternalRecommenderV2Api(uri);
+
+ String newClass;
+ if (api.isReachable()) {
+ newClass = "btn btn-success";
+ classifierInfoSelect.setChoices(api.getAvailableClassifiers());
+ }
+ else {
+ newClass = "btn btn-danger";
+ classifierInfoSelect.setChoices(Collections.emptyList());
+ }
+
+ checkServerConnectionButton.add(AttributeModifier.append("class", newClass));
+
+ aTarget.add(checkServerConnectionButton);
+ aTarget.add(classifierInfoSelect);
+ }
+
+ private void checkClassifier(AjaxRequestTarget aTarget, Form> aForm)
+ {
+ URI uri = URI.create(traits.getRemoteUrl());
+ ExternalRecommenderV2Api api = new ExternalRecommenderV2Api(uri);
+
+ String newClass;
+ String classifierName = traits.getClassifierInfo().getName();
+
+ try {
+ api.getClassifierInfo(classifierName);
+ if (isNotEmpty(classifierName)) {
+ newClass = "btn btn-success";
+ }
+ else {
+ newClass = "btn btn-danger";
+ }
+
+ checkClassifierButton.add(AttributeModifier.append("class", newClass));
+ aTarget.add(checkClassifierButton);
+ }
+ catch (ExternalRecommenderApiException e) {
+ error(e.getMessage());
+ aTarget.addChildren(getPage(), IFeedback.class);
+ }
+ }
+
+}
diff --git a/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v2/ExternalRecommenderTraitsEditor.properties b/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v2/ExternalRecommenderTraitsEditor.properties
new file mode 100644
index 00000000000..862e661915b
--- /dev/null
+++ b/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v2/ExternalRecommenderTraitsEditor.properties
@@ -0,0 +1,21 @@
+# Licensed to the Technische Universit�t Darmstadt under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The Technische Universit�t Darmstadt
+# 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.
+#
+# 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.
+
+remoteUrl=Remote URL
+checkServerConnection=Check server connection
+classifierInfo=Classifier
+checkClassifier=Check classifier connection
+trainable=Trainable
diff --git a/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v2/api/Annotation.java b/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v2/api/Annotation.java
new file mode 100644
index 00000000000..153ae2f78f8
--- /dev/null
+++ b/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v2/api/Annotation.java
@@ -0,0 +1,92 @@
+/*
+ * Licensed to the Technische Universität Darmstadt under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The Technische Universität Darmstadt
+ * 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.
+ *
+ * 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 de.tudarmstadt.ukp.inception.recommendation.imls.external.v2.api;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.Objects;
+
+import org.apache.commons.lang3.builder.ToStringBuilder;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class Annotation
+{
+ private final int begin;
+ private final int end;
+ private final Map features;
+
+ public Annotation(@JsonProperty("begin") int aBegin, @JsonProperty("end") int aEnd,
+ @JsonProperty("features") Map aFeatures)
+ {
+ begin = aBegin;
+ end = aEnd;
+ features = aFeatures != null ? aFeatures : Collections.emptyMap();
+ }
+
+ public Annotation(int aBegin, int aEnd)
+
+ {
+ this(aBegin, aEnd, Collections.emptyMap());
+ }
+
+ @Override
+ public String toString()
+ {
+ return new ToStringBuilder(this) //
+ .append("begin", begin) //
+ .append("end", end) //
+ .append("features", features) //
+ .toString();
+ }
+
+ public int getBegin()
+ {
+ return begin;
+ }
+
+ public int getEnd()
+ {
+ return end;
+ }
+
+ public Map getFeatures()
+ {
+ return features;
+ }
+
+ @Override
+ public boolean equals(Object o)
+ {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ Annotation that = (Annotation) o;
+ return getBegin() == that.getBegin() && getEnd() == that.getEnd()
+ && getFeatures().equals(that.getFeatures());
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return Objects.hash(getBegin(), getEnd(), getFeatures());
+ }
+}
diff --git a/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v2/api/ClassifierInfo.java b/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v2/api/ClassifierInfo.java
new file mode 100644
index 00000000000..e5002a9c6e8
--- /dev/null
+++ b/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v2/api/ClassifierInfo.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Technische Universität Darmstadt under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The Technische Universität Darmstadt
+ * 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.
+ *
+ * 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 de.tudarmstadt.ukp.inception.recommendation.imls.external.v2.api;
+
+import java.io.Serializable;
+
+import org.apache.commons.lang3.builder.ToStringBuilder;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class ClassifierInfo
+ implements Serializable
+{
+ private static final long serialVersionUID = 3361046124201929582L;
+
+ private final String name;
+
+ @JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
+ public ClassifierInfo(@JsonProperty("name") String aName)
+ {
+ name = aName;
+ }
+
+ public String getName()
+ {
+ return name;
+ }
+
+ @Override
+ public String toString()
+ {
+ return new ToStringBuilder(this).append("name", name).toString();
+ }
+}
diff --git a/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v2/api/DatasetList.java b/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v2/api/DatasetList.java
new file mode 100644
index 00000000000..62850980edb
--- /dev/null
+++ b/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v2/api/DatasetList.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Technische Universität Darmstadt under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The Technische Universität Darmstadt
+ * 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.
+ *
+ * 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 de.tudarmstadt.ukp.inception.recommendation.imls.external.v2.api;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.commons.lang3.builder.ToStringBuilder;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class DatasetList
+{
+ private final List names;
+
+ public DatasetList(@JsonProperty("names") List aNames)
+ {
+ names = Collections.unmodifiableList(aNames);
+ }
+
+ public List getNames()
+ {
+ return names;
+ }
+
+ @Override
+ public boolean equals(Object o)
+ {
+ if (this == o)
+ return true;
+ if (o == null || getClass() != o.getClass())
+ return false;
+ DatasetList that = (DatasetList) o;
+ return Objects.equals(getNames(), that.getNames());
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return Objects.hash(getNames());
+ }
+
+ @Override
+ public String toString()
+ {
+ return new ToStringBuilder(this) //
+ .append("names", names) //
+ .toString();
+ }
+}
diff --git a/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v2/api/Document.java b/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v2/api/Document.java
new file mode 100644
index 00000000000..d65d7b70e62
--- /dev/null
+++ b/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v2/api/Document.java
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Technische Universität Darmstadt under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The Technische Universität Darmstadt
+ * 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.
+ *
+ * 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 de.tudarmstadt.ukp.inception.recommendation.imls.external.v2.api;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+import org.apache.commons.lang3.builder.ToStringBuilder;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class Document
+{
+ private final String text;
+ private final Map> annotations;
+ private final long version;
+
+ public Document(@JsonProperty("text") String aText,
+ @JsonProperty("annotations") Map> aAnnotations,
+ @JsonProperty("version") long aVersion)
+ {
+ text = aText;
+ annotations = aAnnotations;
+ version = aVersion;
+ }
+
+ @Override
+ public String toString()
+ {
+ return new ToStringBuilder(this) //
+ .append("text", text) //
+ .append("annotations", annotations) //
+ .append("version", version) //
+ .toString();
+ }
+
+ public String getText()
+ {
+ return text;
+ }
+
+ public Map> getAnnotations()
+ {
+ return annotations;
+ }
+
+ public long getVersion()
+ {
+ return version;
+ }
+
+ @Override
+ public boolean equals(Object o)
+ {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ Document document = (Document) o;
+ return getVersion() == document.getVersion() && getText().equals(document.getText())
+ && getAnnotations().equals(document.getAnnotations());
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return Objects.hash(getText(), getAnnotations(), getVersion());
+ }
+}
diff --git a/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v2/api/DocumentList.java b/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v2/api/DocumentList.java
new file mode 100644
index 00000000000..0a091008a75
--- /dev/null
+++ b/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v2/api/DocumentList.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Technische Universität Darmstadt under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The Technische Universität Darmstadt
+ * 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.
+ *
+ * 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 de.tudarmstadt.ukp.inception.recommendation.imls.external.v2.api;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.commons.lang3.builder.ToStringBuilder;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class DocumentList
+{
+ private final List names;
+ private final List versions;
+
+ public DocumentList(@JsonProperty("names") List aNames,
+ @JsonProperty("versions") List aVersions)
+ {
+ names = Collections.unmodifiableList(aNames);
+ versions = Collections.unmodifiableList(aVersions);
+ }
+
+ public List getNames()
+ {
+ return names;
+ }
+
+ public List getVersions()
+ {
+ return versions;
+ }
+
+ @Override
+ public boolean equals(Object o)
+ {
+ if (this == o)
+ return true;
+ if (o == null || getClass() != o.getClass())
+ return false;
+ DocumentList that = (DocumentList) o;
+ return Objects.equals(getNames(), that.getNames())
+ && Objects.equals(getVersions(), that.getVersions());
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return Objects.hash(getNames(), getVersions());
+ }
+
+ @Override
+ public String toString()
+ {
+ return new ToStringBuilder(this) //
+ .append("names", names) //
+ .append("versions", versions) //
+ .toString();
+ }
+}
diff --git a/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v2/api/ExternalRecommenderApiException.java b/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v2/api/ExternalRecommenderApiException.java
new file mode 100644
index 00000000000..0ebf52d5dae
--- /dev/null
+++ b/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v2/api/ExternalRecommenderApiException.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Technische Universität Darmstadt under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The Technische Universität Darmstadt
+ * 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.
+ *
+ * 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 de.tudarmstadt.ukp.inception.recommendation.imls.external.v2.api;
+
+import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommendationException;
+
+public class ExternalRecommenderApiException
+ extends RecommendationException
+{
+ public ExternalRecommenderApiException(String aMessage)
+ {
+ super(aMessage);
+ }
+
+ public ExternalRecommenderApiException(String aMessage, Exception aCause)
+ {
+ super(aMessage, aCause);
+ }
+
+}
diff --git a/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v2/api/ExternalRecommenderV2Api.java b/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v2/api/ExternalRecommenderV2Api.java
new file mode 100644
index 00000000000..e5dc0f8573e
--- /dev/null
+++ b/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v2/api/ExternalRecommenderV2Api.java
@@ -0,0 +1,347 @@
+/*
+ * Licensed to the Technische Universität Darmstadt under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The Technische Universität Darmstadt
+ * 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.
+ *
+ * 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 de.tudarmstadt.ukp.inception.recommendation.imls.external.v2.api;
+
+import static de.tudarmstadt.ukp.inception.support.json.JSONUtil.fromJsonString;
+import static de.tudarmstadt.ukp.inception.support.json.JSONUtil.getObjectMapper;
+import static de.tudarmstadt.ukp.inception.support.json.JSONUtil.toJsonString;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URLEncoder;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpRequest.BodyPublishers;
+import java.net.http.HttpResponse;
+import java.net.http.HttpResponse.BodyHandlers;
+import java.nio.charset.Charset;
+import java.time.Duration;
+import java.util.List;
+import java.util.StringJoiner;
+
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+
+public class ExternalRecommenderV2Api
+{
+ private static final Logger LOG = LoggerFactory.getLogger(ExternalRecommenderV2Api.class);
+
+ private final HttpClient client;
+ private final String remoteUrl;
+
+ public ExternalRecommenderV2Api(URI aRemoteUrl, Duration aTimeout)
+ {
+ remoteUrl = StringUtils.removeEnd(aRemoteUrl.toString(), "/");
+
+ client = HttpClient.newBuilder() //
+ .version(HttpClient.Version.HTTP_1_1) //
+ .connectTimeout(aTimeout).build();
+ }
+
+ public ExternalRecommenderV2Api(URI aRemoteUrl)
+ {
+ this(aRemoteUrl, Duration.ofSeconds(10));
+ }
+
+ public boolean isReachable()
+ {
+ URI url = urlFor("ping");
+ HttpRequest request = HttpRequest.newBuilder() //
+ .uri(url) //
+ .GET() //
+ .build();
+
+ try {
+ HttpResponse response = client.send(request, BodyHandlers.discarding());
+ return response.statusCode() == 200;
+ }
+ catch (Exception e) {
+ LOG.warn("Error while pinging external recommender server [{}]", url, e);
+ return false;
+ }
+ }
+
+ // Dataset
+
+ public DatasetList listDatasets() throws ExternalRecommenderApiException
+ {
+ URI url = urlFor("dataset");
+ HttpRequest request = HttpRequest.newBuilder() //
+ .uri(url) //
+ .GET() //
+ .build();
+
+ try {
+ HttpResponse response = client.send(request, BodyHandlers.ofString());
+ int status = response.statusCode();
+ if (status != 200) {
+ throw errorf("listDatasets - Unexpected status code [%d]: [%s]", status,
+ response.body());
+ }
+ return fromJsonString(DatasetList.class, response.body());
+ }
+ catch (IOException | InterruptedException e) {
+ throw error("Error while listing datasets", e);
+ }
+ }
+
+ public void createDataset(String aDatasetId) throws ExternalRecommenderApiException
+ {
+ URI url = urlFor("dataset", aDatasetId);
+ HttpRequest request = HttpRequest.newBuilder() //
+ .uri(url) //
+ .PUT(BodyPublishers.noBody()) //
+ .build();
+
+ try {
+ HttpResponse response = client.send(request, BodyHandlers.ofString());
+ int status = response.statusCode();
+ if (status != 204 && status != 409) {
+ throw errorf("createDataset- Unexpected status code [%d]: [%s]", status,
+ response.body());
+ }
+ }
+ catch (IOException | InterruptedException e) {
+ throw errorf("Error while creating dataset [%s]", e, aDatasetId);
+ }
+ }
+
+ public void deleteDataset(String aDatasetId) throws ExternalRecommenderApiException
+ {
+ URI url = urlFor("dataset", aDatasetId);
+ HttpRequest request = HttpRequest.newBuilder() //
+ .uri(url) //
+ .DELETE() //
+ .build();
+
+ try {
+ HttpResponse response = client.send(request, BodyHandlers.ofString());
+ int status = response.statusCode();
+ if (status != 204 && status != 404) {
+ throw errorf("deleteDataset - Unexpected status code [%d]: [%s]", status,
+ response.body());
+ }
+ }
+ catch (IOException | InterruptedException e) {
+ throw errorf("Error while deleting dataset [%s]", e, aDatasetId);
+ }
+ }
+
+ // Documents
+
+ public DocumentList listDocumentsInDataset(String aDatasetId)
+ throws ExternalRecommenderApiException
+ {
+ URI url = urlFor("dataset", aDatasetId);
+ HttpRequest request = HttpRequest.newBuilder() //
+ .uri(url) //
+ .GET() //
+ .build();
+
+ try {
+ HttpResponse response = client.send(request, BodyHandlers.ofString());
+ int status = response.statusCode();
+
+ if (status != 200) {
+ throw errorf("listDocumentsInDataset: Unexpected status code [%d]: [%s]", status,
+ response.body());
+ }
+ return fromJsonString(DocumentList.class, response.body());
+ }
+ catch (IOException | InterruptedException e) {
+ throw errorf("Error while listing documents for dataset [%s]", e, aDatasetId);
+ }
+ }
+
+ public void addDocumentToDataset(String aDatasetId, String aDocumentId, Document aDocument)
+ throws ExternalRecommenderApiException
+ {
+ try {
+ URI url = urlFor("dataset", aDatasetId, aDocumentId);
+ HttpRequest request = HttpRequest.newBuilder() //
+ .uri(url) //
+ .header("Content-Type", "application/json") //
+ .PUT(BodyPublishers.ofString(toJsonString(aDocument))) //
+ .build();
+
+ HttpResponse response = client.send(request, BodyHandlers.ofString());
+ int status = response.statusCode();
+ if (status != 204) {
+ throw errorf("addDocumentToDataset: Unexpected status code [%d]: [%s]", status,
+ response.body());
+ }
+ }
+ catch (IOException | InterruptedException e) {
+ throw errorf("Error while adding document [%s] to dataset [%s]", e, aDocumentId,
+ aDatasetId);
+ }
+ }
+
+ public void deleteDocumentFromDataset(String aDatasetId, String aDocumentId)
+ throws ExternalRecommenderApiException
+ {
+ URI url = urlFor("dataset", aDatasetId, aDocumentId);
+ HttpRequest request = HttpRequest.newBuilder() //
+ .uri(url) //
+ .DELETE() //
+ .build();
+
+ try {
+ HttpResponse response = client.send(request, BodyHandlers.ofString());
+ int status = response.statusCode();
+ if (status != 204 && status != 404) {
+ throw errorf("deleteDocumentFromDataset - Unexpected status code [%d]: [%s]",
+ status, response.body());
+ }
+ }
+ catch (IOException | InterruptedException e) {
+ throw errorf("Error while deleting document [%s] from dataset [%s]", e, aDocumentId,
+ aDatasetId);
+ }
+ }
+
+ // Classifier
+
+ public List getAvailableClassifiers() throws ExternalRecommenderApiException
+ {
+ URI url = urlFor("classifier");
+ HttpRequest request = HttpRequest.newBuilder() //
+ .uri(url) //
+ .GET() //
+ .build();
+
+ try {
+ HttpResponse response = client.send(request, BodyHandlers.ofString());
+ return getObjectMapper().readValue(response.body(), new TypeReference<>()
+ {
+ });
+
+ }
+ catch (IOException | InterruptedException e) {
+ throw error("Error while getting available classifier", e);
+ }
+ }
+
+ public ClassifierInfo getClassifierInfo(String aClassifierId)
+ throws ExternalRecommenderApiException
+ {
+ URI url = urlFor("classifier", aClassifierId);
+ HttpRequest request = HttpRequest.newBuilder() //
+ .uri(url) //
+ .GET() //
+ .build();
+
+ try {
+ HttpResponse response = client.send(request, BodyHandlers.ofString());
+ return fromJsonString(ClassifierInfo.class, response.body());
+ }
+ catch (Exception e) {
+ throw errorf("Error while getting info for classifier [%s]", e, aClassifierId);
+ }
+ }
+
+ public void trainOnDataset(String aClassifierId, String aModelId, String aDatasetId)
+ throws ExternalRecommenderApiException
+ {
+ URI url = urlFor("classifier", aClassifierId, aModelId, "train", aDatasetId);
+ HttpRequest request = HttpRequest.newBuilder() //
+ .uri(url) //
+ .POST(BodyPublishers.noBody()) //
+ .build();
+
+ try {
+ HttpResponse response = client.send(request, BodyHandlers.ofString());
+ int status = response.statusCode();
+ if (status != 202 && status != 429) {
+ throw errorf("trainOnDataset - Unexpected status code [%d]: [%s]", status,
+ response.body());
+ }
+ }
+ catch (IOException | InterruptedException e) {
+ throw errorf("Error while training on dataset [%s]", e, aDatasetId);
+ }
+ }
+
+ public Document predict(String aClassifierId, String aModelId, Document aDocument)
+ throws ExternalRecommenderApiException
+ {
+ URI url = urlFor("classifier", aClassifierId, aModelId, "predict");
+
+ try {
+ HttpRequest request = HttpRequest.newBuilder() //
+ .uri(url) //
+ .header("Content-Type", "application/json") //
+ .POST(BodyPublishers.ofString(toJsonString(aDocument))) //
+ .build();
+ HttpResponse response = client.send(request, BodyHandlers.ofString());
+ LOG.info("Predicting finished with status code [{}]", response.statusCode());
+
+ if (response.statusCode() == 200) {
+ return fromJsonString(Document.class, response.body());
+ }
+ else {
+ String msg = "Error while predicting: " + response.body();
+ LOG.error(msg);
+ throw new ExternalRecommenderApiException(msg);
+ }
+ }
+ catch (IOException | InterruptedException e) {
+ throw errorf("Error while predicting [%s] [%s]", e, aClassifierId, aModelId);
+ }
+ }
+
+ private ExternalRecommenderApiException errorf(String aFormatString, Object... aArgs)
+ {
+ String message = String.format(aFormatString, aArgs);
+ LOG.error(message);
+ return new ExternalRecommenderApiException(message);
+ }
+
+ private ExternalRecommenderApiException errorf(String aFormatString, Exception aCause,
+ Object... aArgs)
+ {
+ String message = String.format(aFormatString, aArgs);
+ LOG.error(message);
+ return new ExternalRecommenderApiException(message, aCause);
+ }
+
+ private ExternalRecommenderApiException error(String aMessage, Exception aCause)
+ {
+ LOG.error(aMessage, aCause);
+ return new ExternalRecommenderApiException(aMessage, aCause);
+ }
+
+ private URI urlFor(String... aParts)
+ {
+ StringJoiner joiner = new StringJoiner("/");
+ for (String part : aParts) {
+ joiner.add(encode(part));
+ }
+
+ return URI.create(remoteUrl + "/" + joiner);
+ }
+
+ private String encode(String aString)
+ {
+ return URLEncoder.encode(aString, Charset.defaultCharset());
+ }
+
+}
diff --git a/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v2/api/FormatConverter.java b/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v2/api/FormatConverter.java
new file mode 100644
index 00000000000..558eea34b0f
--- /dev/null
+++ b/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v2/api/FormatConverter.java
@@ -0,0 +1,101 @@
+/*
+ * Licensed to the Technische Universität Darmstadt under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The Technische Universität Darmstadt
+ * 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.
+ *
+ * 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 de.tudarmstadt.ukp.inception.recommendation.imls.external.v2.api;
+
+import static de.tudarmstadt.ukp.inception.recommendation.api.RecommendationService.FEATURE_NAME_IS_PREDICTION;
+import static java.util.Collections.singletonMap;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.uima.cas.CAS;
+import org.apache.uima.cas.Feature;
+import org.apache.uima.cas.Type;
+import org.apache.uima.cas.text.AnnotationFS;
+import org.apache.uima.fit.util.CasUtil;
+
+import de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.Sentence;
+import de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.Token;
+
+public class FormatConverter
+{
+ public static final String SENTENCE_LAYER = "t.sentence";
+ public static final String TOKEN_LAYER = "t.token";
+ public static final String TARGET_LAYER = "t.annotation";
+ public static final String TARGET_FEATURE = "f.value";
+
+ public Document documentFromCas(CAS aCas, String aLayerName, String aFeatureName, long aVersion)
+ {
+ String text = aCas.getDocumentText();
+ Map> annotations = new HashMap<>();
+
+ // Add sentences
+ Type sentenceType = CasUtil.getAnnotationType(aCas, Sentence.class);
+ List sentences = new ArrayList<>();
+ for (AnnotationFS sentence : CasUtil.select(aCas, sentenceType)) {
+ sentences.add(new Annotation(sentence.getBegin(), sentence.getEnd()));
+ }
+ annotations.put(SENTENCE_LAYER, sentences);
+
+ // Add tokens
+ Type tokenType = CasUtil.getAnnotationType(aCas, Token.class);
+ List tokens = new ArrayList<>();
+ for (AnnotationFS token : CasUtil.select(aCas, tokenType)) {
+ tokens.add(new Annotation(token.getBegin(), token.getEnd()));
+ }
+ annotations.put(TOKEN_LAYER, tokens);
+
+ // Add targets
+ Type targetType = CasUtil.getAnnotationType(aCas, aLayerName);
+ Feature feature = targetType.getFeatureByBaseName(aFeatureName);
+ List targets = new ArrayList<>();
+ for (AnnotationFS target : CasUtil.select(aCas, targetType)) {
+ String featureValue = target.getFeatureValueAsString(feature);
+ Map featureValues = singletonMap(TARGET_FEATURE, featureValue);
+ targets.add(new Annotation(target.getBegin(), target.getEnd(), featureValues));
+ }
+ annotations.put(TARGET_LAYER, targets);
+
+ return new Document(text, annotations, aVersion);
+ }
+
+ public void loadIntoCas(Document aDocument, String aLayerName, String aFeatureName, CAS aCas)
+ {
+ Type targetType = CasUtil.getAnnotationType(aCas, aLayerName);
+ Feature feature = targetType.getFeatureByBaseName(aFeatureName);
+ Feature isPredicted = targetType.getFeatureByBaseName(FEATURE_NAME_IS_PREDICTION);
+
+ List annotations = aDocument.getAnnotations().getOrDefault(TARGET_LAYER,
+ Collections.emptyList());
+
+ for (Annotation annotation : annotations) {
+ int begin = annotation.getBegin();
+ int end = annotation.getEnd();
+ AnnotationFS fs = aCas.createAnnotation(targetType, begin, end);
+ String featureValue = annotation.getFeatures().get(TARGET_FEATURE);
+ fs.setStringValue(feature, featureValue);
+ if (isPredicted != null) {
+ fs.setBooleanValue(isPredicted, true);
+ }
+ aCas.addFsToIndexes(fs);
+ }
+ }
+}
diff --git a/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v2/config/ExternalRecommenderAutoConfiguration.java b/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v2/config/ExternalRecommenderAutoConfiguration.java
new file mode 100644
index 00000000000..37173c6593a
--- /dev/null
+++ b/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v2/config/ExternalRecommenderAutoConfiguration.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Technische Universität Darmstadt under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The Technische Universität Darmstadt
+ * 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.
+ *
+ * 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 de.tudarmstadt.ukp.inception.recommendation.imls.external.v2.config;
+
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import de.tudarmstadt.ukp.inception.recommendation.imls.external.v2.ExternalRecommenderFactory;
+
+@Configuration
+@ConditionalOnProperty(prefix = "recommender.external.v2", name = "enabled", havingValue = "true", //
+ matchIfMissing = true)
+@EnableConfigurationProperties(ExternalRecommenderPropertiesImpl.class)
+public class ExternalRecommenderAutoConfiguration
+{
+ @Bean
+ public ExternalRecommenderFactory externalRecommenderFactoryV2(
+ ExternalRecommenderProperties aProperties)
+ {
+ return new ExternalRecommenderFactory(aProperties);
+ }
+}
diff --git a/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v2/config/ExternalRecommenderProperties.java b/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v2/config/ExternalRecommenderProperties.java
new file mode 100644
index 00000000000..fc15cd800bd
--- /dev/null
+++ b/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v2/config/ExternalRecommenderProperties.java
@@ -0,0 +1,27 @@
+/*
+ * Licensed to the Technische Universität Darmstadt under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The Technische Universität Darmstadt
+ * 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.
+ *
+ * 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 de.tudarmstadt.ukp.inception.recommendation.imls.external.v2.config;
+
+import java.time.Duration;
+
+public interface ExternalRecommenderProperties
+{
+ Duration getConnectTimeout();
+
+ Duration getReadTimeout();
+}
diff --git a/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v2/config/ExternalRecommenderPropertiesImpl.java b/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v2/config/ExternalRecommenderPropertiesImpl.java
new file mode 100644
index 00000000000..50395589f2c
--- /dev/null
+++ b/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v2/config/ExternalRecommenderPropertiesImpl.java
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Technische Universität Darmstadt under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The Technische Universität Darmstadt
+ * 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.
+ *
+ * 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 de.tudarmstadt.ukp.inception.recommendation.imls.external.v2.config;
+
+import static java.time.temporal.ChronoUnit.SECONDS;
+
+import java.time.Duration;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ *
+ * This class is exposed as a Spring Component via {@link ExternalRecommenderAutoConfiguration}.
+ *
+ */
+@ConfigurationProperties("recommender.external")
+public class ExternalRecommenderPropertiesImpl
+ implements ExternalRecommenderProperties
+{
+ private Duration connectTimeout = Duration.of(30, SECONDS);
+ private Duration readTimeout = Duration.of(30, SECONDS);
+
+ @Override
+ public Duration getConnectTimeout()
+ {
+ return connectTimeout;
+ }
+
+ public void setConnectTimeout(Duration aConnectTimeout)
+ {
+ connectTimeout = aConnectTimeout;
+ }
+
+ @Override
+ public Duration getReadTimeout()
+ {
+ return readTimeout;
+ }
+
+ public void setReadTimeout(Duration aReadTimeout)
+ {
+ readTimeout = aReadTimeout;
+ }
+
+}
diff --git a/inception/inception-imls-external/src/test/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/util/Fixtures.java b/inception/inception-imls-external/src/test/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/util/Fixtures.java
new file mode 100644
index 00000000000..6349c97629c
--- /dev/null
+++ b/inception/inception-imls-external/src/test/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/util/Fixtures.java
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Technische Universität Darmstadt under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The Technische Universität Darmstadt
+ * 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.
+ *
+ * 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 de.tudarmstadt.ukp.inception.recommendation.imls.external.util;
+
+import static de.tudarmstadt.ukp.inception.support.json.JSONUtil.fromJsonStream;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.nio.file.Paths;
+
+import org.apache.uima.cas.CAS;
+import org.apache.uima.fit.factory.JCasFactory;
+import org.apache.uima.jcas.JCas;
+import org.apache.uima.util.CasIOUtils;
+
+import de.tudarmstadt.ukp.inception.recommendation.imls.external.v2.api.Document;
+
+public class Fixtures
+{
+
+ public static CAS loadSmallCas() throws Exception
+ {
+ return loadCas("small_cas.xmi");
+
+ }
+
+ public static CAS loadAlaskaCas() throws Exception
+ {
+ return loadCas("alaska.xmi");
+ }
+
+ private static CAS loadCas(String aPathToXmi) throws Exception
+ {
+ try (FileInputStream fis = new FileInputStream(getResource(aPathToXmi))) {
+ JCas jcas = JCasFactory.createJCas();
+ CasIOUtils.load(fis, jcas.getCas());
+ return jcas.getCas();
+ }
+ }
+
+ public static Document loadSmallDocument() throws Exception
+ {
+
+ try (FileInputStream fis = new FileInputStream(getResource("small_cas.json"))) {
+ return fromJsonStream(Document.class, fis);
+ }
+ }
+
+ public static File getResource(String aResourceName)
+ {
+ return Paths.get("src", "test", "resources", aResourceName).toFile();
+ }
+}
diff --git a/inception/inception-imls-external/src/test/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v2/api/ExternalRecommenderV2ApiTest.java b/inception/inception-imls-external/src/test/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v2/api/ExternalRecommenderV2ApiTest.java
new file mode 100644
index 00000000000..c747e070bf6
--- /dev/null
+++ b/inception/inception-imls-external/src/test/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v2/api/ExternalRecommenderV2ApiTest.java
@@ -0,0 +1,202 @@
+/*
+ * Licensed to the Technische Universität Darmstadt under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The Technische Universität Darmstadt
+ * 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.
+ *
+ * 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 de.tudarmstadt.ukp.inception.recommendation.imls.external.v2.api;
+
+import static de.tudarmstadt.ukp.inception.recommendation.imls.external.util.Fixtures.loadAlaskaCas;
+import static de.tudarmstadt.ukp.inception.support.json.JSONUtil.toPrettyJsonString;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.apache.uima.cas.CAS;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+@Disabled("Requires starting a test server")
+public class ExternalRecommenderV2ApiTest
+{
+
+ /// *********************************************
+ /// In order to run these, you need to install the test dependencies
+ /// pip install -e ".[all]"
+ /// and then to run
+ /// make inception_test
+ /// in the galahad repository
+ /// *********************************************
+
+ private ExternalRecommenderV2Api sut;
+
+ private static final String CLASSIFIER_ID = "spacy_ner";
+
+ @BeforeEach
+ public void setUp() throws Exception
+ {
+ sut = new ExternalRecommenderV2Api(URI.create("http://localhost:8000"));
+ purgeServer();
+ }
+
+ @AfterEach
+ public void tearDown() throws Exception
+ {
+ purgeServer();
+ sut = null;
+ }
+
+ // Dataset
+
+ @Test
+ public void testListDatasets() throws Exception
+ {
+ List datasetNames = new ArrayList<>();
+ for (int i = 0; i < 3; i++) {
+ String name = "test_dataset_" + (i + 1);
+ datasetNames.add(name);
+ sut.createDataset(name);
+ }
+
+ DatasetList result = sut.listDatasets();
+ assertThat(result.getNames()).isEqualTo(datasetNames);
+ }
+
+ @Test
+ public void testCreateDataset() throws Exception
+ {
+ sut.createDataset("test_dataset");
+ assertThat(sut.listDatasets().getNames()).isEqualTo(List.of("test_dataset"));
+ }
+
+ @Test
+ public void testDeleteDataset() throws Exception
+ {
+ sut.createDataset("test_dataset");
+ assertThat(sut.listDatasets().getNames()).isEqualTo(List.of("test_dataset"));
+
+ sut.deleteDataset("test_dataset");
+ assertThat(sut.listDatasets().getNames()).isEmpty();
+ }
+
+ // Documents
+
+ @Test
+ public void testListDocumentsInDataset() throws Exception
+ {
+ String datasetId = "test_dataset";
+ sut.createDataset("test_dataset");
+
+ List documentNames = new ArrayList<>();
+ for (int i = 0; i < 3; i++) {
+ String name = "test_document_" + (i + 1);
+ documentNames.add(name);
+ sut.addDocumentToDataset("test_dataset", name, buildAlaskaDocument());
+ }
+
+ DocumentList result = sut.listDocumentsInDataset(datasetId);
+
+ assertThat(result.getNames()).isEqualTo(documentNames);
+ assertThat(result.getNames()).hasSameSizeAs(result.getVersions());
+ }
+
+ @Test
+ public void testAddDocumentToDataset() throws Exception
+ {
+ String datasetId = "test_dataset";
+ sut.createDataset("test_dataset");
+ sut.addDocumentToDataset("test_dataset", "test_document", buildAlaskaDocument());
+
+ DocumentList result = sut.listDocumentsInDataset(datasetId);
+
+ assertThat(result.getNames()).isEqualTo(List.of("test_document"));
+ assertThat(result.getNames()).hasSameSizeAs(result.getVersions());
+ }
+
+ @Test
+ public void testDeleteDocumentFromDataset() throws Exception
+ {
+ sut.createDataset("test_dataset");
+ sut.addDocumentToDataset("test_dataset", "test_document", buildAlaskaDocument());
+ DocumentList result = sut.listDocumentsInDataset("test_dataset");
+ assertThat(result.getNames()).isEqualTo(List.of("test_document"));
+
+ sut.deleteDocumentFromDataset("test_dataset", "test_document");
+ assertThat(sut.listDocumentsInDataset("test_dataset").getNames()).isEmpty();
+ }
+
+ // Classifier
+
+ @Test
+ public void testGetAvailableClassifiers() throws Exception
+ {
+ List availableClassifiers = sut.getAvailableClassifiers();
+ List classifierNames = availableClassifiers.stream() //
+ .map(ClassifierInfo::getName) //
+ .collect(Collectors.toList());
+
+ assertThat(classifierNames).isEqualTo(List.of("sklearn1", "sklearn2", "spacy_ner"));
+ }
+
+ @Test
+ public void testGetClassifierInfo() throws Exception
+ {
+ ClassifierInfo classifierInfo = sut.getClassifierInfo(CLASSIFIER_ID);
+
+ assertThat(classifierInfo.getName()).isEqualTo(CLASSIFIER_ID);
+ }
+
+ // Train/Predict
+
+ @Test
+ public void testTrainOnDataset() throws Exception
+ {
+ sut.createDataset("test_dataset");
+ sut.addDocumentToDataset("test_dataset", "test_document", buildAlaskaDocument());
+
+ sut.trainOnDataset("sklearn1", "test_model", "test_dataset");
+ }
+
+ @Test
+ public void testPredictDocument() throws Exception
+ {
+ Document document = buildAlaskaDocument();
+
+ Document response = sut.predict(CLASSIFIER_ID, "test_model", document);
+
+ System.out.println(toPrettyJsonString(response));
+ }
+
+ private void purgeServer() throws Exception
+ {
+ // Delete existing datasets
+ DatasetList datasetList = sut.listDatasets();
+ for (String datasetName : datasetList.getNames()) {
+ sut.deleteDataset(datasetName);
+ }
+ }
+
+ private Document buildAlaskaDocument() throws Exception
+ {
+ FormatConverter converter = new FormatConverter();
+ CAS cas = loadAlaskaCas();
+ return converter.documentFromCas(cas,
+ "de.tudarmstadt.ukp.dkpro.core.api.ner.type.NamedEntity", "value", 0);
+ }
+}
diff --git a/inception/inception-imls-external/src/test/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v2/api/FormatConverterTest.java b/inception/inception-imls-external/src/test/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v2/api/FormatConverterTest.java
new file mode 100644
index 00000000000..ab907c61236
--- /dev/null
+++ b/inception/inception-imls-external/src/test/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v2/api/FormatConverterTest.java
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Technische Universität Darmstadt under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The Technische Universität Darmstadt
+ * 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.
+ *
+ * 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 de.tudarmstadt.ukp.inception.recommendation.imls.external.v2.api;
+
+import static de.tudarmstadt.ukp.inception.recommendation.imls.external.util.Fixtures.loadSmallCas;
+import static de.tudarmstadt.ukp.inception.recommendation.imls.external.util.Fixtures.loadSmallDocument;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.uima.cas.CAS;
+import org.apache.uima.cas.Feature;
+import org.apache.uima.cas.Type;
+import org.apache.uima.cas.text.AnnotationFS;
+import org.apache.uima.fit.factory.JCasFactory;
+import org.apache.uima.fit.util.CasUtil;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+public class FormatConverterTest
+{
+
+ private final String NER = "de.tudarmstadt.ukp.dkpro.core.api.ner.type.NamedEntity";
+ private final String VALUE = "value";
+
+ private FormatConverter sut;
+
+ @BeforeEach
+ public void setUp() throws Exception
+ {
+ sut = new FormatConverter();
+ }
+
+ @AfterEach
+ public void tearDown() throws Exception
+ {
+ sut = null;
+ }
+
+ @Test
+ public void testDocumentFromCas() throws Exception
+ {
+ CAS cas = loadSmallCas();
+ Document expected = loadSmallDocument();
+
+ Document document = sut.documentFromCas(cas,
+ "de.tudarmstadt.ukp.dkpro.core.api.ner.type.NamedEntity", "value", 23);
+
+ assertThat(document).isEqualTo(expected);
+ }
+
+ @Test
+ public void testLoadDocumentIntoCas() throws Exception
+ {
+ CAS cas = JCasFactory.createJCas().getCas();
+ Document document = loadSmallDocument();
+ sut.loadIntoCas(document, NER, VALUE, cas);
+
+ Type targetType = CasUtil.getAnnotationType(cas, NER);
+ Feature feature = targetType.getFeatureByBaseName(VALUE);
+
+ List result = new ArrayList<>(CasUtil.select(cas, targetType));
+
+ assertThat(result).hasSize(3);
+ assertThat(result.get(0).getFeatureValueAsString(feature)).isEqualTo("PER");
+ assertThat(result.get(1).getFeatureValueAsString(feature)).isEqualTo("OTH");
+ assertThat(result.get(2).getFeatureValueAsString(feature)).isEqualTo("OTH");
+ }
+}
diff --git a/inception/inception-imls-external/src/test/resources/alaska.xmi b/inception/inception-imls-external/src/test/resources/alaska.xmi
new file mode 100644
index 00000000000..9a7f501c6fa
--- /dev/null
+++ b/inception/inception-imls-external/src/test/resources/alaska.xmi
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inception/inception-imls-external/src/test/resources/small_cas.json b/inception/inception-imls-external/src/test/resources/small_cas.json
new file mode 100644
index 00000000000..ad061999ec0
--- /dev/null
+++ b/inception/inception-imls-external/src/test/resources/small_cas.json
@@ -0,0 +1,85 @@
+{
+ "text": "Joe waited for the train . The train was late .",
+ "version": 23,
+ "annotations": {
+ "t.token": [
+ {
+ "begin": 0,
+ "end": 3
+ },
+ {
+ "begin": 4,
+ "end": 10
+ },
+ {
+ "begin": 11,
+ "end": 14
+ },
+ {
+ "begin": 15,
+ "end": 18
+ },
+ {
+ "begin": 19,
+ "end": 24
+ },
+ {
+ "begin": 25,
+ "end": 26
+ },
+ {
+ "begin": 27,
+ "end": 30
+ },
+ {
+ "begin": 31,
+ "end": 36
+ },
+ {
+ "begin": 37,
+ "end": 40
+ },
+ {
+ "begin": 41,
+ "end": 45
+ },
+ {
+ "begin": 46,
+ "end": 47
+ }
+ ],
+ "t.sentence": [
+ {
+ "begin": 0,
+ "end": 26
+ },
+ {
+ "begin": 27,
+ "end": 47
+ }
+ ],
+ "t.annotation": [
+ {
+ "begin": 0,
+ "end": 3,
+ "features": {
+ "f.value": "PER"
+ }
+ },
+ {
+ "begin": 19,
+ "end": 24,
+ "features": {
+ "f.value": "OTH"
+ }
+ },
+ {
+ "begin": 31,
+ "end": 36,
+ "features": {
+ "f.value": "OTH"
+ }
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/inception/inception-imls-external/src/test/resources/small_cas.xmi b/inception/inception-imls-external/src/test/resources/small_cas.xmi
new file mode 100644
index 00000000000..35a7ab5ac7e
--- /dev/null
+++ b/inception/inception-imls-external/src/test/resources/small_cas.xmi
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/recommender/PredictionContext.java b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/recommender/PredictionContext.java
index b1e51cde605..dad216d1666 100644
--- a/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/recommender/PredictionContext.java
+++ b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/recommender/PredictionContext.java
@@ -24,6 +24,7 @@
import java.util.List;
import java.util.Optional;
+import de.tudarmstadt.ukp.clarin.webanno.security.model.User;
import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommenderContext.Key;
import de.tudarmstadt.ukp.inception.scheduling.Monitor;
import de.tudarmstadt.ukp.inception.support.logging.LogMessage;
@@ -53,6 +54,11 @@ synchronized public Optional get(Key aKey)
return modelContext.get(aKey);
}
+ synchronized public Optional getUser()
+ {
+ return modelContext.getUser();
+ }
+
synchronized public void log(LogMessage aMessage)
{
if (closed) {