diff --git a/inception/inception-imls-external/pom.xml b/inception/inception-imls-external/pom.xml index 78ed8528087..65afba01a94 100644 --- a/inception/inception-imls-external/pom.xml +++ b/inception/inception-imls-external/pom.xml @@ -39,6 +39,10 @@ de.tudarmstadt.ukp.inception.app inception-annotation-storage-api + + de.tudarmstadt.ukp.inception.app + inception-annotation-storage + de.tudarmstadt.ukp.inception.app inception-api-render @@ -51,6 +55,10 @@ de.tudarmstadt.ukp.inception.app inception-support + + de.tudarmstadt.ukp.inception.app + inception-security + commons-io @@ -83,6 +91,11 @@ uimafit-core + + org.dkpro.core + dkpro-core-api-segmentation-asl + + org.springframework spring-context @@ -104,6 +117,10 @@ com.fasterxml.jackson.core jackson-core + + com.fasterxml.jackson.core + jackson-databind + com.fasterxml.jackson.core jackson-annotations @@ -126,11 +143,6 @@ inception-testing test - - de.tudarmstadt.ukp.inception.app - inception-annotation-storage - test - 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) {