diff --git a/inception/inception-export/src/test/java/de/tudarmstadt/ukp/inception/export/exporters/ProjectLogExporterTest.java b/inception/inception-export/src/test/java/de/tudarmstadt/ukp/inception/export/exporters/ProjectLogExporterTest.java index 8ce93bb98b4..6a721444aed 100644 --- a/inception/inception-export/src/test/java/de/tudarmstadt/ukp/inception/export/exporters/ProjectLogExporterTest.java +++ b/inception/inception-export/src/test/java/de/tudarmstadt/ukp/inception/export/exporters/ProjectLogExporterTest.java @@ -78,7 +78,7 @@ void setup() repositoryProperties.setPath(tempDir); projectService = new ProjectServiceImpl(userService, applicationEventPublisher, - repositoryProperties, emptyList(), null); + repositoryProperties, emptyList(), emptyList(), null); sut = new ProjectLogExporter(projectService); } diff --git a/inception/inception-project-api/pom.xml b/inception/inception-project-api/pom.xml index 627be2c851a..1c36a4ccfad 100644 --- a/inception/inception-project-api/pom.xml +++ b/inception/inception-project-api/pom.xml @@ -41,6 +41,11 @@ de.tudarmstadt.ukp.inception.app inception-support + + + org.apache.wicket + wicket-core + org.slf4j diff --git a/inception/inception-project-api/src/main/java/de/tudarmstadt/ukp/inception/project/api/FeatureInitializer.java b/inception/inception-project-api/src/main/java/de/tudarmstadt/ukp/inception/project/api/FeatureInitializer.java new file mode 100644 index 00000000000..33246fd62ad --- /dev/null +++ b/inception/inception-project-api/src/main/java/de/tudarmstadt/ukp/inception/project/api/FeatureInitializer.java @@ -0,0 +1,47 @@ +/* + * 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.project.api; + +import java.io.IOException; +import java.util.List; +import java.util.Optional; + +import org.apache.wicket.request.resource.ResourceReference; + +import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; + +public interface FeatureInitializer +{ + String getName(); + + boolean alreadyApplied(AnnotationLayer aLayer); + + List> getDependencies(); + + default Optional getDescription() + { + return Optional.empty(); + } + + default Optional getThumbnail() + { + return Optional.empty(); + } + + void configure(AnnotationLayer aLayer) throws IOException; +} diff --git a/inception/inception-project-api/src/main/java/de/tudarmstadt/ukp/inception/project/api/ProjectService.java b/inception/inception-project-api/src/main/java/de/tudarmstadt/ukp/inception/project/api/ProjectService.java index d5112af2da7..550003b6755 100644 --- a/inception/inception-project-api/src/main/java/de/tudarmstadt/ukp/inception/project/api/ProjectService.java +++ b/inception/inception-project-api/src/main/java/de/tudarmstadt/ukp/inception/project/api/ProjectService.java @@ -532,6 +532,8 @@ default void initializeProject(Project aProject, List aIniti List listProjectInitializers(); + List listFeatureInitializers(); + static MDCContext withProjectLogger(Project aProject) { Validate.notNull(aProject, "Project must be given"); diff --git a/inception/inception-project-initializers-basic/src/main/java/de/tudarmstadt/ukp/inception/project/initializers/basic/CommentFeatureInitializer.java b/inception/inception-project-initializers-basic/src/main/java/de/tudarmstadt/ukp/inception/project/initializers/basic/CommentFeatureInitializer.java new file mode 100644 index 00000000000..768a837789c --- /dev/null +++ b/inception/inception-project-initializers-basic/src/main/java/de/tudarmstadt/ukp/inception/project/initializers/basic/CommentFeatureInitializer.java @@ -0,0 +1,128 @@ +/* + * 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.project.initializers.basic; + +import static java.util.Arrays.asList; +import static org.apache.uima.cas.CAS.TYPE_NAME_STRING; + +import java.io.IOException; +import java.lang.invoke.MethodHandles; +import java.util.List; +import java.util.Optional; + +import org.apache.wicket.request.resource.PackageResourceReference; +import org.apache.wicket.request.resource.ResourceReference; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.annotation.Order; + +import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationFeature; +import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; +import de.tudarmstadt.ukp.clarin.webanno.project.initializers.TokenLayerInitializer; +import de.tudarmstadt.ukp.inception.annotation.feature.string.StringFeatureSupport; +import de.tudarmstadt.ukp.inception.project.api.FeatureInitializer; +import de.tudarmstadt.ukp.inception.project.api.ProjectInitializer; +import de.tudarmstadt.ukp.inception.project.initializers.basic.config.InceptionBasicProjectInitializersAutoConfiguration; +import de.tudarmstadt.ukp.inception.schema.api.AnnotationSchemaService; +import de.tudarmstadt.ukp.inception.support.wicket.resource.Strings; + +/** + *

+ * This class is exposed as a Spring Component via + * {@link InceptionBasicProjectInitializersAutoConfiguration#commentFeatureInitializer}. + *

+ */ +@Order(10) +public class CommentFeatureInitializer + implements FeatureInitializer +{ + private static final PackageResourceReference THUMBNAIL = new PackageResourceReference( + MethodHandles.lookup().lookupClass(), "BasicSpanLayerInitializer.svg"); + + public static final String BASIC_COMMENT_FEATURE_NAME = "comment"; + + private final AnnotationSchemaService annotationSchemaService; + private final StringFeatureSupport stringFeatureSupport; + + @Autowired + public CommentFeatureInitializer(AnnotationSchemaService aAnnotationSchemaService, + StringFeatureSupport aStringFeatureSupport) + { + annotationSchemaService = aAnnotationSchemaService; + stringFeatureSupport = aStringFeatureSupport; + } + + @Override + public String getName() + { + return "Comment feature"; + } + + @Override + public Optional getDescription() + { + return Optional.of(Strings.getString("comment-feature.description")); + } + + @Override + public Optional getThumbnail() + { + return Optional.of(THUMBNAIL); + } + + @Override + public boolean alreadyApplied(AnnotationLayer aLayer) + { + if (aLayer.isBuiltIn()) { + return true; + } + + return annotationSchemaService.existsFeature(BASIC_COMMENT_FEATURE_NAME, aLayer); + } + + @Override + public List> getDependencies() + { + return asList( + // Because locks to token boundaries + TokenLayerInitializer.class, // + // Tagsets + BasicSpanTagSetInitializer.class); + } + + @Override + public void configure(AnnotationLayer aLayer) throws IOException + { + var commentFeature = AnnotationFeature.builder() // + .withName(BASIC_COMMENT_FEATURE_NAME) // + .withUiName("Comment") // + .withDescription("Leave comments here.") // + .withType(TYPE_NAME_STRING) // + .withLayer(aLayer) // + .withCuratable(false) // + .withVisible(false) // + .withIncludeInHover(true) // + .build(); + + var traits = stringFeatureSupport.createDefaultTraits(); + traits.setMultipleRows(true); + traits.setDynamicSize(true); + stringFeatureSupport.writeTraits(commentFeature, traits); + + annotationSchemaService.createFeature(commentFeature); + } +} diff --git a/inception/inception-project-initializers-basic/src/main/java/de/tudarmstadt/ukp/inception/project/initializers/basic/config/InceptionBasicProjectInitializersAutoConfiguration.java b/inception/inception-project-initializers-basic/src/main/java/de/tudarmstadt/ukp/inception/project/initializers/basic/config/InceptionBasicProjectInitializersAutoConfiguration.java index d25e7dd02e8..7b7854f1f4c 100644 --- a/inception/inception-project-initializers-basic/src/main/java/de/tudarmstadt/ukp/inception/project/initializers/basic/config/InceptionBasicProjectInitializersAutoConfiguration.java +++ b/inception/inception-project-initializers-basic/src/main/java/de/tudarmstadt/ukp/inception/project/initializers/basic/config/InceptionBasicProjectInitializersAutoConfiguration.java @@ -23,6 +23,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import de.tudarmstadt.ukp.inception.annotation.feature.string.StringFeatureSupport; import de.tudarmstadt.ukp.inception.project.initializers.basic.BasicProjectInitializer; import de.tudarmstadt.ukp.inception.project.initializers.basic.BasicRelationLayerInitializer; import de.tudarmstadt.ukp.inception.project.initializers.basic.BasicRelationRecommenderInitializer; @@ -30,6 +31,7 @@ import de.tudarmstadt.ukp.inception.project.initializers.basic.BasicSpanLayerInitializer; import de.tudarmstadt.ukp.inception.project.initializers.basic.BasicSpanRecommenderInitializer; import de.tudarmstadt.ukp.inception.project.initializers.basic.BasicSpanTagSetInitializer; +import de.tudarmstadt.ukp.inception.project.initializers.basic.CommentFeatureInitializer; import de.tudarmstadt.ukp.inception.recommendation.api.RecommendationService; import de.tudarmstadt.ukp.inception.recommendation.config.RecommenderServiceAutoConfiguration; import de.tudarmstadt.ukp.inception.recommendation.imls.stringmatch.config.StringMatchingRecommenderAutoConfiguration; @@ -70,6 +72,14 @@ public BasicSpanLayerInitializer basicSpanLayerInitializer( return new BasicSpanLayerInitializer(aAnnotationSchemaService); } + @Bean + public CommentFeatureInitializer commentFeatureInitializer( + AnnotationSchemaService aAnnotationSchemaService, + StringFeatureSupport aStringFeatureSupport) + { + return new CommentFeatureInitializer(aAnnotationSchemaService, aStringFeatureSupport); + } + @ConditionalOnBean({ RecommendationService.class, StringMatchingRecommenderFactory.class }) @Bean public BasicSpanRecommenderInitializer basicSpanRecommenderInitializer( diff --git a/inception/inception-project-initializers-basic/src/main/java/de/tudarmstadt/ukp/inception/project/initializers/basic/wicket-package.properties b/inception/inception-project-initializers-basic/src/main/java/de/tudarmstadt/ukp/inception/project/initializers/basic/wicket-package.properties index 564eaa7a418..0370237779f 100644 --- a/inception/inception-project-initializers-basic/src/main/java/de/tudarmstadt/ukp/inception/project/initializers/basic/wicket-package.properties +++ b/inception/inception-project-initializers-basic/src/main/java/de/tudarmstadt/ukp/inception/project/initializers/basic/wicket-package.properties @@ -18,3 +18,5 @@ basic-span-layer.description=Identify spans and classify them using labels. This basic-relation-layer.description=Identify relations between basic span annotations and classify them using labels. This \ is a good starting point for any annotation task involving relations. You can define new relation labels as you go. \ Adding this layer will also add the basic span layer if it is not present in the project yet. + +comment-feature.description=For annotators to leave comments. diff --git a/inception/inception-project/src/main/java/de/tudarmstadt/ukp/clarin/webanno/project/ProjectServiceImpl.java b/inception/inception-project/src/main/java/de/tudarmstadt/ukp/clarin/webanno/project/ProjectServiceImpl.java index 6d3e10fc52d..84a3f0a8685 100644 --- a/inception/inception-project/src/main/java/de/tudarmstadt/ukp/clarin/webanno/project/ProjectServiceImpl.java +++ b/inception/inception-project/src/main/java/de/tudarmstadt/ukp/clarin/webanno/project/ProjectServiceImpl.java @@ -71,9 +71,7 @@ import org.apache.commons.lang3.Validate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationEventPublisher; -import org.springframework.context.annotation.Lazy; import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.context.event.EventListener; import org.springframework.core.annotation.AnnotationAwareOrderComparator; @@ -92,6 +90,7 @@ import de.tudarmstadt.ukp.clarin.webanno.security.UserDao; import de.tudarmstadt.ukp.clarin.webanno.security.model.User; import de.tudarmstadt.ukp.inception.documents.api.RepositoryProperties; +import de.tudarmstadt.ukp.inception.project.api.FeatureInitializer; import de.tudarmstadt.ukp.inception.project.api.ProjectInitializationRequest; import de.tudarmstadt.ukp.inception.project.api.ProjectInitializer; import de.tudarmstadt.ukp.inception.project.api.ProjectService; @@ -120,22 +119,24 @@ public class ProjectServiceImpl private final UserDao userRepository; private final ApplicationEventPublisher applicationEventPublisher; private final RepositoryProperties repositoryProperties; - private final List initializerProxy; + private final List projectInitializerProxy; + private final List featureInitializerProxy; - private List initializers; + private List projectInitializers; + private List featureInitializers; - @Autowired public ProjectServiceImpl(UserDao aUserRepository, ApplicationEventPublisher aApplicationEventPublisher, RepositoryProperties aRepositoryProperties, - @Lazy @Autowired(required = false) List aInitializerProxy, - EntityManager aEntityManager) + List aProjectInitializerProxy, + List aFeatureInitializerProxy, EntityManager aEntityManager) { entityManager = aEntityManager; userRepository = aUserRepository; applicationEventPublisher = aApplicationEventPublisher; repositoryProperties = aRepositoryProperties; - initializerProxy = aInitializerProxy; + projectInitializerProxy = aProjectInitializerProxy; + featureInitializerProxy = aFeatureInitializerProxy; } @Override @@ -806,14 +807,14 @@ public void onContextRefreshedEvent(ContextRefreshedEvent aEvent) /* package private */ void init() { - List inits = new ArrayList<>(); + var projectInits = new ArrayList(); - if (initializerProxy != null) { - inits.addAll(initializerProxy); - AnnotationAwareOrderComparator.sort(inits); + if (projectInitializerProxy != null) { + projectInits.addAll(projectInitializerProxy); + AnnotationAwareOrderComparator.sort(projectInits); - Set> initializerClasses = new HashSet<>(); - for (ProjectInitializer init : inits) { + var initializerClasses = new HashSet>(); + for (var init : projectInits) { if (initializerClasses.add(init.getClass())) { LOG.debug("Found project initializer: {}", ClassUtils.getAbbreviatedName(init.getClass(), 20)); @@ -826,9 +827,33 @@ public void onContextRefreshedEvent(ContextRefreshedEvent aEvent) } } - BaseLoggers.BOOT_LOG.info("Found [{}] project initializers", inits.size()); + BaseLoggers.BOOT_LOG.info("Found [{}] project initializers", projectInits.size()); - initializers = unmodifiableList(inits); + projectInitializers = unmodifiableList(projectInits); + + var featureInits = new ArrayList(); + + if (featureInitializerProxy != null) { + featureInits.addAll(featureInitializerProxy); + AnnotationAwareOrderComparator.sort(featureInits); + + var initializerClasses = new HashSet>(); + for (var init : featureInits) { + if (initializerClasses.add(init.getClass())) { + LOG.debug("Found feature initializer: {}", + ClassUtils.getAbbreviatedName(init.getClass(), 20)); + } + else { + throw new IllegalStateException("There cannot be more than once instance " + + "of each feature initializer class! Duplicate instance of class: " + + init.getClass()); + } + } + } + + BaseLoggers.BOOT_LOG.info("Found [{}] feature initializers", projectInits.size()); + + featureInitializers = unmodifiableList(featureInits); } private void addMissingProjectSlugs() @@ -862,21 +887,27 @@ private String generateRandomSlug() @Override public List listProjectInitializers() { - return initializers; + return projectInitializers; + } + + @Override + public List listFeatureInitializers() + { + return featureInitializers; } @Override @Transactional public void initializeProject(ProjectInitializationRequest aRequest) throws IOException { - initializeProject(aRequest, initializers.stream() // + initializeProject(aRequest, projectInitializers.stream() // .filter(ProjectInitializer::applyByDefault) // .collect(Collectors.toList())); } private ProjectInitializer findProjectInitializer(Class aType) { - return initializers.stream().filter(i -> aType.isAssignableFrom(i.getClass())) // + return projectInitializers.stream().filter(i -> aType.isAssignableFrom(i.getClass())) // .findFirst() // .orElseThrow(() -> new IllegalArgumentException( "No initializer of type [" + aType + "] exists!")); @@ -912,7 +943,7 @@ public void initializeProject(ProjectInitializationRequest aRequest, { var allInits = new HashSet>(); var applied = new HashSet>(); - for (var initializer : initializers) { + for (var initializer : projectInitializers) { allInits.add(initializer.getClass()); if (initializer.alreadyApplied(aRequest.getProject())) { applied.add(initializer.getClass()); diff --git a/inception/inception-project/src/main/java/de/tudarmstadt/ukp/clarin/webanno/project/config/ProjectServiceAutoConfiguration.java b/inception/inception-project/src/main/java/de/tudarmstadt/ukp/clarin/webanno/project/config/ProjectServiceAutoConfiguration.java index 882eec89bde..60c82b407dd 100644 --- a/inception/inception-project/src/main/java/de/tudarmstadt/ukp/clarin/webanno/project/config/ProjectServiceAutoConfiguration.java +++ b/inception/inception-project/src/main/java/de/tudarmstadt/ukp/clarin/webanno/project/config/ProjectServiceAutoConfiguration.java @@ -32,6 +32,7 @@ import de.tudarmstadt.ukp.clarin.webanno.project.exporters.ProjectPermissionsExporter; import de.tudarmstadt.ukp.clarin.webanno.security.UserDao; import de.tudarmstadt.ukp.inception.documents.api.RepositoryProperties; +import de.tudarmstadt.ukp.inception.project.api.FeatureInitializer; import de.tudarmstadt.ukp.inception.project.api.ProjectInitializer; import de.tudarmstadt.ukp.inception.project.api.ProjectService; import jakarta.persistence.EntityManager; @@ -46,10 +47,12 @@ public class ProjectServiceAutoConfiguration public ProjectServiceImpl projectService(UserDao aUserRepository, ApplicationEventPublisher aApplicationEventPublisher, RepositoryProperties aRepositoryProperties, - @Lazy @Autowired(required = false) List aInitializerProxy) + @Lazy @Autowired(required = false) List aProjectInitializerProxy, + @Lazy @Autowired(required = false) List aFeatureInitializerProxy) { return new ProjectServiceImpl(aUserRepository, aApplicationEventPublisher, - aRepositoryProperties, aInitializerProxy, entityManager); + aRepositoryProperties, aProjectInitializerProxy, aFeatureInitializerProxy, + entityManager); } @Bean diff --git a/inception/inception-project/src/test/java/de/tudarmstadt/ukp/clarin/webanno/project/ProjectServiceImplTest.java b/inception/inception-project/src/test/java/de/tudarmstadt/ukp/clarin/webanno/project/ProjectServiceImplTest.java index 0d6f278587d..cd9909954ec 100644 --- a/inception/inception-project/src/test/java/de/tudarmstadt/ukp/clarin/webanno/project/ProjectServiceImplTest.java +++ b/inception/inception-project/src/test/java/de/tudarmstadt/ukp/clarin/webanno/project/ProjectServiceImplTest.java @@ -99,7 +99,7 @@ public void setUp() throws Exception MDC.put(Logging.KEY_REPOSITORY_PATH, repositoryProperties.getPath().toString()); sut = new ProjectServiceImpl(userService, applicationEventPublisher, repositoryProperties, - null, testEntityManager.getEntityManager()); + null, null, testEntityManager.getEntityManager()); // create users beate = new User("beate", Role.ROLE_USER, Role.ROLE_ADMIN); diff --git a/inception/inception-ui-project/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/project/layers/FeatureTemplateSelectedEvent.java b/inception/inception-ui-project/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/project/layers/FeatureTemplateSelectedEvent.java new file mode 100644 index 00000000000..ac9424ac551 --- /dev/null +++ b/inception/inception-ui-project/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/project/layers/FeatureTemplateSelectedEvent.java @@ -0,0 +1,42 @@ +/* + * 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.clarin.webanno.ui.project.layers; + +import org.apache.wicket.ajax.AjaxRequestTarget; +import org.wicketstuff.event.annotation.AbstractAjaxAwareEvent; + +import de.tudarmstadt.ukp.inception.project.api.FeatureInitializer; + +public class FeatureTemplateSelectedEvent + extends AbstractAjaxAwareEvent +{ + private final FeatureInitializer featureInitializer; + + public FeatureTemplateSelectedEvent(AjaxRequestTarget aTarget, + FeatureInitializer aFeatureInitializer) + { + super(aTarget); + + featureInitializer = aFeatureInitializer; + } + + public FeatureInitializer getLayerInitializer() + { + return featureInitializer; + } +} diff --git a/inception/inception-ui-project/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/project/layers/FeatureTemplateSelectionDialogPanel.html b/inception/inception-ui-project/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/project/layers/FeatureTemplateSelectionDialogPanel.html new file mode 100644 index 00000000000..8df27eb444d --- /dev/null +++ b/inception/inception-ui-project/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/project/layers/FeatureTemplateSelectionDialogPanel.html @@ -0,0 +1,65 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/inception/inception-ui-project/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/project/layers/FeatureTemplateSelectionDialogPanel.java b/inception/inception-ui-project/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/project/layers/FeatureTemplateSelectionDialogPanel.java new file mode 100644 index 00000000000..28ffe78a7bb --- /dev/null +++ b/inception/inception-ui-project/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/project/layers/FeatureTemplateSelectionDialogPanel.java @@ -0,0 +1,103 @@ +/* + * 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.clarin.webanno.ui.project.layers; + +import java.lang.invoke.MethodHandles; +import java.util.List; + +import org.apache.wicket.ajax.AjaxRequestTarget; +import org.apache.wicket.event.Broadcast; +import org.apache.wicket.extensions.ajax.markup.html.modal.ModalDialog; +import org.apache.wicket.markup.html.WebMarkupContainer; +import org.apache.wicket.markup.html.basic.Label; +import org.apache.wicket.markup.html.image.Image; +import org.apache.wicket.markup.html.list.ListItem; +import org.apache.wicket.markup.html.list.ListView; +import org.apache.wicket.markup.html.panel.GenericPanel; +import org.apache.wicket.model.IModel; +import org.apache.wicket.request.resource.PackageResourceReference; +import org.apache.wicket.spring.injection.annot.SpringBean; + +import de.tudarmstadt.ukp.clarin.webanno.model.Project; +import de.tudarmstadt.ukp.clarin.webanno.security.UserDao; +import de.tudarmstadt.ukp.inception.project.api.FeatureInitializer; +import de.tudarmstadt.ukp.inception.project.api.ProjectService; +import de.tudarmstadt.ukp.inception.schema.api.config.AnnotationSchemaProperties; +import de.tudarmstadt.ukp.inception.support.lambda.LambdaAjaxLink; +import de.tudarmstadt.ukp.inception.support.markdown.MarkdownLabel; +import de.tudarmstadt.ukp.inception.support.spring.ApplicationEventPublisherHolder; + +public class FeatureTemplateSelectionDialogPanel + extends GenericPanel +{ + private static final long serialVersionUID = 2112018755924139726L; + + private static final PackageResourceReference NO_THUMBNAIL = new PackageResourceReference( + MethodHandles.lookup().lookupClass(), "no-thumbnail.svg"); + + private @SpringBean ProjectService projectService; + private @SpringBean UserDao userRepository; + private @SpringBean AnnotationSchemaProperties annotationEditorProperties; + private @SpringBean ApplicationEventPublisherHolder applicationEventPublisherHolder; + + private LambdaAjaxLink closeDialogButton; + + public FeatureTemplateSelectionDialogPanel(String aId, IModel aProjectModel, + IModel> aInitializers) + { + super(aId, aProjectModel); + + var initializers = new ListView("templates", aInitializers) + { + private static final long serialVersionUID = 1L; + + @Override + protected void populateItem(ListItem aItem) + { + aItem.queue(new LambdaAjaxLink("create", + _target -> actionCreateLayer(_target, aItem.getModelObject()))); + aItem.queue(new Label("name", aItem.getModel().map(FeatureInitializer::getName))); + aItem.queue(new MarkdownLabel("description", + aItem.getModel().map(FeatureInitializer::getDescription) + .map($ -> $.orElse("No description")))); + aItem.queue(new Image("thumbnail", aItem.getModel() + .map(FeatureInitializer::getThumbnail).map($ -> $.orElse(NO_THUMBNAIL)))); + } + }; + queue(initializers); + + var container = new WebMarkupContainer("container"); + container.setOutputMarkupId(true); + queue(container); + + closeDialogButton = new LambdaAjaxLink("closeDialog", this::actionCancel); + closeDialogButton.setOutputMarkupId(true); + queue(closeDialogButton); + } + + private void actionCreateLayer(AjaxRequestTarget aTarget, FeatureInitializer aInitializer) + { + send(this, Broadcast.BUBBLE, new FeatureTemplateSelectedEvent(aTarget, aInitializer)); + findParent(ModalDialog.class).close(aTarget); + } + + protected void actionCancel(AjaxRequestTarget aTarget) + { + findParent(ModalDialog.class).close(aTarget); + } +} diff --git a/inception/inception-ui-project/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/project/layers/ProjectLayersPanel.html b/inception/inception-ui-project/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/project/layers/ProjectLayersPanel.html index 747b47fb7a0..6d051d8af0f 100644 --- a/inception/inception-ui-project/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/project/layers/ProjectLayersPanel.html +++ b/inception/inception-ui-project/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/project/layers/ProjectLayersPanel.html @@ -210,6 +210,7 @@
+
diff --git a/inception/inception-ui-project/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/project/layers/ProjectLayersPanel.java b/inception/inception-ui-project/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/project/layers/ProjectLayersPanel.java index 73ece6c709e..1080ee8fd7f 100644 --- a/inception/inception-ui-project/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/project/layers/ProjectLayersPanel.java +++ b/inception/inception-ui-project/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/project/layers/ProjectLayersPanel.java @@ -81,9 +81,11 @@ import de.tudarmstadt.ukp.clarin.webanno.ui.core.settings.ProjectSettingsPanelBase; import de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.Sentence; import de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.Token; +import de.tudarmstadt.ukp.inception.annotation.layer.chain.ChainLayerSupport; import de.tudarmstadt.ukp.inception.bootstrap.BootstrapFileInputField; import de.tudarmstadt.ukp.inception.bootstrap.BootstrapModalDialog; import de.tudarmstadt.ukp.inception.export.LayerImportExportUtils; +import de.tudarmstadt.ukp.inception.project.api.FeatureInitializer; import de.tudarmstadt.ukp.inception.project.api.ProjectInitializationRequest; import de.tudarmstadt.ukp.inception.project.api.ProjectService; import de.tudarmstadt.ukp.inception.schema.api.AnnotationSchemaService; @@ -169,13 +171,9 @@ public class LayerSelectionPane private static final long serialVersionUID = -1L; private final Map colors = new HashMap<>(); - private final BootstrapModalDialog dialog; - private final Select layerSelection; - private final IModel> layerInitializers; - private final LambdaAjaxLink addButton; public LayerSelectionPane(String id, IModel aModel) @@ -189,14 +187,14 @@ public LayerSelectionPane(String id, IModel aModel) dialog.trapFocus(); queue(dialog); - add(new DocLink("helpLinkLayers", "sect_projects_layers")); + queue(new DocLink("helpLinkLayers", "sect_projects_layers")); - add(new LambdaAjaxLink("create", this::actionCreateLayer)); + queue(new LambdaAjaxLink("create", this::actionCreateLayer)); addButton = new LambdaAjaxLink("add", this::actionAddLayer); addButton.setOutputMarkupPlaceholderTag(true); addButton.add(visibleWhenNot(layerInitializers.map(List::isEmpty))); - add(addButton); + queue(addButton); layerSelection = new Select<>("layerSelection", aModel); var layers = new ListView("layers", LambdaModel.of(this::listLayers)) @@ -223,7 +221,7 @@ public void onComponentTagBody(MarkupStream markupStream, } }; - add(layerSelection.add(layers)); + queue(layerSelection.add(layers)); layerSelection.setOutputMarkupId(true); layerSelection.add(OnChangeAjaxBehavior.onChange(_target -> { featureDetailForm.setModelObject(null); @@ -292,7 +290,7 @@ public void onLayerTemplateSelected(LayerTemplateSelectedEvent aEvent) var request = ProjectInitializationRequest.builder().withProject(getModelObject()) .build(); projectService.initializeProject(request, asList(initializer)); - info("Applying project initializer [" + initializer.getName() + "]"); + success("Applied project initializer [" + initializer.getName() + "]"); } catch (Exception e) { error("Error applying project initializer [" + initializer.getName() + "]: " @@ -441,6 +439,9 @@ public class FeatureSelectionForm private LambdaAjaxLink btnMoveUp; private LambdaAjaxLink btnMoveDown; private ListChoice overviewList; + private final BootstrapModalDialog dialog; + private final IModel> featureInitializers; + private final LambdaAjaxLink addButton; public FeatureSelectionForm(String id, IModel aModel) { @@ -448,6 +449,18 @@ public FeatureSelectionForm(String id, IModel aModel) setOutputMarkupPlaceholderTag(true); + featureInitializers = LoadableDetachableModel.of(this::listFeatureInitializers); + add(LambdaBehavior.onDetach(featureInitializers::detach)); + + dialog = new BootstrapModalDialog(MID_DIALOG); + dialog.trapFocus(); + queue(dialog); + + addButton = new LambdaAjaxLink("add", this::actionAddFeature); + addButton.setOutputMarkupPlaceholderTag(true); + addButton.add(visibleWhenNot(featureInitializers.map(List::isEmpty))); + queue(addButton); + add(new DocLink("featuresHelpLink", "sect_projects_layers_features")); overviewList = new ListChoice("feature") @@ -560,7 +573,7 @@ private void actionCreateFeature(AjaxRequestTarget aTarget) // cancel selection of feature list selectedFeature.setObject(null); - AnnotationFeature newFeature = new AnnotationFeature(); + var newFeature = new AnnotationFeature(); newFeature.setLayer(layerDetailForm.getModelObject()); newFeature.setProject(ProjectLayersPanel.this.getModelObject()); featureDetailForm.setDefaultModelObject(newFeature); @@ -573,14 +586,23 @@ private void actionCreateFeature(AjaxRequestTarget aTarget) + featureDetailForm.getInitialFocusComponent().getMarkupId() + "').focus();")); } + private void actionAddFeature(AjaxRequestTarget aTarget) + { + var dialogContent = new FeatureTemplateSelectionDialogPanel( + BootstrapModalDialog.CONTENT_ID, ProjectLayersPanel.this.getModel(), + featureInitializers); + dialog.open(dialogContent, aTarget); + } + private List listFeatures() { - List features = annotationService + var features = annotationService .listAnnotationFeature(layerDetailForm.getModelObject()); - if (CHAIN_TYPE.equals(layerDetailForm.getModelObject().getType()) + + if (ChainLayerSupport.TYPE.equals(layerDetailForm.getModelObject().getType()) && !layerDetailForm.getModelObject().isLinkedListBehavior()) { - List filtered = new ArrayList<>(); - for (AnnotationFeature f : features) { + var filtered = new ArrayList(); + for (var f : features) { if (!COREFERENCE_RELATION_FEATURE.equals(f.getName())) { filtered.add(f); } @@ -592,6 +614,17 @@ private List listFeatures() } } + private List listFeatureInitializers() + { + if (!selectedLayer.isPresent().getObject()) { + return emptyList(); + } + + return projectService.listFeatureInitializers().stream() + .filter(initializer -> !initializer.alreadyApplied(selectedLayer.getObject())) + .toList(); + } + @Override protected void onConfigure() { @@ -600,5 +633,27 @@ protected void onConfigure() setVisible(selectedLayer.getObject() != null && nonNull(selectedLayer.getObject().getId())); } + + @OnEvent + public void onFeatureTemplateSelected(FeatureTemplateSelectedEvent aEvent) + { + var target = aEvent.getTarget(); + var initializer = aEvent.getLayerInitializer(); + try { + target.add(overviewList); + target.add(addButton); + target.addChildren(getPage(), IFeedback.class); + initializer.configure(selectedLayer.getObject()); + success("Applied feature initializer [" + initializer.getName() + "]"); + } + catch (Exception e) { + error("Error applying feature initializer [" + initializer.getName() + "]: " + + ExceptionUtils.getRootCauseMessage(e)); + LOG.error("Error applying feature initializer {}", initializer, e); + } + + applicationEventPublisherHolder.get().publishEvent(new LayerConfigurationChangedEvent( + this, ProjectLayersPanel.this.getModelObject())); + } } } diff --git a/inception/inception-ui-project/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/project/layers/ProjectLayersPanel.properties b/inception/inception-ui-project/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/project/layers/ProjectLayersPanel.properties index d4ae34fac3e..467711a33b3 100755 --- a/inception/inception-ui-project/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/project/layers/ProjectLayersPanel.properties +++ b/inception/inception-ui-project/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/project/layers/ProjectLayersPanel.properties @@ -64,3 +64,4 @@ DeleteFeatureDialog.title=Confirmation DeleteFeatureDialog.text=Are you sure you want to permanently delete the feature?

This action triggers a forced upgrade of all annotation documents to the modified type system. All users who are have currently opened documents from this project in the editor will be forced to reload the document before they can continue to work. Not reloading the document may lead to undefined situations.

Depending on the number of documents and annotators in this project, completing the deletion of the feature and upgrading all the annotations may take a while. To complete the action, please enter the feature name into the input field below. addLayerDialogTitle=Add pre-defined layer... +addFeatureDialogTitle=Add pre-defined feature...