From e1e9b62ef7675133da19116609ff745337706353 Mon Sep 17 00:00:00 2001 From: Richard Eckart de Castilho Date: Fri, 24 May 2019 14:49:28 +0200 Subject: [PATCH 001/453] #1203 - Improve HTML annotation editor - Display all annotations in the CAS --- .../ukp/inception/htmleditor/HtmlAnnotationEditor.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/inception-html-editor/src/main/java/de/tudarmstadt/ukp/inception/htmleditor/HtmlAnnotationEditor.java b/inception-html-editor/src/main/java/de/tudarmstadt/ukp/inception/htmleditor/HtmlAnnotationEditor.java index e1dd3a87369..5e707bad1e9 100644 --- a/inception-html-editor/src/main/java/de/tudarmstadt/ukp/inception/htmleditor/HtmlAnnotationEditor.java +++ b/inception-html-editor/src/main/java/de/tudarmstadt/ukp/inception/htmleditor/HtmlAnnotationEditor.java @@ -300,8 +300,7 @@ private void read(AjaxRequestTarget aTarget) CAS cas = getCasProvider().get(); VDocument vdoc = new VDocument(); - preRenderer.render(vdoc, aState.getWindowBeginOffset(), aState.getWindowEndOffset(), - cas, getLayersToRender()); + preRenderer.render(vdoc, 0, cas.getDocumentText().length(), cas, getLayersToRender()); List annotations = new ArrayList<>(); From 96599e37206c0973e3a17fb3111aa4bf11ac48d2 Mon Sep 17 00:00:00 2001 From: uwinch Date: Fri, 21 Jun 2019 17:44:51 +0200 Subject: [PATCH 002/453] #1256 Integrate curation into annotation page - init set up - start on new curation tab --- inception-app-webapp/pom.xml | 6 + inception-curation/pom.xml | 37 +++++++ .../curation/CurationEditorExtension.java | 55 +++++++++ .../curation/sidebar/CurationSidebar.html | 42 +++++++ .../curation/sidebar/CurationSidebar.java | 104 ++++++++++++++++++ .../sidebar/CurationSidebar.properties | 19 ++++ .../sidebar/CurationSidebarFactory.java | 61 ++++++++++ .../inception/curation/sidebar/data_table.png | Bin 0 -> 1544 bytes pom.xml | 10 +- 9 files changed, 333 insertions(+), 1 deletion(-) create mode 100644 inception-curation/pom.xml create mode 100644 inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationEditorExtension.java create mode 100644 inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.html create mode 100644 inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java create mode 100644 inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.properties create mode 100644 inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebarFactory.java create mode 100644 inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/data_table.png diff --git a/inception-app-webapp/pom.xml b/inception-app-webapp/pom.xml index 9cb3c8955f9..2784f4752d9 100644 --- a/inception-app-webapp/pom.xml +++ b/inception-app-webapp/pom.xml @@ -47,6 +47,10 @@ de.tudarmstadt.ukp.inception.app inception-image + + de.tudarmstadt.ukp.inception.app + inception-curation + @@ -490,6 +494,7 @@ ${javamelody.version} --> + @@ -684,6 +689,7 @@ de.tudarmstadt.ukp.inception.app:inception-external-search-pubannotation de.tudarmstadt.ukp.inception.app:inception-active-learning de.tudarmstadt.ukp.inception.app:inception-log + de.tudarmstadt.ukp.inception.app:inception-curation de.tudarmstadt.ukp.clarin.webanno:webanno-brat de.tudarmstadt.ukp.inception.app:inception-html-editor diff --git a/inception-curation/pom.xml b/inception-curation/pom.xml new file mode 100644 index 00000000000..817f5902f68 --- /dev/null +++ b/inception-curation/pom.xml @@ -0,0 +1,37 @@ + + 4.0.0 + + de.tudarmstadt.ukp.inception.app + inception-app + 0.10.0-SNAPSHOT + + inception-curation + INCEpTION - Curation + + + de.tudarmstadt.ukp.clarin.webanno + webanno-api-annotation + + + de.tudarmstadt.ukp.clarin.webanno + webanno-model + + + de.tudarmstadt.ukp.clarin.webanno + webanno-ui-annotation + + + + org.springframework + spring-core + + + org.springframework + spring-beans + + + org.springframework + spring-context + + + \ No newline at end of file diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationEditorExtension.java b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationEditorExtension.java new file mode 100644 index 00000000000..995850eae44 --- /dev/null +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationEditorExtension.java @@ -0,0 +1,55 @@ +/* + * Copyright 2019 + * Ubiquitous Knowledge Processing (UKP) Lab + * Technische Universität Darmstadt + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.tudarmstadt.ukp.inception.curation; + +import java.io.IOException; + +import org.apache.uima.cas.CAS; +import org.apache.wicket.ajax.AjaxRequestTarget; + +import de.tudarmstadt.ukp.clarin.webanno.api.annotation.AnnotationEditorExtension; +import de.tudarmstadt.ukp.clarin.webanno.api.annotation.AnnotationEditorExtensionImplBase; +import de.tudarmstadt.ukp.clarin.webanno.api.annotation.action.AnnotationActionHandler; +import de.tudarmstadt.ukp.clarin.webanno.api.annotation.exception.AnnotationException; +import de.tudarmstadt.ukp.clarin.webanno.api.annotation.model.AnnotatorState; +import de.tudarmstadt.ukp.clarin.webanno.api.annotation.model.VID; +import de.tudarmstadt.ukp.clarin.webanno.api.annotation.rendering.model.VDocument; + +public class CurationEditorExtension + extends AnnotationEditorExtensionImplBase + implements AnnotationEditorExtension +{ + + @Override + public void handleAction(AnnotationActionHandler aPanel, AnnotatorState aState, + AjaxRequestTarget aTarget, CAS aCas, VID aParamId, String aAction, int aBegin, int aEnd) + throws AnnotationException, IOException + { + // TODO Auto-generated method stub + + } + + @Override + public void render(CAS aCas, AnnotatorState aState, VDocument aVdoc, int aWindowBeginOffset, + int aWindowEndOffset) + { + // TODO Auto-generated method stub + + } + +} diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.html b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.html new file mode 100644 index 00000000000..3b99da02955 --- /dev/null +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.html @@ -0,0 +1,42 @@ + + + + +
+
+
+

+
+
+
+
+

+
+
+ +
+ +
+
+
+
+
+ \ No newline at end of file diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java new file mode 100644 index 00000000000..800250e7d5f --- /dev/null +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java @@ -0,0 +1,104 @@ +/* + * Copyright 2019 + * Ubiquitous Knowledge Processing (UKP) Lab + * Technische Universität Darmstadt + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.tudarmstadt.ukp.inception.curation.sidebar; + +import java.util.List; +import java.util.stream.Collectors; + +import org.apache.wicket.ajax.AjaxRequestTarget; +import org.apache.wicket.markup.html.WebMarkupContainer; +import org.apache.wicket.markup.html.form.CheckBoxMultipleChoice; +import org.apache.wicket.markup.html.form.ChoiceRenderer; +import org.apache.wicket.markup.html.form.Form; +import org.apache.wicket.model.IModel; +import org.apache.wicket.model.Model; +import org.apache.wicket.spring.injection.annot.SpringBean; + +import de.tudarmstadt.ukp.clarin.webanno.api.CasProvider; +import de.tudarmstadt.ukp.clarin.webanno.api.ProjectService; +import de.tudarmstadt.ukp.clarin.webanno.api.annotation.action.AnnotationActionHandler; +import de.tudarmstadt.ukp.clarin.webanno.api.annotation.model.AnnotatorState; +import de.tudarmstadt.ukp.clarin.webanno.model.PermissionLevel; +import de.tudarmstadt.ukp.clarin.webanno.security.UserDao; +import de.tudarmstadt.ukp.clarin.webanno.security.model.User; +import de.tudarmstadt.ukp.clarin.webanno.support.lambda.LambdaAjaxLink; +import de.tudarmstadt.ukp.clarin.webanno.ui.annotation.AnnotationPage; +import de.tudarmstadt.ukp.clarin.webanno.ui.annotation.sidebar.AnnotationSidebar_ImplBase; + +public class CurationSidebar + extends AnnotationSidebar_ImplBase +{ + private static final long serialVersionUID = -4195790451286055737L; + + private @SpringBean UserDao userRepository; + private @SpringBean ProjectService projectService; + + private CheckBoxMultipleChoice selectedUsers; + private final WebMarkupContainer mainContainer; + + private AnnotatorState state; + private AnnotationPage annoPage; + + public CurationSidebar(String aId, IModel aModel, + AnnotationActionHandler aActionHandler, CasProvider aCasProvider, + AnnotationPage aAnnotationPage) + { + super(aId, aModel, aActionHandler, aCasProvider, aAnnotationPage); + state = aModel.getObject(); + annoPage = aAnnotationPage; + mainContainer = new WebMarkupContainer("mainContainer"); + mainContainer.setOutputMarkupId(true); + add(mainContainer); + + // TODO: put in listview ? + selectedUsers = new CheckBoxMultipleChoice("users", Model.of(), listUsers()); + selectedUsers.setChoiceRenderer(new ChoiceRenderer() { + + private static final long serialVersionUID = -8165699251116827372L; + + @Override + public Object getDisplayValue(User aUser) + { + return aUser.getUsername(); + } + + }); + selectedUsers.setOutputMarkupId(true); + + Form usersForm = new Form("usersForm"); + usersForm.add(new LambdaAjaxLink("showUsers", this::showUsers)); + usersForm.add(selectedUsers); + mainContainer.add(usersForm); + } + + private List listUsers() + { + return projectService + .listProjectUsersWithPermissions(state.getProject(), PermissionLevel.ANNOTATOR) + .stream().filter(user -> !user.equals(userRepository.getCurrentUser())) + .collect(Collectors.toList()); + } + + private void showUsers(AjaxRequestTarget aTarget) + { + // TODO Auto-generated method stub + // refresh should call render of PreRenderer and render of editor-extensions ? + annoPage.actionRefreshDocument(aTarget); + } + +} diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.properties b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.properties new file mode 100644 index 00000000000..4171e2b86ab --- /dev/null +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.properties @@ -0,0 +1,19 @@ +# Copyright 2019 +# Ubiquitous Knowledge Processing (UKP) Lab and FG Language Technology +# Technische Universitt Darmstadt +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +usersLabel=Users +show=Show +curationHeader=Curation \ No newline at end of file diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebarFactory.java b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebarFactory.java new file mode 100644 index 00000000000..d01448613f6 --- /dev/null +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebarFactory.java @@ -0,0 +1,61 @@ +/* + * Copyright 2019 + * Ubiquitous Knowledge Processing (UKP) Lab + * Technische Universität Darmstadt + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.tudarmstadt.ukp.inception.curation.sidebar; + +import org.apache.wicket.model.IModel; +import org.apache.wicket.request.resource.PackageResourceReference; +import org.apache.wicket.request.resource.ResourceReference; +import org.springframework.stereotype.Component; + +import de.tudarmstadt.ukp.clarin.webanno.api.CasProvider; +import de.tudarmstadt.ukp.clarin.webanno.api.annotation.action.AnnotationActionHandler; +import de.tudarmstadt.ukp.clarin.webanno.api.annotation.model.AnnotatorState; +import de.tudarmstadt.ukp.clarin.webanno.ui.annotation.AnnotationPage; +import de.tudarmstadt.ukp.clarin.webanno.ui.annotation.sidebar.AnnotationSidebarFactory_ImplBase; +import de.tudarmstadt.ukp.clarin.webanno.ui.annotation.sidebar.AnnotationSidebar_ImplBase; + +@Component +public class CurationSidebarFactory + extends AnnotationSidebarFactory_ImplBase +{ + + private static final ResourceReference ICON = new PackageResourceReference( + CurationSidebarFactory.class, "data_table.png"); + + @Override + public String getDisplayName() + { + return "Curation"; + } + + @Override + public ResourceReference getIcon() + { + return ICON; + } + + @Override + public AnnotationSidebar_ImplBase create(String aId, IModel aModel, + AnnotationActionHandler aActionHandler, CasProvider aCasProvider, + AnnotationPage aAnnotationPage) + { + return new CurationSidebar(aId, aModel, aActionHandler, aCasProvider, + aAnnotationPage); + } + +} diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/data_table.png b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/data_table.png new file mode 100644 index 0000000000000000000000000000000000000000..5e8c4332c4303ce1b4f0187e3b911d88f2a6f356 GIT binary patch literal 1544 zcmV+j2KV`iP)y6cs@|yXI)E7zXTx#3aWXmu9#lx|@cO z5`w~VDe*EYH7Y1i?{jwH9B|onnkLW8GvB_?^Pcy-=j{7Edo~C_(8EN~LvKKiVe#85 zuE(0oG2K))B&f%kN)AhYaL~|4u+dz4*top{am+WDc`@)ab>{We8o}b0I^df#z_+!4 zMF+;@sWVUVhx=(@IE!zpxQmVD-Ply#BcFAUSmi}$USBQ4bfpYC&jPcL^&on4A>J+O zMNDdux-uqDoq3WU;-`TSquk}^mG=;uaSJoH72(CqZp=`A_N)?PFmdWE@UaNsI`7F_=D#rshFwInj6>{F#qtZk^Fs?uY<51xfM2|65YYn_@ zD#V&YZ3s(oB7CzT)ulmV@~aQEW68GkzXCNd+gylM2iiSxS zR%Hy(oC2JT0e*=?YpNukj`yq*tGwuK%Ci9*Xi;8}yG#U57y;KzG^R*Hr{40c605xE z&63yOK$~d{{`@@)4aLjQSdxIIvX%W{OrCm&X|%q9;Hm469f{mfK<<&g2G}H8wg5Hr zfHU)fx&;~-lW$3rC!pPoVU9Up2B%qu?j#~)P$FbtG&ohB`drD=H8A{*_4zn{@dhR( z^*3Q~e?1PJnek{&ege%KLIegKLR7vp(}>eMVznWRk*$+-0@zst?2>J_t5%0I9zN-$ zY#p<@4WEiiF1Q>i7oYgm5o9?#U^(87iAhD&M()eaLx-6B6+dVfWas2*PasxIVQYx)F7u{yH zX${Z`cQp8SLh92!4UCOf%8^*yiTV3mFz<&Ze6a5V=KttML}CH*99g(47r1msaN7rj z?^YYIY7IooSCHNcEUxkGgyeUN9_4Yb&`F=!OQPf}h)g~84z7FG8-rr%4!ce7gklosmH}m9zMW8b>a7EvkWGXJKl<3K-ixc-&~CHh zs$8I6hOPdl4+u})uAhN6T5;oAaj9v5)9Zw%yiO>w%8SlCy%BkvcwBEpY(QoWmwN== zJ|O&Ubp~_|w2KpV)LC%8csUwN2RWg{Y-QK9TJd zp62xJgu+v2UZ4hY91fI~mSFF9d+XReD{}hZ7RinHTs8*w89_ z?dG5X>;rk~1BQ@JxXuO48Nvz4Q;#(TekJ2@b>8zogwzKNAq%Jd1}v!P#YcmjkTH4c z58A*jxy#NWoRIvj!Mh+%EOwV$oKGG#-{)4=X<$sAdYs3K0}kPX90P_>UXi8~|HFkUcK5akC`ycX>5kIBEP uHOzaWApkinception-layer-docmetadata inception-image inception-scheduling + inception-example-imls-data-majority inception-doc - + + inception-curation @@ -612,6 +614,11 @@ inception-search-mtas 0.10.0-SNAPSHOT + + de.tudarmstadt.ukp.inception.app + inception-curation + 0.10.0-SNAPSHOT + 4.0.0 @@ -23,15 +40,31 @@ org.springframework - spring-core + spring-context - org.springframework - spring-beans + org.apache.uima + uimaj-core - org.springframework - spring-context + de.tudarmstadt.ukp.clarin.webanno + webanno-api + + + de.tudarmstadt.ukp.clarin.webanno + webanno-security + + + org.apache.wicket + wicket-core + + + org.apache.wicket + wicket-spring + + + org.danekja + jdk-serializable-functional \ No newline at end of file diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.html b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.html index 3b99da02955..2340c2bef46 100644 --- a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.html +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.html @@ -28,11 +28,18 @@

-
- +
+ + + + + +
+ +
diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java index 800250e7d5f..169f9ea318f 100644 --- a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java @@ -17,16 +17,21 @@ */ package de.tudarmstadt.ukp.inception.curation.sidebar; +import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.stream.Collectors; -import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.markup.html.WebMarkupContainer; -import org.apache.wicket.markup.html.form.CheckBoxMultipleChoice; -import org.apache.wicket.markup.html.form.ChoiceRenderer; +import org.apache.wicket.markup.html.basic.Label; +import org.apache.wicket.markup.html.form.Check; +import org.apache.wicket.markup.html.form.CheckGroup; import org.apache.wicket.markup.html.form.Form; +import org.apache.wicket.markup.html.list.ListItem; +import org.apache.wicket.markup.html.list.ListView; import org.apache.wicket.model.IModel; -import org.apache.wicket.model.Model; +import org.apache.wicket.model.LoadableDetachableModel; +import org.apache.wicket.model.util.ListModel; import org.apache.wicket.spring.injection.annot.SpringBean; import de.tudarmstadt.ukp.clarin.webanno.api.CasProvider; @@ -36,7 +41,6 @@ import de.tudarmstadt.ukp.clarin.webanno.model.PermissionLevel; import de.tudarmstadt.ukp.clarin.webanno.security.UserDao; import de.tudarmstadt.ukp.clarin.webanno.security.model.User; -import de.tudarmstadt.ukp.clarin.webanno.support.lambda.LambdaAjaxLink; import de.tudarmstadt.ukp.clarin.webanno.ui.annotation.AnnotationPage; import de.tudarmstadt.ukp.clarin.webanno.ui.annotation.sidebar.AnnotationSidebar_ImplBase; @@ -48,8 +52,7 @@ public class CurationSidebar private @SpringBean UserDao userRepository; private @SpringBean ProjectService projectService; - private CheckBoxMultipleChoice selectedUsers; - private final WebMarkupContainer mainContainer; + private CheckGroup selectedUsers; private AnnotatorState state; private AnnotationPage annoPage; @@ -61,32 +64,46 @@ public CurationSidebar(String aId, IModel aModel, super(aId, aModel, aActionHandler, aCasProvider, aAnnotationPage); state = aModel.getObject(); annoPage = aAnnotationPage; - mainContainer = new WebMarkupContainer("mainContainer"); + WebMarkupContainer mainContainer = new WebMarkupContainer("mainContainer"); mainContainer.setOutputMarkupId(true); add(mainContainer); - // TODO: put in listview ? - selectedUsers = new CheckBoxMultipleChoice("users", Model.of(), listUsers()); - selectedUsers.setChoiceRenderer(new ChoiceRenderer() { + // set up user-checklist + Form> usersForm = new Form>("usersForm", + new ListModel(new ArrayList<>())) + { - private static final long serialVersionUID = -8165699251116827372L; + private static final long serialVersionUID = 1L; @Override - public Object getDisplayValue(User aUser) + protected void onSubmit() { - return aUser.getUsername(); + showUsers(); } - - }); - selectedUsers.setOutputMarkupId(true); + + }; + selectedUsers = new CheckGroup("selectedUsers", usersForm.getModelObject()); + ListView users = new ListView("users", + LoadableDetachableModel.of(this::listUsers)) + { + + private static final long serialVersionUID = 1L; + + @Override + protected void populateItem(ListItem aItem) + { + aItem.add(new Check("user", aItem.getModel())); + aItem.add(new Label("name", aItem.getModelObject().getUsername())); + + } + }; + selectedUsers.add(users); - Form usersForm = new Form("usersForm"); - usersForm.add(new LambdaAjaxLink("showUsers", this::showUsers)); usersForm.add(selectedUsers); mainContainer.add(usersForm); } - private List listUsers() + private List listUsers() { return projectService .listProjectUsersWithPermissions(state.getProject(), PermissionLevel.ANNOTATOR) @@ -94,11 +111,16 @@ private List listUsers() .collect(Collectors.toList()); } - private void showUsers(AjaxRequestTarget aTarget) + private void showUsers() { // TODO Auto-generated method stub + // for testing + Collection users = selectedUsers.getModelObject(); + for (User user : users) { + System.out.println(user.getUsername()); + } // refresh should call render of PreRenderer and render of editor-extensions ? - annoPage.actionRefreshDocument(aTarget); + //annoPage.actionRefreshDocument(aTarget); } } From 934245efaab1b56387a1e4d4000d6eb752ed6b33 Mon Sep 17 00:00:00 2001 From: uwinch Date: Thu, 11 Jul 2019 08:09:16 +0200 Subject: [PATCH 004/453] #1256 Integarte curation into annotation page - started on curation service --- .../curation/CurationEditorExtension.java | 28 +++++ .../inception/curation/CurationService.java | 36 ++++++ .../curation/CurationServiceImpl.java | 106 ++++++++++++++++++ .../curation/sidebar/CurationSidebar.java | 17 ++- 4 files changed, 178 insertions(+), 9 deletions(-) create mode 100644 inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationService.java create mode 100644 inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationServiceImpl.java diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationEditorExtension.java b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationEditorExtension.java index 995850eae44..645b180e0df 100644 --- a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationEditorExtension.java +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationEditorExtension.java @@ -18,10 +18,14 @@ package de.tudarmstadt.ukp.inception.curation; import java.io.IOException; +import java.util.Collection; import org.apache.uima.cas.CAS; import org.apache.wicket.ajax.AjaxRequestTarget; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import de.tudarmstadt.ukp.clarin.webanno.api.DocumentService; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.AnnotationEditorExtension; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.AnnotationEditorExtensionImplBase; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.action.AnnotationActionHandler; @@ -29,11 +33,23 @@ import de.tudarmstadt.ukp.clarin.webanno.api.annotation.model.AnnotatorState; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.model.VID; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.rendering.model.VDocument; +import de.tudarmstadt.ukp.clarin.webanno.security.model.User; +@Component(CurationEditorExtension.EXTENSION_ID) public class CurationEditorExtension extends AnnotationEditorExtensionImplBase implements AnnotationEditorExtension { + public static final String EXTENSION_ID = "curationEditorExtension"; + + private @Autowired DocumentService documentService; + private @Autowired CurationService curationService; + + @Override + public String getBeanName() + { + return EXTENSION_ID; + } @Override public void handleAction(AnnotationActionHandler aPanel, AnnotatorState aState, @@ -52,4 +68,16 @@ public void render(CAS aCas, AnnotatorState aState, VDocument aVdoc, int aWindow } + public void selectedUsersChanged(AnnotatorState aAnnotatorState, Collection aUsers) + { + System.out.println("CurrentExtension: " + this.toString()); + System.out.println("Currentuser: " + aAnnotatorState.getUser().getUsername()); + for (User user : aUsers) { + + System.out.println(user.getUsername()); + } + } + + + } diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationService.java b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationService.java new file mode 100644 index 00000000000..716b17ecbe6 --- /dev/null +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationService.java @@ -0,0 +1,36 @@ +/* + * Copyright 2019 + * Ubiquitous Knowledge Processing (UKP) Lab + * Technische Universität Darmstadt + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.tudarmstadt.ukp.inception.curation; + +import java.util.List; + +import de.tudarmstadt.ukp.clarin.webanno.model.Project; +import de.tudarmstadt.ukp.clarin.webanno.security.model.User; + +public interface CurationService +{ + /** + * List users that were selected to be shown for curation by the given user + */ + public List listUsersSelectedForCuration(User aCurrentUser, Project project); + + /** + * Store the users that were selected to be shown for curation by the given user + */ + public void updateUsersSelectedForCuration(User aCurrentUser, Project project); +} diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationServiceImpl.java b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationServiceImpl.java new file mode 100644 index 00000000000..51dc1f58705 --- /dev/null +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationServiceImpl.java @@ -0,0 +1,106 @@ +/* + * Copyright 2019 + * Ubiquitous Knowledge Processing (UKP) Lab + * Technische Universität Darmstadt + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.tudarmstadt.ukp.inception.curation; + +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.springframework.stereotype.Component; + +import de.tudarmstadt.ukp.clarin.webanno.model.Project; +import de.tudarmstadt.ukp.clarin.webanno.security.model.User; + +@Component +public class CurationServiceImpl implements CurationService +{ + // stores info on which users are selected and which doc is the curation-doc + private ConcurrentMap curationStates; + + public CurationServiceImpl() + { + // TODO Auto-generated constructor stub + curationStates = new ConcurrentHashMap<>(); + } + + /** + * Key to identify curation session for a specific user and project + */ + private class CurationStateKey + { + private String username; + private long projectId; + + public CurationStateKey(User aUser, Project aProject) { + username = aUser.getUsername(); + projectId = aProject.getId(); + } + + public String getUsername() + { + return username; + } + + public long getProjectId() + { + return projectId; + } + + @Override + public int hashCode() + { + return new HashCodeBuilder().append(username).append(projectId).toHashCode(); + } + + @Override + public boolean equals(Object aOther) + { + if (!(aOther instanceof CurationStateKey)) { + return false; + } + CurationStateKey castOther = (CurationStateKey) aOther; + return new EqualsBuilder().append(username, castOther.username) + .append(projectId, castOther.projectId).isEquals(); + } + } + + private CurationState getCurationState(User aUser, Project aProject) { + return curationStates.get(new CurationStateKey(aUser, aProject)); + } + + private class CurationState + { + //TODO + } + + @Override + public List listUsersSelectedForCuration(User aCurrentUser, Project aProject) + { + // TODO Auto-generated method stub + return null; + } + + @Override + public void updateUsersSelectedForCuration(User aCurrentUser, Project aProject) + { + // TODO Auto-generated method stub + + } +} diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java index 169f9ea318f..2592fee1e80 100644 --- a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java @@ -18,7 +18,6 @@ package de.tudarmstadt.ukp.inception.curation.sidebar; import java.util.ArrayList; -import java.util.Collection; import java.util.List; import java.util.stream.Collectors; @@ -36,6 +35,7 @@ import de.tudarmstadt.ukp.clarin.webanno.api.CasProvider; import de.tudarmstadt.ukp.clarin.webanno.api.ProjectService; +import de.tudarmstadt.ukp.clarin.webanno.api.annotation.AnnotationEditorExtensionRegistry; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.action.AnnotationActionHandler; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.model.AnnotatorState; import de.tudarmstadt.ukp.clarin.webanno.model.PermissionLevel; @@ -43,6 +43,7 @@ import de.tudarmstadt.ukp.clarin.webanno.security.model.User; import de.tudarmstadt.ukp.clarin.webanno.ui.annotation.AnnotationPage; import de.tudarmstadt.ukp.clarin.webanno.ui.annotation.sidebar.AnnotationSidebar_ImplBase; +import de.tudarmstadt.ukp.inception.curation.CurationEditorExtension; public class CurationSidebar extends AnnotationSidebar_ImplBase @@ -51,11 +52,12 @@ public class CurationSidebar private @SpringBean UserDao userRepository; private @SpringBean ProjectService projectService; + private @SpringBean AnnotationEditorExtensionRegistry extensionRegistry; private CheckGroup selectedUsers; private AnnotatorState state; - private AnnotationPage annoPage; +// private AnnotationPage annoPage; public CurationSidebar(String aId, IModel aModel, AnnotationActionHandler aActionHandler, CasProvider aCasProvider, @@ -63,7 +65,7 @@ public CurationSidebar(String aId, IModel aModel, { super(aId, aModel, aActionHandler, aCasProvider, aAnnotationPage); state = aModel.getObject(); - annoPage = aAnnotationPage; +// annoPage = aAnnotationPage; WebMarkupContainer mainContainer = new WebMarkupContainer("mainContainer"); mainContainer.setOutputMarkupId(true); add(mainContainer); @@ -113,12 +115,9 @@ private List listUsers() private void showUsers() { - // TODO Auto-generated method stub - // for testing - Collection users = selectedUsers.getModelObject(); - for (User user : users) { - System.out.println(user.getUsername()); - } + ((CurationEditorExtension) extensionRegistry + .getExtension(CurationEditorExtension.EXTENSION_ID)) + .selectedUsersChanged(getModelObject(), selectedUsers.getModelObject()); // refresh should call render of PreRenderer and render of editor-extensions ? //annoPage.actionRefreshDocument(aTarget); } From d205facacedb9fefefbf790de0cbd5a3e3903c12 Mon Sep 17 00:00:00 2001 From: uwinch Date: Fri, 12 Jul 2019 17:40:30 +0200 Subject: [PATCH 005/453] #1256 Integrate curation into annotation page - added curation target choice to ui - saving users and curation target per user --- .../curation/CurationEditorExtension.java | 32 ++++-- .../inception/curation/CurationService.java | 28 ++++- .../curation/CurationServiceImpl.java | 102 ++++++++++++++---- .../curation/sidebar/CurationSidebar.html | 12 +++ .../curation/sidebar/CurationSidebar.java | 77 +++++++++++-- .../sidebar/CurationSidebar.properties | 5 +- 6 files changed, 208 insertions(+), 48 deletions(-) diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationEditorExtension.java b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationEditorExtension.java index 645b180e0df..3038cc1b6d8 100644 --- a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationEditorExtension.java +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationEditorExtension.java @@ -18,14 +18,14 @@ package de.tudarmstadt.ukp.inception.curation; import java.io.IOException; -import java.util.Collection; +import java.util.List; +import java.util.Optional; import org.apache.uima.cas.CAS; import org.apache.wicket.ajax.AjaxRequestTarget; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; -import de.tudarmstadt.ukp.clarin.webanno.api.DocumentService; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.AnnotationEditorExtension; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.AnnotationEditorExtensionImplBase; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.action.AnnotationActionHandler; @@ -42,7 +42,6 @@ public class CurationEditorExtension { public static final String EXTENSION_ID = "curationEditorExtension"; - private @Autowired DocumentService documentService; private @Autowired CurationService curationService; @Override @@ -65,18 +64,29 @@ public void render(CAS aCas, AnnotatorState aState, VDocument aVdoc, int aWindow int aWindowEndOffset) { // TODO Auto-generated method stub - - } - - public void selectedUsersChanged(AnnotatorState aAnnotatorState, Collection aUsers) - { + String currentUser = aState.getUser().getUsername(); System.out.println("CurrentExtension: " + this.toString()); - System.out.println("Currentuser: " + aAnnotatorState.getUser().getUsername()); - for (User user : aUsers) { - + System.out.println("Currentuser: " + currentUser); + Optional> selectedUsers = curationService + .listUsersSelectedForCuration(currentUser, aState.getProject().getId()); + if (!selectedUsers.isPresent()) { + return; + } + + for (User user : selectedUsers.get()) { System.out.println(user.getUsername()); } } + +// public void selectedUsersChanged(AnnotatorState aAnnotatorState, Collection aUsers) +// { +// System.out.println("CurrentExtension: " + this.toString()); +// System.out.println("Currentuser: " + aAnnotatorState.getUser().getUsername()); +// for (User user : aUsers) { +// +// System.out.println(user.getUsername()); +// } +// } diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationService.java b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationService.java index 716b17ecbe6..f6354f34b80 100644 --- a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationService.java +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationService.java @@ -17,9 +17,14 @@ */ package de.tudarmstadt.ukp.inception.curation; +import java.io.IOException; +import java.util.Collection; import java.util.List; +import java.util.Optional; -import de.tudarmstadt.ukp.clarin.webanno.model.Project; +import org.apache.uima.cas.CAS; + +import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.clarin.webanno.security.model.User; public interface CurationService @@ -27,10 +32,27 @@ public interface CurationService /** * List users that were selected to be shown for curation by the given user */ - public List listUsersSelectedForCuration(User aCurrentUser, Project project); + public Optional> listUsersSelectedForCuration(String aCurrentUser, long aProjectId); + + /** + * retrieves CAS associated with curation doc for the given user + */ + public Optional retrieveCurationCAS(String aUser, long aProjectId) throws IOException; /** * Store the users that were selected to be shown for curation by the given user */ - public void updateUsersSelectedForCuration(User aCurrentUser, Project project); + public void updateUsersSelectedForCuration(String aCurrentUser, long aProjectId, + Collection aUsers); + + /** + * Store document that curated items should be saved to + */ + public void updateCurationDoc(String aCurrentUser, long aProjectId, + SourceDocument aCurationDoc); + + /** + * Removed stored curation information after user session has ended + */ + public void removeCurrentUserInformation(String aCurrentUser, long aProjectId); } diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationServiceImpl.java b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationServiceImpl.java index 51dc1f58705..0ae6fc99c3b 100644 --- a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationServiceImpl.java +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationServiceImpl.java @@ -17,15 +17,22 @@ */ package de.tudarmstadt.ukp.inception.curation; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; import java.util.List; +import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.apache.uima.cas.CAS; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; -import de.tudarmstadt.ukp.clarin.webanno.model.Project; +import de.tudarmstadt.ukp.clarin.webanno.api.DocumentService; +import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.clarin.webanno.security.model.User; @Component @@ -33,10 +40,11 @@ public class CurationServiceImpl implements CurationService { // stores info on which users are selected and which doc is the curation-doc private ConcurrentMap curationStates; + + private @Autowired DocumentService documentService; public CurationServiceImpl() { - // TODO Auto-generated constructor stub curationStates = new ConcurrentHashMap<>(); } @@ -48,19 +56,9 @@ private class CurationStateKey private String username; private long projectId; - public CurationStateKey(User aUser, Project aProject) { - username = aUser.getUsername(); - projectId = aProject.getId(); - } - - public String getUsername() - { - return username; - } - - public long getProjectId() - { - return projectId; + public CurationStateKey(String aUser, long aProject) { + username = aUser; + projectId = aProject; } @Override @@ -81,26 +79,84 @@ public boolean equals(Object aOther) } } - private CurationState getCurationState(User aUser, Project aProject) { - return curationStates.get(new CurationStateKey(aUser, aProject)); + private CurationState getCurationState(String aUser, long aProjectId) { + synchronized (curationStates) { + return curationStates.computeIfAbsent(new CurationStateKey(aUser, aProjectId), + key -> new CurationState()); + } } private class CurationState { - //TODO + private List selectedUsers; + // source document of the curated document + private SourceDocument curationDoc; + + public List getSelectedUsers() + { + return selectedUsers; + } + + public void setSelectedUsers(Collection aSelectedUsers) + { + selectedUsers = new ArrayList<>(aSelectedUsers); + } + + public SourceDocument getCurationDoc() + { + return curationDoc; + } + + public void setCurationDoc(SourceDocument aCurationDoc) + { + curationDoc = aCurationDoc; + } } @Override - public List listUsersSelectedForCuration(User aCurrentUser, Project aProject) + public Optional> listUsersSelectedForCuration(String aCurrentUser, long aProjectId) { - // TODO Auto-generated method stub - return null; + return Optional.ofNullable(getCurationState(aCurrentUser, aProjectId).getSelectedUsers()); } @Override - public void updateUsersSelectedForCuration(User aCurrentUser, Project aProject) + public Optional retrieveCurationCAS(String aUser, long aProjectId) throws IOException { - // TODO Auto-generated method stub + SourceDocument doc = getCurationState(aUser, aProjectId).getCurationDoc(); + if (doc == null) { + return Optional.empty(); + } + return Optional.of(documentService + .readAnnotationCas(doc, aUser)); } + + @Override + public void updateUsersSelectedForCuration(String aCurrentUser, long aProjectId, + Collection aSelectedUsers) + { + synchronized (curationStates) + { + getCurationState(aCurrentUser, aProjectId).setSelectedUsers(aSelectedUsers); + } + } + + @Override + public void updateCurationDoc(String aCurrentUser, long aProjectId, SourceDocument aCurationDoc) + { + synchronized (curationStates) + { + getCurationState(aCurrentUser, aProjectId).setCurationDoc(aCurationDoc); + } + } + + @Override + public void removeCurrentUserInformation(String aCurrentUser, long aProjectId) + { + synchronized (curationStates) + { + curationStates.remove(new CurationStateKey(aCurrentUser, aProjectId)); + } + } + } diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.html b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.html index 2340c2bef46..eba2470070a 100644 --- a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.html +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.html @@ -42,6 +42,18 @@

+
+
+

+
+
+ + +
+ +
diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java index 2592fee1e80..0147f1db1e9 100644 --- a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java @@ -17,8 +17,13 @@ */ package de.tudarmstadt.ukp.inception.curation.sidebar; +import static de.tudarmstadt.ukp.clarin.webanno.api.WebAnnoConst.CURATION_USER; + import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; import java.util.List; +import java.util.Optional; import java.util.stream.Collectors; import org.apache.wicket.markup.html.WebMarkupContainer; @@ -26,11 +31,12 @@ import org.apache.wicket.markup.html.form.Check; import org.apache.wicket.markup.html.form.CheckGroup; import org.apache.wicket.markup.html.form.Form; +import org.apache.wicket.markup.html.form.RadioChoice; import org.apache.wicket.markup.html.list.ListItem; import org.apache.wicket.markup.html.list.ListView; import org.apache.wicket.model.IModel; import org.apache.wicket.model.LoadableDetachableModel; -import org.apache.wicket.model.util.ListModel; +import org.apache.wicket.model.PropertyModel; import org.apache.wicket.spring.injection.annot.SpringBean; import de.tudarmstadt.ukp.clarin.webanno.api.CasProvider; @@ -39,22 +45,30 @@ import de.tudarmstadt.ukp.clarin.webanno.api.annotation.action.AnnotationActionHandler; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.model.AnnotatorState; import de.tudarmstadt.ukp.clarin.webanno.model.PermissionLevel; +import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.clarin.webanno.security.UserDao; import de.tudarmstadt.ukp.clarin.webanno.security.model.User; import de.tudarmstadt.ukp.clarin.webanno.ui.annotation.AnnotationPage; import de.tudarmstadt.ukp.clarin.webanno.ui.annotation.sidebar.AnnotationSidebar_ImplBase; -import de.tudarmstadt.ukp.inception.curation.CurationEditorExtension; +import de.tudarmstadt.ukp.inception.curation.CurationService; + public class CurationSidebar extends AnnotationSidebar_ImplBase { private static final long serialVersionUID = -4195790451286055737L; + private static final String DEFAULT_CURATION_TARGET = "my document"; private @SpringBean UserDao userRepository; private @SpringBean ProjectService projectService; private @SpringBean AnnotationEditorExtensionRegistry extensionRegistry; + private @SpringBean CurationService curationService; private CheckGroup selectedUsers; + + private final List curationTargets = Arrays + .asList(new String[] { DEFAULT_CURATION_TARGET, "curation document" }); + private String selectedCurationTarget = DEFAULT_CURATION_TARGET; private AnnotatorState state; // private AnnotationPage annoPage; @@ -71,10 +85,45 @@ public CurationSidebar(String aId, IModel aModel, add(mainContainer); // set up user-checklist + Form> usersForm = createUserSelection(); + mainContainer.add(usersForm); + + // set up curation target radio button + Form targetForm = new Form("settingsForm") { + + private static final long serialVersionUID = -5535838955781542216L; + + @Override + protected void onSubmit() + { + updateSettings(); + } + }; + RadioChoice curationTargetBtn = new RadioChoice("curationTargetRadioBtn", + new PropertyModel(this, "selectedCurationTarget"), curationTargets); + targetForm.add(curationTargetBtn); + mainContainer.add(targetForm); + } + + private void updateSettings() + { + SourceDocument doc = state.getDocument(); + long project = state.getProject().getId(); + + if (selectedCurationTarget.equals(DEFAULT_CURATION_TARGET)) { + curationService.updateCurationDoc(userRepository.getCurrentUser().getUsername(), + project, doc); + } + else { + curationService.updateCurationDoc(CURATION_USER, project, doc); + } + } + + private Form> createUserSelection() + { Form> usersForm = new Form>("usersForm", - new ListModel(new ArrayList<>())) + LoadableDetachableModel.of(this::listSelectedUsers)) { - private static final long serialVersionUID = 1L; @Override @@ -88,7 +137,6 @@ protected void onSubmit() ListView users = new ListView("users", LoadableDetachableModel.of(this::listUsers)) { - private static final long serialVersionUID = 1L; @Override @@ -100,9 +148,18 @@ protected void populateItem(ListItem aItem) } }; selectedUsers.add(users); - usersForm.add(selectedUsers); - mainContainer.add(usersForm); + return usersForm; + } + + private List listSelectedUsers() + { + Optional> users = curationService.listUsersSelectedForCuration( + userRepository.getCurrentUser().getUsername(), state.getProject().getId()); + if (!users.isPresent()) { + return new ArrayList<>(); + } + return users.get(); } private List listUsers() @@ -115,9 +172,9 @@ private List listUsers() private void showUsers() { - ((CurationEditorExtension) extensionRegistry - .getExtension(CurationEditorExtension.EXTENSION_ID)) - .selectedUsersChanged(getModelObject(), selectedUsers.getModelObject()); + Collection users = selectedUsers.getModelObject(); + curationService.updateUsersSelectedForCuration( + userRepository.getCurrentUser().getUsername(), state.getProject().getId(), users); // refresh should call render of PreRenderer and render of editor-extensions ? //annoPage.actionRefreshDocument(aTarget); } diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.properties b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.properties index 4171e2b86ab..eb3736bbccd 100644 --- a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.properties +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.properties @@ -15,5 +15,8 @@ # limitations under the License. # usersLabel=Users +settingsLabel=Settings show=Show -curationHeader=Curation \ No newline at end of file +apply=Apply +curationHeader=Curation +curationTarget=Save as \ No newline at end of file From 413357cc0e7db0ebf420041eb2809779389bca77 Mon Sep 17 00:00:00 2001 From: uwinch Date: Tue, 16 Jul 2019 15:40:40 +0200 Subject: [PATCH 006/453] #1256 Integrate curation into annotation page - fix getting cases for render --- inception-app-webapp/pom.xml | 11 ++-- .../tudarmstadt/ukp/inception/INCEpTION.java | 5 +- inception-curation/pom.xml | 6 +- .../curation/CurationEditorExtension.java | 55 +++++++++++++------ .../inception/curation/CurationService.java | 9 +-- .../curation/CurationServiceImpl.java | 31 +++++++---- .../curation/sidebar/CurationSidebar.java | 32 +++++++++-- 7 files changed, 103 insertions(+), 46 deletions(-) diff --git a/inception-app-webapp/pom.xml b/inception-app-webapp/pom.xml index 38915aceaef..0deadf1b200 100644 --- a/inception-app-webapp/pom.xml +++ b/inception-app-webapp/pom.xml @@ -52,10 +52,6 @@ de.tudarmstadt.ukp.inception.app inception-image - - de.tudarmstadt.ukp.inception.app - inception-curation - @@ -503,7 +499,12 @@ ${javamelody.version} --> - + + + de.tudarmstadt.ukp.inception.app + inception-curation + 0.11.0-SNAPSHOT + diff --git a/inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/INCEpTION.java b/inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/INCEpTION.java index c5bbc174421..22416b6a01a 100644 --- a/inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/INCEpTION.java +++ b/inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/INCEpTION.java @@ -56,6 +56,7 @@ import de.tudarmstadt.ukp.clarin.webanno.support.standalone.LoadingSplashScreen; import de.tudarmstadt.ukp.clarin.webanno.support.standalone.ShutdownDialogAvailableEvent; import de.tudarmstadt.ukp.clarin.webanno.ui.annotation.AnnotationPageMenuItem; +import de.tudarmstadt.ukp.clarin.webanno.ui.curation.page.CurationPageMenuItem; import de.tudarmstadt.ukp.clarin.webanno.ui.monitoring.page.AgreementPageMenuItem; import de.tudarmstadt.ukp.clarin.webanno.ui.monitoring.page.MonitoringPageMenuItem; import de.tudarmstadt.ukp.inception.app.config.InceptionApplicationContextInitializer; @@ -82,7 +83,9 @@ AutomationMiraTemplateExporter.class, AutomationTrainingDocumentExporter.class, // INCEpTION uses the original DKPro Core CoNLL-U components - ConllUFormatSupport.class + ConllUFormatSupport.class, + // exclude curation from dashboard + CurationPageMenuItem.class })}) @EntityScan(basePackages = { // Include WebAnno entity packages separately so we can skip the automation entities! diff --git a/inception-curation/pom.xml b/inception-curation/pom.xml index 1b9a2c8755e..65037ae72e9 100644 --- a/inception-curation/pom.xml +++ b/inception-curation/pom.xml @@ -20,7 +20,7 @@ de.tudarmstadt.ukp.inception.app inception-app - 0.10.0-SNAPSHOT + 0.11.0-SNAPSHOT inception-curation INCEpTION - Curation @@ -66,5 +66,9 @@ org.danekja jdk-serializable-functional + + de.tudarmstadt.ukp.clarin.webanno + webanno-brat + \ No newline at end of file diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationEditorExtension.java b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationEditorExtension.java index 3038cc1b6d8..1b30a680f1e 100644 --- a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationEditorExtension.java +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationEditorExtension.java @@ -23,6 +23,8 @@ import org.apache.uima.cas.CAS; import org.apache.wicket.ajax.AjaxRequestTarget; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -32,7 +34,9 @@ import de.tudarmstadt.ukp.clarin.webanno.api.annotation.exception.AnnotationException; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.model.AnnotatorState; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.model.VID; +import de.tudarmstadt.ukp.clarin.webanno.api.annotation.rendering.PreRenderer; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.rendering.model.VDocument; +import de.tudarmstadt.ukp.clarin.webanno.brat.message.SpanAnnotationResponse; import de.tudarmstadt.ukp.clarin.webanno.security.model.User; @Component(CurationEditorExtension.EXTENSION_ID) @@ -42,7 +46,10 @@ public class CurationEditorExtension { public static final String EXTENSION_ID = "curationEditorExtension"; + private Logger log = LoggerFactory.getLogger(getClass()); + private @Autowired CurationService curationService; + private @Autowired PreRenderer preRenderer; @Override public String getBeanName() @@ -55,8 +62,16 @@ public void handleAction(AnnotationActionHandler aPanel, AnnotatorState aState, AjaxRequestTarget aTarget, CAS aCas, VID aParamId, String aAction, int aBegin, int aEnd) throws AnnotationException, IOException { - // TODO Auto-generated method stub - + // only process actions relevant curation + if (!aParamId.getExtensionId().equals(EXTENSION_ID)) { + return; + } + // Annotation has been selected for gold + if (SpanAnnotationResponse.is(aAction)) { //TODO is this action only for spans + //, what about relations + // TODO: store annotation in user CAS + } + } @Override @@ -65,29 +80,33 @@ public void render(CAS aCas, AnnotatorState aState, VDocument aVdoc, int aWindow { // TODO Auto-generated method stub String currentUser = aState.getUser().getUsername(); - System.out.println("CurrentExtension: " + this.toString()); - System.out.println("Currentuser: " + currentUser); + long projectId = aState.getProject().getId(); Optional> selectedUsers = curationService - .listUsersSelectedForCuration(currentUser, aState.getProject().getId()); + .listUsersSelectedForCuration(currentUser, projectId); if (!selectedUsers.isPresent()) { return; } for (User user : selectedUsers.get()) { - System.out.println(user.getUsername()); + try { + Optional userCas = curationService.retrieveCurationCAS(user.getUsername(), + projectId, aState.getDocument()); + if (!userCas.isPresent()) { + log.error(String.format("Could not retrieve CAS for user %s and project %d", + user.getUsername(), projectId)); + continue; + } + // FIXME cannot add the same annotations, change VID ? + preRenderer.render(aVdoc, aWindowBeginOffset, aWindowEndOffset, userCas.get(), + aState.getAnnotationLayers()); //TODO: might need to filter the layers + } + catch (IOException e) { + log.error(String.format("Could not retrieve CAS for user %s and project %d", + user.getUsername(), projectId)); + e.printStackTrace(); + } + } } -// public void selectedUsersChanged(AnnotatorState aAnnotatorState, Collection aUsers) -// { -// System.out.println("CurrentExtension: " + this.toString()); -// System.out.println("Currentuser: " + aAnnotatorState.getUser().getUsername()); -// for (User user : aUsers) { -// -// System.out.println(user.getUsername()); -// } -// } - - - } diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationService.java b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationService.java index f6354f34b80..4dd28cfc765 100644 --- a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationService.java +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationService.java @@ -37,7 +37,8 @@ public interface CurationService /** * retrieves CAS associated with curation doc for the given user */ - public Optional retrieveCurationCAS(String aUser, long aProjectId) throws IOException; + public Optional retrieveCurationCAS(String aUser, long aProjectId, SourceDocument aDoc) + throws IOException; /** * Store the users that were selected to be shown for curation by the given user @@ -46,10 +47,10 @@ public void updateUsersSelectedForCuration(String aCurrentUser, long aProjectId, Collection aUsers); /** - * Store document that curated items should be saved to + * Store which name the curated document should be associated with */ - public void updateCurationDoc(String aCurrentUser, long aProjectId, - SourceDocument aCurationDoc); + public void updateCurationName(String aCurrentUser, long aProjectId, + String aCurationName); /** * Removed stored curation information after user session has ended diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationServiceImpl.java b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationServiceImpl.java index 0ae6fc99c3b..a457a3d95a9 100644 --- a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationServiceImpl.java +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationServiceImpl.java @@ -82,7 +82,7 @@ public boolean equals(Object aOther) private CurationState getCurationState(String aUser, long aProjectId) { synchronized (curationStates) { return curationStates.computeIfAbsent(new CurationStateKey(aUser, aProjectId), - key -> new CurationState()); + key -> new CurationState(aUser)); } } @@ -90,8 +90,14 @@ private class CurationState { private List selectedUsers; // source document of the curated document - private SourceDocument curationDoc; + // the curationdoc can be retrieved from user (CURATION or current) and projectId + private String curationUser; + public CurationState(String aUser) + { + curationUser = aUser; + } + public List getSelectedUsers() { return selectedUsers; @@ -102,14 +108,14 @@ public void setSelectedUsers(Collection aSelectedUsers) selectedUsers = new ArrayList<>(aSelectedUsers); } - public SourceDocument getCurationDoc() + public String getCurationName() { - return curationDoc; + return curationUser; } - public void setCurationDoc(SourceDocument aCurationDoc) + public void setCurationName(String aCurationName) { - curationDoc = aCurationDoc; + curationUser = aCurationName; } } @@ -120,15 +126,16 @@ public Optional> listUsersSelectedForCuration(String aCurrentUser, lo } @Override - public Optional retrieveCurationCAS(String aUser, long aProjectId) throws IOException + public Optional retrieveCurationCAS(String aUser, long aProjectId, SourceDocument aDoc) + throws IOException { - SourceDocument doc = getCurationState(aUser, aProjectId).getCurationDoc(); - if (doc == null) { + String curationUser = getCurationState(aUser, aProjectId).getCurationName(); + if (curationUser == null) { return Optional.empty(); } return Optional.of(documentService - .readAnnotationCas(doc, aUser)); + .readAnnotationCas(aDoc, curationUser)); } @Override @@ -142,11 +149,11 @@ public void updateUsersSelectedForCuration(String aCurrentUser, long aProjectId, } @Override - public void updateCurationDoc(String aCurrentUser, long aProjectId, SourceDocument aCurationDoc) + public void updateCurationName(String aCurrentUser, long aProjectId, String aUserName) { synchronized (curationStates) { - getCurationState(aCurrentUser, aProjectId).setCurationDoc(aCurationDoc); + getCurationState(aCurrentUser, aProjectId).setCurationName(aUserName);; } } diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java index 0147f1db1e9..80ee92f21c0 100644 --- a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java @@ -40,10 +40,12 @@ import org.apache.wicket.spring.injection.annot.SpringBean; import de.tudarmstadt.ukp.clarin.webanno.api.CasProvider; +import de.tudarmstadt.ukp.clarin.webanno.api.DocumentService; import de.tudarmstadt.ukp.clarin.webanno.api.ProjectService; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.AnnotationEditorExtensionRegistry; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.action.AnnotationActionHandler; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.model.AnnotatorState; +import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationDocumentState; import de.tudarmstadt.ukp.clarin.webanno.model.PermissionLevel; import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.clarin.webanno.security.UserDao; @@ -63,6 +65,7 @@ public class CurationSidebar private @SpringBean ProjectService projectService; private @SpringBean AnnotationEditorExtensionRegistry extensionRegistry; private @SpringBean CurationService curationService; + private @SpringBean DocumentService documentService; private CheckGroup selectedUsers; @@ -107,15 +110,16 @@ protected void onSubmit() private void updateSettings() { - SourceDocument doc = state.getDocument(); + String currentUsername = userRepository.getCurrentUser().getUsername(); long project = state.getProject().getId(); if (selectedCurationTarget.equals(DEFAULT_CURATION_TARGET)) { - curationService.updateCurationDoc(userRepository.getCurrentUser().getUsername(), - project, doc); + + curationService.updateCurationName(currentUsername, + project, currentUsername); } else { - curationService.updateCurationDoc(CURATION_USER, project, doc); + curationService.updateCurationName(currentUsername, project, CURATION_USER); } } @@ -162,14 +166,32 @@ private List listSelectedUsers() return users.get(); } + /** + * retrieve annotators of this document which finished annotating + */ private List listUsers() { return projectService .listProjectUsersWithPermissions(state.getProject(), PermissionLevel.ANNOTATOR) - .stream().filter(user -> !user.equals(userRepository.getCurrentUser())) + .stream().filter(user -> !user.equals(userRepository.getCurrentUser()) + && hasFinishedDoc(user)) .collect(Collectors.toList()); } + private boolean hasFinishedDoc(User aUser) + { + SourceDocument doc = state.getDocument(); + String username = aUser.getUsername(); + if (documentService.existsAnnotationDocument(doc, username) && + documentService.getAnnotationDocument(doc, username).getState() + .equals(AnnotationDocumentState.FINISHED)) { + return true; + } + else { + return false; + } + } + private void showUsers() { Collection users = selectedUsers.getModelObject(); From 3702ee105f5024d30bac1f0e735203afd662cb2f Mon Sep 17 00:00:00 2001 From: Marcel de Boer Date: Wed, 31 Jul 2019 21:23:15 +0200 Subject: [PATCH 007/453] 1054 - Dealing with many results in search sidebar - integrate paging directly in the query - implement wicket Dataprovider and Dataview for search results - adjust SearchService and Sidebar to work with the paging --- .../ukp/inception/search/ResultsGroup.java | 30 +++++++ .../inception/search/SearchQueryRequest.java | 21 ++++- .../search/SearchResultsProvider.java | 70 ++++++++++++++++ .../ukp/inception/search/SearchService.java | 7 +- .../inception/search/SearchServiceImpl.java | 6 +- .../search/index/mtas/MtasDocumentIndex.java | 80 ++++++++++--------- .../sidebar/SearchAnnotationSidebar.java | 59 +++++--------- 7 files changed, 186 insertions(+), 87 deletions(-) create mode 100644 inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/ResultsGroup.java create mode 100644 inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/SearchResultsProvider.java diff --git a/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/ResultsGroup.java b/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/ResultsGroup.java new file mode 100644 index 00000000000..956b139e098 --- /dev/null +++ b/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/ResultsGroup.java @@ -0,0 +1,30 @@ +package de.tudarmstadt.ukp.inception.search; + +import java.io.Serializable; +import java.util.List; + +public class ResultsGroup + implements Serializable +{ + + private static final long serialVersionUID = -4448435773623997560L; + private final String groupKey; + private final List results; + + public ResultsGroup(String aGroupKey, List aResults) + { + groupKey = aGroupKey; + results = aResults; + } + + public String getGroupKey() + { + return groupKey; + } + + public List getResults() + { + return results; + } + +} diff --git a/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/SearchQueryRequest.java b/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/SearchQueryRequest.java index 5697ed1b8a7..0f55cb33498 100644 --- a/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/SearchQueryRequest.java +++ b/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/SearchQueryRequest.java @@ -36,20 +36,23 @@ public class SearchQueryRequest private final AnnotationLayer annoationLayer; private final AnnotationFeature annotationFeature; + private final long offset; + private final long count; + public SearchQueryRequest(Project aProject, User aUser, String aQuery) { this(aProject, aUser, aQuery, null); } public SearchQueryRequest(Project aProject, User aUser, String aQuery, - SourceDocument aLimitedToDocument) + SourceDocument aLimitedToDocument) { - this(aProject, aUser, aQuery, aLimitedToDocument, null, null); + this(aProject, aUser, aQuery, aLimitedToDocument, null, null, 0, Integer.MAX_VALUE); } public SearchQueryRequest(Project aProject, User aUser, String aQuery, SourceDocument aLimitedToDocument, AnnotationLayer aAnnotationLayer, - AnnotationFeature aAnnotationFeature) + AnnotationFeature aAnnotationFeature, long aOffset, long aCount) { super(); project = aProject; @@ -58,6 +61,8 @@ public SearchQueryRequest(Project aProject, User aUser, String aQuery, limitedToDocument = aLimitedToDocument; annoationLayer = aAnnotationLayer; annotationFeature = aAnnotationFeature; + offset = aOffset; + count = aCount; } public Project getProject() @@ -89,4 +94,14 @@ public AnnotationFeature getAnnotationFeature() { return annotationFeature; } + + public long getOffset() + { + return offset; + } + + public long getCount() + { + return count; + } } diff --git a/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/SearchResultsProvider.java b/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/SearchResultsProvider.java new file mode 100644 index 00000000000..1322efbfb17 --- /dev/null +++ b/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/SearchResultsProvider.java @@ -0,0 +1,70 @@ +package de.tudarmstadt.ukp.inception.search; + +import java.io.IOException; +import java.util.Iterator; +import java.util.Map; +import java.util.stream.Collectors; + +import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationFeature; +import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; +import de.tudarmstadt.ukp.clarin.webanno.model.Project; +import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; +import de.tudarmstadt.ukp.clarin.webanno.security.model.User; +import org.apache.wicket.markup.repeater.data.IDataProvider; +import org.apache.wicket.model.IModel; +import org.apache.wicket.model.Model; +import org.apache.wicket.spring.injection.annot.SpringBean; + +public class SearchResultsProvider implements IDataProvider +{ + private @SpringBean SearchService searchService; + + private User user; + private Project project; + private String query; + private SourceDocument document; + private AnnotationLayer annotationLayer; + private AnnotationFeature annotationFeature; + + public SearchResultsProvider(User aUser, Project aProject, String aQuery, + SourceDocument aDocument, AnnotationLayer aAnnotationLayer, + AnnotationFeature aAnnotationFeature) { + user = aUser; + project = aProject; + query = aQuery; + document = aDocument; + annotationLayer = aAnnotationLayer; + annotationFeature = aAnnotationFeature; + + } + + public Iterator iterator(long first, long count) + { + try { + Map queryResults = searchService + .query(user, project, query, document, + annotationLayer, annotationFeature, first, count) + .entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> + new ResultsGroup(e.getKey(), e.getValue()))); + return queryResults.entrySet().iterator(); + } + catch (IOException e) { + e.printStackTrace(); + } + catch (ExecutionException e) { + e.printStackTrace(); + } + return null; + } + + public long size() + { + return 10L; + } + + public IModel model(Object object) + { + return new Model((SearchResult)object); + } +} + diff --git a/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/SearchService.java b/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/SearchService.java index 67e58356b57..c3e67ee669b 100644 --- a/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/SearchService.java +++ b/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/SearchService.java @@ -40,7 +40,7 @@ List query(User aUser, Project aProject, String aQuery) /** * Receive the search results un-grouped as a list. - * See {@link #query(User, Project, String, SourceDocument, AnnotationLayer, AnnotationFeature)} + * See {@link #query(User, Project, String, SourceDocument, AnnotationLayer, AnnotationFeature, int, int)} */ List query(User aUser, Project aProject, String aQuery, SourceDocument aDocument) throws IOException, ExecutionException; @@ -54,6 +54,9 @@ List query(User aUser, Project aProject, String aQuery, SourceDocu * @param aDocument limit search to this document or search in the whole project if null * @param aAnnotationLayer the layer that the grouping feature belongs to * @param aAnnotationFeature the feature that is used to group the results + * @param aOffset offset used for the paging of the search results i.e. the index of the first + * search result of the page + * @param aCount number of search results to be returned, starting from aOffset * @return a Map where the keys are the group-keys (e.g. feature-values) and the values are * lists of search results that belong to this group. * @throws IOException @@ -61,7 +64,7 @@ List query(User aUser, Project aProject, String aQuery, SourceDocu */ Map> query(User aUser, Project aProject, String aQuery, SourceDocument aDocument, AnnotationLayer aAnnotationLayer, - AnnotationFeature aAnnotationFeature) throws IOException, ExecutionException; + AnnotationFeature aAnnotationFeature, long aOffset, long aCount) throws IOException, ExecutionException; void reindex(Project aproject) throws IOException; diff --git a/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/SearchServiceImpl.java b/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/SearchServiceImpl.java index bd5db1da2c0..130e26802c3 100644 --- a/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/SearchServiceImpl.java +++ b/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/SearchServiceImpl.java @@ -327,7 +327,7 @@ public List query(User aUser, Project aProject, String aQuery) SourceDocument aDocument) throws IOException, ExecutionException { Map> groupedResults = query(aUser, aProject, aQuery, aDocument, - null, null); + null, null, 0, Integer.MAX_VALUE); List resultsAsList = new ArrayList<>(); groupedResults.values().stream() .forEach(resultsGroup -> resultsAsList.addAll(resultsGroup)); @@ -338,7 +338,7 @@ public List query(User aUser, Project aProject, String aQuery) @Transactional public Map> query(User aUser, Project aProject, String aQuery, SourceDocument aDocument, AnnotationLayer aAnnotationLayer, - AnnotationFeature aAnnotationFeature) throws IOException, ExecutionException + AnnotationFeature aAnnotationFeature, long offset, long count) throws IOException, ExecutionException { log.debug("Starting query for user [{}] in project [{}]({})", aUser.getUsername(), aProject.getName(), aProject.getId()); @@ -384,7 +384,7 @@ public Map> query(User aUser, results = index.getPhysicalIndex().executeQuery( new SearchQueryRequest(aProject, aUser, aQuery, aDocument, - aAnnotationLayer, aAnnotationFeature)); + aAnnotationLayer, aAnnotationFeature, offset, count)); } } diff --git a/inception-search-mtas/src/main/java/de/tudarmstadt/ukp/inception/search/index/mtas/MtasDocumentIndex.java b/inception-search-mtas/src/main/java/de/tudarmstadt/ukp/inception/search/index/mtas/MtasDocumentIndex.java index 815f86f6e92..77ba4126b0e 100644 --- a/inception-search-mtas/src/main/java/de/tudarmstadt/ukp/inception/search/index/mtas/MtasDocumentIndex.java +++ b/inception-search-mtas/src/main/java/de/tudarmstadt/ukp/inception/search/index/mtas/MtasDocumentIndex.java @@ -303,6 +303,11 @@ private Map> doQuery(IndexReader aIndexReader, final float boost = 0; SpanWeight spanweight = q.rewrite(aIndexReader).createWeight(searcher, false, boost); + long offset = aRequest.getOffset(); + long count = aRequest.getCount(); + long current = 0; + + resultIteration: while (leafReaderContextIterator.hasNext()) { LeafReaderContext leafReaderContext = leafReaderContextIterator.next(); try { @@ -312,8 +317,7 @@ private Map> doQuery(IndexReader aIndexReader, CodecInfo mtasCodecInfo = CodecInfo.getCodecInfoFromTerms(terms); if (spans != null) { while (spans.nextDoc() != Spans.NO_MORE_DOCS) { - if (segmentReader.numDocs() == segmentReader.maxDoc() - || segmentReader.getLiveDocs().get(spans.docID())) { + if (segmentReader.numDocs() == segmentReader.maxDoc() || segmentReader.getLiveDocs().get(spans.docID())) { Document document = segmentReader.document(spans.docID()); // Retrieve user @@ -321,27 +325,25 @@ private Map> doQuery(IndexReader aIndexReader, // Retrieve source and annotation document ids String rawSourceDocumentId = document.get(FIELD_SOURCE_DOCUMENT_ID); - String rawAnnotationDocumentId = document - .get(FIELD_ANNOTATION_DOCUMENT_ID); + String rawAnnotationDocumentId = document.get(FIELD_ANNOTATION_DOCUMENT_ID); if (rawSourceDocumentId == null || rawAnnotationDocumentId == null) { log.trace("Indexed document lacks source/annotation document IDs" - + " - source: {}, annotation: {}", - rawSourceDocumentId, rawAnnotationDocumentId); + + " - source: {}, annotation: {}", rawSourceDocumentId, + rawAnnotationDocumentId); continue; } long sourceDocumentId = Long.valueOf(rawSourceDocumentId); long annotationDocumentId = Long.valueOf(rawAnnotationDocumentId); - + // If the query is limited to a given document, skip any results // which are not in the given document - Optional limitedToDocument = aRequest - .getLimitedToDocument(); + Optional limitedToDocument = aRequest.getLimitedToDocument(); if (limitedToDocument.isPresent() && !Objects - .equals(limitedToDocument.get().getId(), sourceDocumentId)) { + .equals(limitedToDocument.get().getId(), sourceDocumentId)) { log.trace("Query limited to document {}, skipping results for " - + "document {}", - limitedToDocument.get().getId(), sourceDocumentId); + + "document {}", limitedToDocument.get().getId(), + sourceDocumentId); continue; } @@ -350,19 +352,18 @@ private Map> doQuery(IndexReader aIndexReader, // Exclude result if the retrieved document is a sourcedocument // (that is, has annotationDocument = -1) AND it has a // corresponding annotation document for this user - log.trace("Skipping results from indexed source document {} in" - + "favor of results from the corresponding annotation " - + "document", sourceDocumentId); + log.trace("Skipping results from indexed source document {} in" + "favor of results from the corresponding annotation " + + "document", sourceDocumentId); continue; } - else if (annotationDocumentId != -1 - && !aRequest.getUser().getUsername().equals(user)) { + else if (annotationDocumentId != -1 && !aRequest.getUser().getUsername() + .equals(user)) { // Exclude result if the retrieved document is an annotation // document (that is, annotationDocument != -1 and its username // is different from the quering user log.trace("Skipping results from annotation document for user {} " + "which does not match the requested user {}", user, - aRequest.getUser().getUsername()); + aRequest.getUser().getUsername()); continue; } @@ -374,16 +375,24 @@ else if (annotationDocumentId != -1 // log.debug("******** New doc {}-{}", + spans.docID(), idValue); while (spans.nextStartPosition() != Spans.NO_MORE_POSITIONS) { + if (current < offset) { + current++; + continue; + } + if (current - offset + 1 > count) { + break resultIteration; + } int matchStart = spans.startPosition(); int matchEnd = spans.endPosition(); - + int windowStart = Math.max(matchStart - RESULT_WINDOW_SIZE, 0); int windowEnd = matchEnd + RESULT_WINDOW_SIZE - 1; - + // Retrieve all indexed objects within the matching range - List tokens = mtasCodecInfo.getObjectsByPositions( - field, spans.docID(), windowStart, windowEnd); - + List tokens = mtasCodecInfo + .getObjectsByPositions(field, spans.docID(), windowStart, + windowEnd); + tokens.sort(Comparator.comparing(MtasTokenString::getOffsetStart)); if (tokens.isEmpty()) { @@ -396,34 +405,28 @@ else if (annotationDocumentId != -1 StringBuilder rightContext = new StringBuilder(); result.setDocumentId(sourceDocumentId); result.setDocumentTitle(documentTitle); - result.setOffsetStart(tokens.stream() - .filter(t -> t.getPositionStart() >= matchStart && - t.getPositionEnd() < matchEnd) - .mapToInt(MtasTokenString::getOffsetStart) - .min() - .getAsInt()); - result.setOffsetEnd(tokens.stream() - .filter(t -> t.getPositionStart() >= matchStart && - t.getPositionEnd() < matchEnd) - .mapToInt(MtasTokenString::getOffsetEnd) - .max() - .getAsInt()); + result.setOffsetStart(tokens.stream().filter( + t -> t.getPositionStart() >= matchStart && t.getPositionEnd() < matchEnd) + .mapToInt(MtasTokenString::getOffsetStart).min().getAsInt()); + result.setOffsetEnd(tokens.stream().filter( + t -> t.getPositionStart() >= matchStart && t.getPositionEnd() < matchEnd) + .mapToInt(MtasTokenString::getOffsetEnd).max().getAsInt()); result.setTokenStart(matchStart); result.setTokenLength(matchEnd - matchStart); - + MtasTokenString prevToken = null; for (MtasTokenString token : tokens) { if (!token.getPrefix().equals(DEFAULT_PREFIX)) { continue; } - + // When searching for an annotation, we don't get the matching // text back... not sure why... String tokenText = CodecUtil.termValue(token.getValue()); if (tokenText == null) { continue; } - + if (token.getPositionStart() < matchStart) { fill(leftContext, prevToken, token); leftContext.append(tokenText); @@ -465,7 +468,6 @@ else if (token.getPositionStart() >= matchEnd) { addToResults(results, result.getDocumentTitle(), result); } - } } } diff --git a/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebar.java b/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebar.java index b47efc34155..90be624724d 100644 --- a/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebar.java +++ b/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebar.java @@ -36,6 +36,8 @@ import java.util.Set; import java.util.stream.Collectors; +import de.tudarmstadt.ukp.inception.search.ResultsGroup; +import de.tudarmstadt.ukp.inception.search.SearchResultsProvider; import org.apache.uima.cas.CAS; import org.apache.uima.cas.Type; import org.apache.uima.cas.text.AnnotationFS; @@ -55,6 +57,8 @@ import org.apache.wicket.markup.html.list.ListItem; import org.apache.wicket.markup.html.list.ListView; import org.apache.wicket.markup.html.panel.Fragment; +import org.apache.wicket.markup.repeater.Item; +import org.apache.wicket.markup.repeater.data.DataView; import org.apache.wicket.model.CompoundPropertyModel; import org.apache.wicket.model.IModel; import org.apache.wicket.model.LoadableDetachableModel; @@ -117,6 +121,7 @@ public class SearchAnnotationSidebar private IModel targetQuery = Model.of(""); private IModel searchOptions = CompoundPropertyModel.of(new SearchOptions()); private IModel> groupedSearchResults; + private SearchResultsProvider resultsProvider; private Map groupLevelSelections; private IModel createOptions = CompoundPropertyModel .of(new CreateAnnotationsOptions()); @@ -162,7 +167,7 @@ public SearchAnnotationSidebar(String aId, IModel aModel, _target.add(searchOptionsForm); })); - groupedSearchResults = LambdaModel.of(this::getSearchResultsGrouped); + resultsProvider = getSearchResultsGrouped(); // Add link for re-indexing the project searchOptionsForm.add(new LambdaAjaxLink("reindexProject", t -> { @@ -175,14 +180,14 @@ public SearchAnnotationSidebar(String aId, IModel aModel, resultsGroupContainer.setOutputMarkupId(true); mainContainer.add(resultsGroupContainer); - ListView searchResultGroups = new ListView("searchResultGroups") + DataView> searchResultGroups = new DataView>("searchResultGroups", resultsProvider) { private static final long serialVersionUID = -631500052426449048L; @Override - protected void populateItem(ListItem item) + protected void populateItem(Item> item) { - ResultsGroup result = item.getModelObject(); + ResultsGroup result = item.getModelObject().getValue(); item.add(new Label("groupTitle", LoadableDetachableModel .of(() -> result.getGroupKey() + " (" + result.getResults().size() + ")"))); item.add(createGroupLevelSelectionCheckBox("selectAllInGroup", @@ -193,11 +198,11 @@ protected void populateItem(ListItem item) groupedSearchResults.getObject().get(result.getGroupKey())))); } }; - searchResultGroups.setModel(LoadableDetachableModel.of(() -> + /*searchResultGroups.setModel(LoadableDetachableModel.of(() -> groupedSearchResults.getObject().values().stream() .sorted(Comparator.comparing(ResultsGroup::getGroupKey)) .collect(Collectors.toList()))); - resultsGroupContainer.add(searchResultGroups); + resultsGroupContainer.add(searchResultGroups);*/ Form annotationForm = new Form<>("annotateForm"); // create annotate-button and options form @@ -304,10 +309,10 @@ private void actionSearch(AjaxRequestTarget aTarget, Form aForm) { aTarget.addChildren(getPage(), IFeedback.class); } - private Map getSearchResultsGrouped() + private SearchResultsProvider getSearchResultsGrouped() { if (isBlank(targetQuery.getObject())) { - return Collections.emptyMap(); + return null;//Collections.emptyMap(); } // If a layer is selected but no feature show error @@ -315,7 +320,7 @@ private Map getSearchResultsGrouped() && searchOptions.getObject().getGroupingFeature() == null) { error( "A feature has to be selected in order to group by feature values. If you want to group by document title, select none for both layer and feature."); - return Collections.emptyMap(); + return null;//Collections.emptyMap(); } try { @@ -327,19 +332,16 @@ private Map getSearchResultsGrouped() applicationEventPublisher.get().publishEvent(new SearchQueryEvent(this, project, currentUser.getUsername(), targetQuery.getObject(), limitToDocument)); SearchOptions opt = searchOptions.getObject(); - Map queryResults = searchService - .query(currentUser, project, targetQuery.getObject(), limitToDocument, - opt.getGroupingLayer(), opt.getGroupingFeature()) - .entrySet().stream().collect(Collectors.toMap(Entry::getKey, e -> - new ResultsGroup(e.getKey(), e.getValue()))); + SearchResultsProvider searchResultsProvider = new SearchResultsProvider(currentUser, project, targetQuery.getObject(), limitToDocument, + opt.getGroupingLayer(), opt.getGroupingFeature()); // init group level selection as soon as we know what the group-keys are - groupLevelSelections = initGroupLevelSelections(queryResults.keySet()); - return queryResults; + //groupLevelSelections = initGroupLevelSelections(queryResults.keySet()); + return searchResultsProvider; } catch (Exception e) { error("Error in the query: " + e.getMessage()); - return Collections.emptyMap(); + return null;//Collections.emptyMap(); } } @@ -503,29 +505,6 @@ private boolean featureValuesMatchCurrentState(AnnotationFS aAnnotationFS) } return true; } - - private class ResultsGroup implements Serializable - { - private static final long serialVersionUID = -4448435773623997560L; - private final String groupKey; - private final List results; - - public ResultsGroup(String aGroupKey, List aResults) - { - groupKey = aGroupKey; - results = aResults; - } - - public String getGroupKey() - { - return groupKey; - } - - public List getResults() - { - return results; - } - } private class SearchResultGroup extends Fragment From 85f01154a1a7413e66429ba02cc83a36b0a09b0f Mon Sep 17 00:00:00 2001 From: Marcel de Boer Date: Mon, 5 Aug 2019 20:01:34 +0200 Subject: [PATCH 008/453] 1054 - Dealing with many results in search sidebar - add function to just count the total number of results of a query separately from fetching the actual results - cache the total number of results for the current query in the dataprovider - cache the current page in the dataprovider to keep track of annotation selections of the results and avoid unecessary queries - remove hashmap for storing the values of the current group selections and instead set these values on the fly in on onConfigure() of the groupselection-checkbox --- .../ukp/inception/search/ResultsGroup.java | 2 + .../search/SearchResultsProvider.java | 104 ++++++++++++---- .../ukp/inception/search/SearchService.java | 6 +- .../inception/search/SearchServiceImpl.java | 54 ++++++++ .../inception/search/index/PhysicalIndex.java | 2 + .../search/index/mtas/MtasDocumentIndex.java | 117 ++++++++++++++++++ .../sidebar/SearchAnnotationSidebar.html | 10 ++ .../sidebar/SearchAnnotationSidebar.java | 112 ++++++++++------- .../SearchAnnotationSidebar.properties | 1 + .../search/sidebar/options/SearchOptions.java | 12 ++ 10 files changed, 348 insertions(+), 72 deletions(-) diff --git a/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/ResultsGroup.java b/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/ResultsGroup.java index 956b139e098..0b1c8164a10 100644 --- a/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/ResultsGroup.java +++ b/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/ResultsGroup.java @@ -27,4 +27,6 @@ public List getResults() return results; } + + } diff --git a/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/SearchResultsProvider.java b/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/SearchResultsProvider.java index 1322efbfb17..1ad8c8167cf 100644 --- a/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/SearchResultsProvider.java +++ b/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/SearchResultsProvider.java @@ -1,8 +1,7 @@ package de.tudarmstadt.ukp.inception.search; import java.io.IOException; -import java.util.Iterator; -import java.util.Map; +import java.util.*; import java.util.stream.Collectors; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationFeature; @@ -17,7 +16,7 @@ public class SearchResultsProvider implements IDataProvider { - private @SpringBean SearchService searchService; + private SearchService searchService; private User user; private Project project; @@ -26,45 +25,100 @@ public class SearchResultsProvider implements IDataProvider private AnnotationLayer annotationLayer; private AnnotationFeature annotationFeature; - public SearchResultsProvider(User aUser, Project aProject, String aQuery, - SourceDocument aDocument, AnnotationLayer aAnnotationLayer, - AnnotationFeature aAnnotationFeature) { - user = aUser; - project = aProject; - query = aQuery; - document = aDocument; - annotationLayer = aAnnotationLayer; - annotationFeature = aAnnotationFeature; + private long totalResults = 0; + private IModel> currentPageCache; + private long pageCacheState = -1; // -1 if a new query has been initialized, different otherwise + private long currentoffset = -1; // + private long currentcount = -1; + public SearchResultsProvider(SearchService aSearchService, IModel> aCurrentPageCache) { + searchService = aSearchService; + currentPageCache = aCurrentPageCache; } public Iterator iterator(long first, long count) { - try { - Map queryResults = searchService - .query(user, project, query, document, - annotationLayer, annotationFeature, first, count) - .entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> - new ResultsGroup(e.getKey(), e.getValue()))); - return queryResults.entrySet().iterator(); + if (query == null) { + currentPageCache.setObject(Collections.emptyList()); + return currentPageCache.getObject().iterator(); } - catch (IOException e) { - e.printStackTrace(); + // Query if: + // We just initialized a new query (pageCacheState = -1) (might be replaced with currentPageCache.getObject() == null) + // or + // we want to retrieve a new page of the current query (currentoffset != first, currentcount != count) + if (pageCacheState == -1 || (currentoffset != first || currentcount != count)) { + try { + List queryResults = searchService + .query(user, project, query, document, annotationLayer, annotationFeature, first, + count).entrySet().stream().map(e -> new ResultsGroup(e.getKey(), e.getValue())) + .collect(Collectors.toList()); + currentPageCache.setObject(queryResults); + //groupLevelSelectionsCache = initGroupLevelSelections(queryResults.stream().map(rg -> rg.getGroupKey()).collect(Collectors.toSet())); + pageCacheState = 1; + currentcount = count; + currentoffset = first; + return queryResults.iterator(); + } + catch (IOException e) { + e.printStackTrace(); + } + catch (ExecutionException e) { + e.printStackTrace(); + } } - catch (ExecutionException e) { - e.printStackTrace(); + else { + return currentPageCache.getObject().iterator(); } return null; } public long size() { - return 10L; + if (totalResults == -1) { + try { + totalResults = searchService + .determineNumOfQueryResults(user, project, query, document, annotationLayer, + annotationFeature); + return totalResults; + } + catch (ExecutionException e) { + e.printStackTrace(); + return 0; + } + } + else { + return totalResults; + } } public IModel model(Object object) { - return new Model((SearchResult)object); + return new Model((ResultsGroup) object); + } + + /** + * Sets the query parameters in the SearchResultsProvider. + * Calling the {@link #iterator(long, long)} method of the SearchResultsProvider will then + * execute the query. + */ + public void initializeQuery(User aUser, Project aProject, String aQuery, + SourceDocument aDocument, AnnotationLayer aAnnotationLayer, + AnnotationFeature aAnnotationFeature) { + user = aUser; + project = aProject; + query = aQuery; + document = aDocument; + annotationLayer = aAnnotationLayer; + annotationFeature = aAnnotationFeature; + + totalResults = -1; // reset size cache + pageCacheState = -1; // reset page cache + + } + + public void emptyQuery() { + query = null; + totalResults = 0; } } diff --git a/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/SearchService.java b/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/SearchService.java index c3e67ee669b..8f2833c7218 100644 --- a/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/SearchService.java +++ b/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/SearchService.java @@ -40,7 +40,7 @@ List query(User aUser, Project aProject, String aQuery) /** * Receive the search results un-grouped as a list. - * See {@link #query(User, Project, String, SourceDocument, AnnotationLayer, AnnotationFeature, int, int)} + * See {@link #query(User, Project, String, SourceDocument, AnnotationLayer, AnnotationFeature, long, long)} */ List query(User aUser, Project aProject, String aQuery, SourceDocument aDocument) throws IOException, ExecutionException; @@ -77,4 +77,8 @@ Map> query(User aUser, Project aProject, String aQuer void indexDocument(AnnotationDocument aAnnotationDocument, CAS aJCas); boolean isIndexInProgress(Project aProject); + + long determineNumOfQueryResults(User aUser, Project aProject, String aQuery, + SourceDocument aDocument, AnnotationLayer aAnnotationLayer, + AnnotationFeature aAnnotationFeature) throws ExecutionException; } diff --git a/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/SearchServiceImpl.java b/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/SearchServiceImpl.java index 130e26802c3..3db4e9fd147 100644 --- a/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/SearchServiceImpl.java +++ b/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/SearchServiceImpl.java @@ -506,4 +506,58 @@ public boolean isIndexInProgress(Project aProject) { return indexScheduler.isIndexInProgress(aProject); } + + @Override public long determineNumOfQueryResults(User aUser, Project aProject, String aQuery, + SourceDocument aDocument, AnnotationLayer aAnnotationLayer, + AnnotationFeature aAnnotationFeature) throws ExecutionException + { + log.debug("Starting query for user [{}] in project [{}]({})", aUser.getUsername(), + aProject.getName(), aProject.getId()); + + long numResults = -1; + + Index index = getIndexFromMemory(aProject); + + if (index.getInvalid()) { + if (!indexScheduler.isIndexInProgress(aProject)) { + // Index is invalid, schedule a new index rebuild + indexScheduler.enqueueReindexTask(aProject); + } + + // Throw execution exception so that the user knows the query was not run + throw (new ExecutionException("Query not executed because index is in invalid state. Try again later.")); + } + else { + // Index is valid, try to execute the query + + if (!index.getPhysicalIndex().isCreated()) { + // Physical index does not exist. + + // Set the invalid flag + index.setInvalid(true); + updateIndex(index); + + // Schedule new reindexing process + indexScheduler.enqueueReindexTask(aProject); + + // Throw execution exception so that the user knows the query was not run + throw (new ExecutionException("Query not executed because index is in invalid state. Try again later.")); + } + else { + // Physical index exists + + if (!index.getPhysicalIndex().isOpen()) { + // Physical index is not open. Open it. + index.getPhysicalIndex().openPhysicalIndex(); + } + + log.debug("Running query: [{}]", aQuery); + + numResults = index.getPhysicalIndex().numberofQueryResults(new SearchQueryRequest(aProject, aUser, aQuery, aDocument, + aAnnotationLayer, aAnnotationFeature, 0L, 0L)); + } + + } + return numResults; + } } diff --git a/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/index/PhysicalIndex.java b/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/index/PhysicalIndex.java index 9ffed5a302b..36bbd4f9c4d 100644 --- a/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/index/PhysicalIndex.java +++ b/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/index/PhysicalIndex.java @@ -68,4 +68,6 @@ Map> executeQuery(SearchQueryRequest aRequest) * @throws IOException */ public Optional getTimestamp(AnnotationDocument aDocument) throws IOException; + + long numberofQueryResults(SearchQueryRequest aSearchQueryRequest) throws ExecutionException; } diff --git a/inception-search-mtas/src/main/java/de/tudarmstadt/ukp/inception/search/index/mtas/MtasDocumentIndex.java b/inception-search-mtas/src/main/java/de/tudarmstadt/ukp/inception/search/index/mtas/MtasDocumentIndex.java index 77ba4126b0e..2fe081dfd6a 100644 --- a/inception-search-mtas/src/main/java/de/tudarmstadt/ukp/inception/search/index/mtas/MtasDocumentIndex.java +++ b/inception-search-mtas/src/main/java/de/tudarmstadt/ukp/inception/search/index/mtas/MtasDocumentIndex.java @@ -241,6 +241,34 @@ public Map> executeQuery(SearchQueryRequest aRequest) } } + @Override + public long numberofQueryResults(SearchQueryRequest aRequest) throws ExecutionException + { + try { + log.trace("Determining number of results for query {} on index {}", aRequest, getIndexDir()); + + Directory directory = FSDirectory.open(getIndexDir().toPath()); + IndexReader indexReader = DirectoryReader.open(directory); + + String modifiedQuery = parseQuery(aRequest.getQuery()); + MtasSpanQuery mtasSpanQuery; + try (Reader reader = new StringReader(modifiedQuery)) { + MtasCQLParser parser = new MtasCQLParser(reader); + mtasSpanQuery = parser.parse(FIELD_CONTENT, DEFAULT_PREFIX, null, null, null); + } + + return countResults(indexReader, aRequest, mtasSpanQuery); + } + catch (mtas.parser.cql.ParseException e) { + log.error("Unable to parse query: [{}]" + aRequest.getQuery(), e); + throw new ExecutionException("Unable to parse query [" + aRequest.getQuery() + "]", e); + } + catch (Exception e) { + log.error("Query execution error", e); + throw (new ExecutionException("Query execution error", e)); + } + } + private String parseQuery(String aQuery) { String result; @@ -276,6 +304,93 @@ private String parseQuery(String aQuery) return result; } + private long countResults(IndexReader aIndexReader, + SearchQueryRequest aRequest, MtasSpanQuery q) throws IOException + { + ListIterator leafReaderContextIterator = aIndexReader.leaves() + .listIterator(); + + IndexSearcher searcher = new IndexSearcher(aIndexReader); + + Map annotatableDocuments = listAnnotatableDocuments(aRequest.getProject(), + aRequest.getUser()); + + final float boost = 0; + SpanWeight spanweight = q.rewrite(aIndexReader).createWeight(searcher, false, boost); + + long current = 0; + + while (leafReaderContextIterator.hasNext()) { + LeafReaderContext leafReaderContext = leafReaderContextIterator.next(); + try { + Spans spans = spanweight.getSpans(leafReaderContext, SpanWeight.Postings.POSITIONS); + SegmentReader segmentReader = (SegmentReader) leafReaderContext.reader(); + if (spans != null) { + while (spans.nextDoc() != Spans.NO_MORE_DOCS) { + if (segmentReader.numDocs() == segmentReader.maxDoc() || segmentReader.getLiveDocs().get(spans.docID())) { + Document document = segmentReader.document(spans.docID()); + + // Retrieve user + String user = document.get(FIELD_USER); + + // Retrieve source and annotation document ids + String rawSourceDocumentId = document.get(FIELD_SOURCE_DOCUMENT_ID); + String rawAnnotationDocumentId = document.get(FIELD_ANNOTATION_DOCUMENT_ID); + if (rawSourceDocumentId == null || rawAnnotationDocumentId == null) { + log.trace("Indexed document lacks source/annotation document IDs" + + " - source: {}, annotation: {}", rawSourceDocumentId, + rawAnnotationDocumentId); + continue; + + } + long sourceDocumentId = Long.valueOf(rawSourceDocumentId); + long annotationDocumentId = Long.valueOf(rawAnnotationDocumentId); + + // If the query is limited to a given document, skip any results + // which are not in the given document + Optional limitedToDocument = aRequest.getLimitedToDocument(); + if (limitedToDocument.isPresent() && !Objects + .equals(limitedToDocument.get().getId(), sourceDocumentId)) { + log.trace("Query limited to document {}, skipping results for " + + "document {}", limitedToDocument.get().getId(), + sourceDocumentId); + continue; + } + + if (annotatableDocuments.containsKey(sourceDocumentId) + && annotationDocumentId == -1) { + // Exclude result if the retrieved document is a sourcedocument + // (that is, has annotationDocument = -1) AND it has a + // corresponding annotation document for this user + log.trace("Skipping results from indexed source document {} in" + "favor of results from the corresponding annotation " + + "document", sourceDocumentId); + continue; + } + else if (annotationDocumentId != -1 && !aRequest.getUser().getUsername() + .equals(user)) { + // Exclude result if the retrieved document is an annotation + // document (that is, annotationDocument != -1 and its username + // is different from the quering user + log.trace("Skipping results from annotation document for user {} " + + "which does not match the requested user {}", user, + aRequest.getUser().getUsername()); + continue; + } + + while (spans.nextStartPosition() != Spans.NO_MORE_POSITIONS) { + current++; + } + } + } + } + } + catch (Exception e) { + log.error("Unable to process query results", e); + } + } + return current; + } + private Map listAnnotatableDocuments(Project aProject, User aUser) { Map annotateableDocuments = new HashMap<>(); @@ -307,6 +422,7 @@ private Map> doQuery(IndexReader aIndexReader, long count = aRequest.getCount(); long current = 0; + resultIteration: while (leafReaderContextIterator.hasNext()) { LeafReaderContext leafReaderContext = leafReaderContextIterator.next(); @@ -382,6 +498,7 @@ else if (annotationDocumentId != -1 && !aRequest.getUser().getUsername() if (current - offset + 1 > count) { break resultIteration; } + current++; int matchStart = spans.startPosition(); int matchEnd = spans.endPosition(); diff --git a/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebar.html b/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebar.html index 5268d64080a..6e235e1d07e 100644 --- a/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebar.html +++ b/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebar.html @@ -77,6 +77,15 @@
data-container="body"> +
+ +
+ +
+
diff --git a/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebar.java b/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebar.java index 90be624724d..2ecdf482b8d 100644 --- a/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebar.java +++ b/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebar.java @@ -25,23 +25,18 @@ import java.io.IOException; import java.io.Serializable; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.Map.Entry; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; import java.util.stream.Collectors; +import de.tudarmstadt.ukp.clarin.webanno.support.lambda.LambdaBehavior; import de.tudarmstadt.ukp.inception.search.ResultsGroup; import de.tudarmstadt.ukp.inception.search.SearchResultsProvider; import org.apache.uima.cas.CAS; import org.apache.uima.cas.Type; import org.apache.uima.cas.text.AnnotationFS; import org.apache.uima.fit.util.CasUtil; +import org.apache.wicket.Component; import org.apache.wicket.MarkupContainer; import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.ajax.form.AjaxFormComponentUpdatingBehavior; @@ -56,13 +51,16 @@ import org.apache.wicket.markup.html.form.TextArea; import org.apache.wicket.markup.html.list.ListItem; import org.apache.wicket.markup.html.list.ListView; +import org.apache.wicket.markup.html.navigation.paging.PagingNavigator; import org.apache.wicket.markup.html.panel.Fragment; import org.apache.wicket.markup.repeater.Item; +import org.apache.wicket.markup.repeater.RepeatingView; import org.apache.wicket.markup.repeater.data.DataView; import org.apache.wicket.model.CompoundPropertyModel; import org.apache.wicket.model.IModel; import org.apache.wicket.model.LoadableDetachableModel; import org.apache.wicket.model.Model; +import org.apache.wicket.model.util.ListModel; import org.apache.wicket.spring.injection.annot.SpringBean; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -120,13 +118,13 @@ public class SearchAnnotationSidebar private IModel targetQuery = Model.of(""); private IModel searchOptions = CompoundPropertyModel.of(new SearchOptions()); - private IModel> groupedSearchResults; - private SearchResultsProvider resultsProvider; - private Map groupLevelSelections; + private IModel> groupedSearchResults = new ListModel<>(); + private SearchResultsProvider resultsProvider = new SearchResultsProvider(searchService, groupedSearchResults); private IModel createOptions = CompoundPropertyModel .of(new CreateAnnotationsOptions()); private IModel deleteOptions = CompoundPropertyModel .of(new DeleteAnnotationsOptions()); + private DataView searchResultGroups; DropDownChoice groupingFeature = new BootstrapSelect<>("groupingFeature", Collections.emptyList(), new ChoiceRenderer<>("uiName")); @@ -158,6 +156,7 @@ public SearchAnnotationSidebar(String aId, IModel aModel, annotationService.listAnnotationLayer(getModelObject().getProject()))); searchOptionsForm.add(groupingFeature); groupingFeature.setNullValid(true); + searchOptionsForm.add(createResultsPerPageSelection("itemsPerPage")); searchOptionsForm.add(visibleWhen(() -> searchOptionsForm.getModelObject().isVisible())); searchOptionsForm.setOutputMarkupPlaceholderTag(true); searchForm.add(searchOptionsForm); @@ -167,7 +166,7 @@ public SearchAnnotationSidebar(String aId, IModel aModel, _target.add(searchOptionsForm); })); - resultsProvider = getSearchResultsGrouped(); + getSearchResultsGrouped(); // Add link for re-indexing the project searchOptionsForm.add(new LambdaAjaxLink("reindexProject", t -> { @@ -180,29 +179,31 @@ public SearchAnnotationSidebar(String aId, IModel aModel, resultsGroupContainer.setOutputMarkupId(true); mainContainer.add(resultsGroupContainer); - DataView> searchResultGroups = new DataView>("searchResultGroups", resultsProvider) + searchResultGroups = new DataView("searchResultGroups", resultsProvider) { private static final long serialVersionUID = -631500052426449048L; @Override - protected void populateItem(Item> item) + protected void populateItem(Item item) { - ResultsGroup result = item.getModelObject().getValue(); + ResultsGroup result = item.getModelObject(); item.add(new Label("groupTitle", LoadableDetachableModel .of(() -> result.getGroupKey() + " (" + result.getResults().size() + ")"))); item.add(createGroupLevelSelectionCheckBox("selectAllInGroup", - Model.of(groupLevelSelections.get(result.getGroupKey())), + result.getGroupKey())); item.add(new SearchResultGroup("group", "resultGroup", SearchAnnotationSidebar.this, - result.getGroupKey(), LambdaModel.of(() -> - groupedSearchResults.getObject().get(result.getGroupKey())))); + result.getGroupKey(), LambdaModel.of(() -> result))); } }; /*searchResultGroups.setModel(LoadableDetachableModel.of(() -> groupedSearchResults.getObject().values().stream() .sorted(Comparator.comparing(ResultsGroup::getGroupKey)) - .collect(Collectors.toList()))); - resultsGroupContainer.add(searchResultGroups);*/ + .collect(Collectors.toList())));*/ + searchResultGroups.setItemsPerPage(searchOptions.getObject().getItemsPerPage()); + resultsGroupContainer.add(searchResultGroups); + mainContainer.add(new PagingNavigator("pagingNavigator", searchResultGroups)); + Form annotationForm = new Form<>("annotateForm"); // create annotate-button and options form @@ -243,19 +244,21 @@ protected void populateItem(Item> item) })); annotationForm.setDefaultButton(annotateButton); - annotationForm.add(visibleWhen(() -> !groupedSearchResults.getObject().isEmpty())); + annotationForm.add(visibleWhen(() -> true));//!groupedSearchResults.getObject().isEmpty())); mainContainer.add(annotationForm); } - private Map initGroupLevelSelections( - Set groupKeys) + private DropDownChoice createResultsPerPageSelection(String aId) { - Map selections = new HashMap<>(); - for (String key : groupKeys) { - selections.put(key, true); - } - return selections; + List choices = new ArrayList<>(); + // TODO: define the values somewhere else or replace with a numberfield + choices.add(5L); + choices.add(10L); + choices.add(20L); + choices.add(50L); + DropDownChoice itemsPerPageChoice = new BootstrapSelect<>(aId, choices); + return itemsPerPageChoice; } private DropDownChoice createLayerDropDownChoice(String aId, @@ -278,26 +281,40 @@ private DropDownChoice createLayerDropDownChoice(String aId, return layerChoice; } - private AjaxCheckBox createGroupLevelSelectionCheckBox(String aId, IModel aModel, + private AjaxCheckBox createGroupLevelSelectionCheckBox(String aId, String aGroupKey) { - AjaxCheckBox selectAllCheckBox = new AjaxCheckBox(aId, aModel) + AjaxCheckBox selectAllCheckBox = new AjaxCheckBox(aId, Model.of(true)) { private static final long serialVersionUID = 2431702654443882657L; @Override protected void onUpdate(AjaxRequestTarget target) { - for (Entry entry : groupedSearchResults.getObject() - .entrySet()) { - if (entry.getKey().equals(aGroupKey)) { - entry.getValue().getResults().stream() + for (ResultsGroup resultsGroup : groupedSearchResults.getObject()) { + if (resultsGroup.getGroupKey().equals(aGroupKey)) { + resultsGroup.getResults().stream() .forEach(r -> r.setSelectedForAnnotation(getModelObject())); } } - groupLevelSelections.put(aGroupKey, getModelObject()); target.add(resultsGroupContainer); } + + @Override + protected void onConfigure() { + super.onConfigure(); + for(ResultsGroup resultsGroup : groupedSearchResults.getObject()) { + if (resultsGroup.getGroupKey().equals(aGroupKey)) { + List unselectedResults = resultsGroup.getResults().stream().filter( sr -> !sr.isSelectedForAnnotation()).collect(Collectors.toList()); + if (unselectedResults.isEmpty()) { + setModelObject(true); + } + else { + setModelObject(false); + } + } + } + } }; return selectAllCheckBox; } @@ -305,14 +322,18 @@ protected void onUpdate(AjaxRequestTarget target) private void actionSearch(AjaxRequestTarget aTarget, Form aForm) { selectedResult = null; groupedSearchResults.detach(); + searchResultGroups.setItemsPerPage(searchOptions.getObject().getItemsPerPage()); + getSearchResultsGrouped(); aTarget.add(mainContainer); aTarget.addChildren(getPage(), IFeedback.class); } - private SearchResultsProvider getSearchResultsGrouped() + private void getSearchResultsGrouped() { + if (isBlank(targetQuery.getObject())) { - return null;//Collections.emptyMap(); + resultsProvider.emptyQuery(); + return; } // If a layer is selected but no feature show error @@ -320,7 +341,8 @@ private SearchResultsProvider getSearchResultsGrouped() && searchOptions.getObject().getGroupingFeature() == null) { error( "A feature has to be selected in order to group by feature values. If you want to group by document title, select none for both layer and feature."); - return null;//Collections.emptyMap(); + resultsProvider.emptyQuery(); + return; } try { @@ -332,16 +354,15 @@ private SearchResultsProvider getSearchResultsGrouped() applicationEventPublisher.get().publishEvent(new SearchQueryEvent(this, project, currentUser.getUsername(), targetQuery.getObject(), limitToDocument)); SearchOptions opt = searchOptions.getObject(); - SearchResultsProvider searchResultsProvider = new SearchResultsProvider(currentUser, project, targetQuery.getObject(), limitToDocument, + resultsProvider.initializeQuery(currentUser, project, targetQuery.getObject(), limitToDocument, opt.getGroupingLayer(), opt.getGroupingFeature()); - - // init group level selection as soon as we know what the group-keys are - //groupLevelSelections = initGroupLevelSelections(queryResults.keySet()); - return searchResultsProvider; + groupedSearchResults.setObject(null); + return; } catch (Exception e) { error("Error in the query: " + e.getMessage()); - return null;//Collections.emptyMap(); + resultsProvider.emptyQuery(); + return; } } @@ -373,7 +394,7 @@ public void actionApplyToSelectedResults(AjaxRequestTarget aTarget, // Group the results by document such that we can process one CAS at a time Map> resultsByDocument = groupedSearchResults.getObject() - .values().stream() + .stream() // the grouping can be based on some other strategy than the document, so // we re-group here .flatMap(group -> group.getResults().stream()) @@ -548,7 +569,6 @@ protected void onUpdate(AjaxRequestTarget target) if (!getModelObject()) { // not all results in the document are selected, so set document // level selection to false - groupLevelSelections.put(groupKey, false); target.add(resultsGroupContainer); } } diff --git a/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebar.properties b/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebar.properties index 76b41ce5437..17f9f103106 100644 --- a/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebar.properties +++ b/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebar.properties @@ -22,4 +22,5 @@ groupingFeature=Feature reindex=Rebuild index overrideMode=Override existing Annotations deleteOnlyMatchingFeatureValues= Delete only matching feature values +itemsPerPage = Results per Page diff --git a/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/options/SearchOptions.java b/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/options/SearchOptions.java index 20d6a08e2b2..f463da13ef5 100644 --- a/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/options/SearchOptions.java +++ b/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/options/SearchOptions.java @@ -30,6 +30,8 @@ public class SearchOptions extends Options private AnnotationFeature groupingFeature; + private long itemsPerPage = 10; + public boolean isLimitedToCurrentDocument() { return limitedToCurrentDocument; @@ -59,4 +61,14 @@ public void setGroupingFeature(AnnotationFeature aGroupingFeature) { groupingFeature = aGroupingFeature; } + + public long getItemsPerPage() + { + return itemsPerPage; + } + + public void setItemsPerPage(long aItemsPerPage) + { + itemsPerPage = aItemsPerPage; + } } From 78a690c8b7fe0531ea1a8cd07ddf525ac59ae0a9 Mon Sep 17 00:00:00 2001 From: Richard Eckart de Castilho Date: Sun, 11 Aug 2019 19:05:59 +0200 Subject: [PATCH 009/453] #1054 - Dealing with many results in search sidebar - Fix build --- .../ukp/inception/search/ResultsGroup.java | 22 +++++-- .../ukp/inception/search/SearchService.java | 10 ++-- .../inception/search/SearchServiceImpl.java | 12 ++-- .../search/index/mtas/MtasDocumentIndex.java | 36 +++++++---- .../sidebar/SearchAnnotationSidebar.java | 36 ++++++----- .../sidebar}/SearchResultsProvider.java | 59 +++++++++++++++---- 6 files changed, 121 insertions(+), 54 deletions(-) rename {inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search => inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar}/SearchResultsProvider.java (66%) diff --git a/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/ResultsGroup.java b/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/ResultsGroup.java index 0b1c8164a10..5cbc598ef0d 100644 --- a/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/ResultsGroup.java +++ b/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/ResultsGroup.java @@ -1,3 +1,20 @@ +/* + * Copyright 2019 + * Ubiquitous Knowledge Processing (UKP) Lab + * Technische Universität Darmstadt + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package de.tudarmstadt.ukp.inception.search; import java.io.Serializable; @@ -6,8 +23,8 @@ public class ResultsGroup implements Serializable { - private static final long serialVersionUID = -4448435773623997560L; + private final String groupKey; private final List results; @@ -26,7 +43,4 @@ public List getResults() { return results; } - - - } diff --git a/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/SearchService.java b/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/SearchService.java index 8f2833c7218..bc91d74226b 100644 --- a/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/SearchService.java +++ b/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/SearchService.java @@ -39,8 +39,9 @@ List query(User aUser, Project aProject, String aQuery) throws IOException, ExecutionException; /** - * Receive the search results un-grouped as a list. - * See {@link #query(User, Project, String, SourceDocument, AnnotationLayer, AnnotationFeature, long, long)} + * Receive the search results un-grouped as a list. See + * {@link #query(User, Project, String, SourceDocument, AnnotationLayer, AnnotationFeature, + * long, long)} */ List query(User aUser, Project aProject, String aQuery, SourceDocument aDocument) throws IOException, ExecutionException; @@ -63,8 +64,9 @@ List query(User aUser, Project aProject, String aQuery, SourceDocu * @throws ExecutionException */ Map> query(User aUser, Project aProject, String aQuery, - SourceDocument aDocument, AnnotationLayer aAnnotationLayer, - AnnotationFeature aAnnotationFeature, long aOffset, long aCount) throws IOException, ExecutionException; + SourceDocument aDocument, AnnotationLayer aAnnotationLayer, + AnnotationFeature aAnnotationFeature, long aOffset, long aCount) + throws IOException, ExecutionException; void reindex(Project aproject) throws IOException; diff --git a/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/SearchServiceImpl.java b/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/SearchServiceImpl.java index 3db4e9fd147..a5a43604432 100644 --- a/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/SearchServiceImpl.java +++ b/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/SearchServiceImpl.java @@ -336,9 +336,10 @@ public List query(User aUser, Project aProject, String aQuery) @Override @Transactional - public Map> query(User aUser, - Project aProject, String aQuery, SourceDocument aDocument, AnnotationLayer aAnnotationLayer, - AnnotationFeature aAnnotationFeature, long offset, long count) throws IOException, ExecutionException + public Map> query(User aUser, Project aProject, String aQuery, + SourceDocument aDocument, AnnotationLayer aAnnotationLayer, + AnnotationFeature aAnnotationFeature, long offset, long count) + throws IOException, ExecutionException { log.debug("Starting query for user [{}] in project [{}]({})", aUser.getUsername(), aProject.getName(), aProject.getId()); @@ -553,8 +554,9 @@ public boolean isIndexInProgress(Project aProject) log.debug("Running query: [{}]", aQuery); - numResults = index.getPhysicalIndex().numberofQueryResults(new SearchQueryRequest(aProject, aUser, aQuery, aDocument, - aAnnotationLayer, aAnnotationFeature, 0L, 0L)); + numResults = index.getPhysicalIndex() + .numberofQueryResults(new SearchQueryRequest(aProject, aUser, aQuery, + aDocument, aAnnotationLayer, aAnnotationFeature, 0L, 0L)); } } diff --git a/inception-search-mtas/src/main/java/de/tudarmstadt/ukp/inception/search/index/mtas/MtasDocumentIndex.java b/inception-search-mtas/src/main/java/de/tudarmstadt/ukp/inception/search/index/mtas/MtasDocumentIndex.java index 2fe081dfd6a..50b91b89174 100644 --- a/inception-search-mtas/src/main/java/de/tudarmstadt/ukp/inception/search/index/mtas/MtasDocumentIndex.java +++ b/inception-search-mtas/src/main/java/de/tudarmstadt/ukp/inception/search/index/mtas/MtasDocumentIndex.java @@ -245,7 +245,8 @@ public Map> executeQuery(SearchQueryRequest aRequest) public long numberofQueryResults(SearchQueryRequest aRequest) throws ExecutionException { try { - log.trace("Determining number of results for query {} on index {}", aRequest, getIndexDir()); + log.trace("Determining number of results for query {} on index {}", aRequest, + getIndexDir()); Directory directory = FSDirectory.open(getIndexDir().toPath()); IndexReader indexReader = DirectoryReader.open(directory); @@ -327,7 +328,8 @@ private long countResults(IndexReader aIndexReader, SegmentReader segmentReader = (SegmentReader) leafReaderContext.reader(); if (spans != null) { while (spans.nextDoc() != Spans.NO_MORE_DOCS) { - if (segmentReader.numDocs() == segmentReader.maxDoc() || segmentReader.getLiveDocs().get(spans.docID())) { + if (segmentReader.numDocs() == segmentReader.maxDoc() + || segmentReader.getLiveDocs().get(spans.docID())) { Document document = segmentReader.document(spans.docID()); // Retrieve user @@ -335,7 +337,8 @@ private long countResults(IndexReader aIndexReader, // Retrieve source and annotation document ids String rawSourceDocumentId = document.get(FIELD_SOURCE_DOCUMENT_ID); - String rawAnnotationDocumentId = document.get(FIELD_ANNOTATION_DOCUMENT_ID); + String rawAnnotationDocumentId = document + .get(FIELD_ANNOTATION_DOCUMENT_ID); if (rawSourceDocumentId == null || rawAnnotationDocumentId == null) { log.trace("Indexed document lacks source/annotation document IDs" + " - source: {}, annotation: {}", rawSourceDocumentId, @@ -348,7 +351,8 @@ private long countResults(IndexReader aIndexReader, // If the query is limited to a given document, skip any results // which are not in the given document - Optional limitedToDocument = aRequest.getLimitedToDocument(); + Optional limitedToDocument = aRequest + .getLimitedToDocument(); if (limitedToDocument.isPresent() && !Objects .equals(limitedToDocument.get().getId(), sourceDocumentId)) { log.trace("Query limited to document {}, skipping results for " @@ -433,7 +437,8 @@ private Map> doQuery(IndexReader aIndexReader, CodecInfo mtasCodecInfo = CodecInfo.getCodecInfoFromTerms(terms); if (spans != null) { while (spans.nextDoc() != Spans.NO_MORE_DOCS) { - if (segmentReader.numDocs() == segmentReader.maxDoc() || segmentReader.getLiveDocs().get(spans.docID())) { + if (segmentReader.numDocs() == segmentReader.maxDoc() + || segmentReader.getLiveDocs().get(spans.docID())) { Document document = segmentReader.document(spans.docID()); // Retrieve user @@ -441,7 +446,8 @@ private Map> doQuery(IndexReader aIndexReader, // Retrieve source and annotation document ids String rawSourceDocumentId = document.get(FIELD_SOURCE_DOCUMENT_ID); - String rawAnnotationDocumentId = document.get(FIELD_ANNOTATION_DOCUMENT_ID); + String rawAnnotationDocumentId = document + .get(FIELD_ANNOTATION_DOCUMENT_ID); if (rawSourceDocumentId == null || rawAnnotationDocumentId == null) { log.trace("Indexed document lacks source/annotation document IDs" + " - source: {}, annotation: {}", rawSourceDocumentId, @@ -454,7 +460,8 @@ private Map> doQuery(IndexReader aIndexReader, // If the query is limited to a given document, skip any results // which are not in the given document - Optional limitedToDocument = aRequest.getLimitedToDocument(); + Optional limitedToDocument = aRequest + .getLimitedToDocument(); if (limitedToDocument.isPresent() && !Objects .equals(limitedToDocument.get().getId(), sourceDocumentId)) { log.trace("Query limited to document {}, skipping results for " @@ -522,12 +529,15 @@ else if (annotationDocumentId != -1 && !aRequest.getUser().getUsername() StringBuilder rightContext = new StringBuilder(); result.setDocumentId(sourceDocumentId); result.setDocumentTitle(documentTitle); - result.setOffsetStart(tokens.stream().filter( - t -> t.getPositionStart() >= matchStart && t.getPositionEnd() < matchEnd) - .mapToInt(MtasTokenString::getOffsetStart).min().getAsInt()); - result.setOffsetEnd(tokens.stream().filter( - t -> t.getPositionStart() >= matchStart && t.getPositionEnd() < matchEnd) - .mapToInt(MtasTokenString::getOffsetEnd).max().getAsInt()); + result.setOffsetStart(tokens.stream() + .filter(t -> t.getPositionStart() >= matchStart + && t.getPositionEnd() < matchEnd) + .mapToInt(MtasTokenString::getOffsetStart).min() + .getAsInt()); + result.setOffsetEnd(tokens.stream() + .filter(t -> t.getPositionStart() >= matchStart + && t.getPositionEnd() < matchEnd) + .mapToInt(MtasTokenString::getOffsetEnd).max().getAsInt()); result.setTokenStart(matchStart); result.setTokenLength(matchEnd - matchStart); diff --git a/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebar.java b/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebar.java index 2ecdf482b8d..97d689966d9 100644 --- a/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebar.java +++ b/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebar.java @@ -24,19 +24,19 @@ import static org.apache.commons.lang3.StringUtils.isBlank; import java.io.IOException; -import java.io.Serializable; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; import java.util.Map.Entry; +import java.util.Objects; +import java.util.Optional; import java.util.stream.Collectors; -import de.tudarmstadt.ukp.clarin.webanno.support.lambda.LambdaBehavior; -import de.tudarmstadt.ukp.inception.search.ResultsGroup; -import de.tudarmstadt.ukp.inception.search.SearchResultsProvider; import org.apache.uima.cas.CAS; import org.apache.uima.cas.Type; import org.apache.uima.cas.text.AnnotationFS; import org.apache.uima.fit.util.CasUtil; -import org.apache.wicket.Component; import org.apache.wicket.MarkupContainer; import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.ajax.form.AjaxFormComponentUpdatingBehavior; @@ -54,7 +54,6 @@ import org.apache.wicket.markup.html.navigation.paging.PagingNavigator; import org.apache.wicket.markup.html.panel.Fragment; import org.apache.wicket.markup.repeater.Item; -import org.apache.wicket.markup.repeater.RepeatingView; import org.apache.wicket.markup.repeater.data.DataView; import org.apache.wicket.model.CompoundPropertyModel; import org.apache.wicket.model.IModel; @@ -94,6 +93,7 @@ import de.tudarmstadt.ukp.inception.app.ui.search.sidebar.options.CreateAnnotationsOptions; import de.tudarmstadt.ukp.inception.app.ui.search.sidebar.options.DeleteAnnotationsOptions; import de.tudarmstadt.ukp.inception.app.ui.search.sidebar.options.SearchOptions; +import de.tudarmstadt.ukp.inception.search.ResultsGroup; import de.tudarmstadt.ukp.inception.search.SearchResult; import de.tudarmstadt.ukp.inception.search.SearchService; import de.tudarmstadt.ukp.inception.search.event.SearchQueryEvent; @@ -115,15 +115,15 @@ public class SearchAnnotationSidebar private final WebMarkupContainer mainContainer; private final WebMarkupContainer resultsGroupContainer; + private final SearchResultsProvider resultsProvider; private IModel targetQuery = Model.of(""); private IModel searchOptions = CompoundPropertyModel.of(new SearchOptions()); private IModel> groupedSearchResults = new ListModel<>(); - private SearchResultsProvider resultsProvider = new SearchResultsProvider(searchService, groupedSearchResults); private IModel createOptions = CompoundPropertyModel - .of(new CreateAnnotationsOptions()); + .of(new CreateAnnotationsOptions()); private IModel deleteOptions = CompoundPropertyModel - .of(new DeleteAnnotationsOptions()); + .of(new DeleteAnnotationsOptions()); private DataView searchResultGroups; DropDownChoice groupingFeature = new BootstrapSelect<>("groupingFeature", @@ -139,6 +139,9 @@ public SearchAnnotationSidebar(String aId, IModel aModel, currentUser = userRepository.getCurrentUser(); + resultsProvider = new SearchResultsProvider(searchService, + groupedSearchResults); + mainContainer = new WebMarkupContainer("mainContainer"); mainContainer.setOutputMarkupId(true); add(mainContainer); @@ -301,11 +304,14 @@ protected void onUpdate(AjaxRequestTarget target) } @Override - protected void onConfigure() { + protected void onConfigure() + { super.onConfigure(); - for(ResultsGroup resultsGroup : groupedSearchResults.getObject()) { + for (ResultsGroup resultsGroup : groupedSearchResults.getObject()) { if (resultsGroup.getGroupKey().equals(aGroupKey)) { - List unselectedResults = resultsGroup.getResults().stream().filter( sr -> !sr.isSelectedForAnnotation()).collect(Collectors.toList()); + List unselectedResults = resultsGroup.getResults().stream() + .filter(sr -> !sr.isSelectedForAnnotation()) + .collect(Collectors.toList()); if (unselectedResults.isEmpty()) { setModelObject(true); } @@ -354,8 +360,8 @@ private void getSearchResultsGrouped() applicationEventPublisher.get().publishEvent(new SearchQueryEvent(this, project, currentUser.getUsername(), targetQuery.getObject(), limitToDocument)); SearchOptions opt = searchOptions.getObject(); - resultsProvider.initializeQuery(currentUser, project, targetQuery.getObject(), limitToDocument, - opt.getGroupingLayer(), opt.getGroupingFeature()); + resultsProvider.initializeQuery(currentUser, project, targetQuery.getObject(), + limitToDocument, opt.getGroupingLayer(), opt.getGroupingFeature()); groupedSearchResults.setObject(null); return; } diff --git a/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/SearchResultsProvider.java b/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchResultsProvider.java similarity index 66% rename from inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/SearchResultsProvider.java rename to inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchResultsProvider.java index 1ad8c8167cf..5283031109c 100644 --- a/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/SearchResultsProvider.java +++ b/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchResultsProvider.java @@ -1,21 +1,45 @@ -package de.tudarmstadt.ukp.inception.search; +/* + * Copyright 2019 + * Ubiquitous Knowledge Processing (UKP) Lab + * Technische Universität Darmstadt + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.tudarmstadt.ukp.inception.app.ui.search.sidebar; import java.io.IOException; -import java.util.*; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; import java.util.stream.Collectors; +import org.apache.wicket.markup.repeater.data.IDataProvider; +import org.apache.wicket.model.IModel; +import org.apache.wicket.model.Model; + import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationFeature; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; import de.tudarmstadt.ukp.clarin.webanno.model.Project; import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.clarin.webanno.security.model.User; -import org.apache.wicket.markup.repeater.data.IDataProvider; -import org.apache.wicket.model.IModel; -import org.apache.wicket.model.Model; -import org.apache.wicket.spring.injection.annot.SpringBean; +import de.tudarmstadt.ukp.inception.search.ExecutionException; +import de.tudarmstadt.ukp.inception.search.ResultsGroup; +import de.tudarmstadt.ukp.inception.search.SearchService; public class SearchResultsProvider implements IDataProvider { + private static final long serialVersionUID = -4937781923274074722L; + private SearchService searchService; private User user; @@ -31,11 +55,14 @@ public class SearchResultsProvider implements IDataProvider private long currentoffset = -1; // private long currentcount = -1; - public SearchResultsProvider(SearchService aSearchService, IModel> aCurrentPageCache) { + public SearchResultsProvider(SearchService aSearchService, + IModel> aCurrentPageCache) + { searchService = aSearchService; currentPageCache = aCurrentPageCache; } + @Override public Iterator iterator(long first, long count) { if (query == null) { @@ -43,17 +70,21 @@ public Iterator iterator(long first, long count) return currentPageCache.getObject().iterator(); } // Query if: - // We just initialized a new query (pageCacheState = -1) (might be replaced with currentPageCache.getObject() == null) + // We just initialized a new query (pageCacheState = -1) (might be replaced with + // currentPageCache.getObject() == null) // or - // we want to retrieve a new page of the current query (currentoffset != first, currentcount != count) + // we want to retrieve a new page of the current query (currentoffset != first, currentcount + // != count) if (pageCacheState == -1 || (currentoffset != first || currentcount != count)) { try { List queryResults = searchService - .query(user, project, query, document, annotationLayer, annotationFeature, first, - count).entrySet().stream().map(e -> new ResultsGroup(e.getKey(), e.getValue())) - .collect(Collectors.toList()); + .query(user, project, query, document, annotationLayer, annotationFeature, + first, count) + .entrySet().stream().map(e -> new ResultsGroup(e.getKey(), e.getValue())) + .collect(Collectors.toList()); currentPageCache.setObject(queryResults); - //groupLevelSelectionsCache = initGroupLevelSelections(queryResults.stream().map(rg -> rg.getGroupKey()).collect(Collectors.toSet())); + // groupLevelSelectionsCache = initGroupLevelSelections(queryResults.stream().map(rg + // -> rg.getGroupKey()).collect(Collectors.toSet())); pageCacheState = 1; currentcount = count; currentoffset = first; @@ -72,6 +103,7 @@ public Iterator iterator(long first, long count) return null; } + @Override public long size() { if (totalResults == -1) { @@ -91,6 +123,7 @@ public long size() } } + @Override public IModel model(Object object) { return new Model((ResultsGroup) object); From e2f3d6e2a0bca7f001527ac1f52ef76d5492f897 Mon Sep 17 00:00:00 2001 From: uwinch Date: Tue, 13 Aug 2019 17:59:43 +0200 Subject: [PATCH 010/453] #1256 Integrate curation into annotation page - started on rendering via copying pre-renders --- inception-app-webapp/pom.xml | 7 ++-- inception-curation/pom.xml | 12 ++++++ .../curation/CurationEditorExtension.java | 41 ++++++++++++++++--- .../ukp/inception/curation/CurationVID.java | 36 ++++++++++++++++ .../curation/sidebar/CurationSidebar.java | 3 ++ .../RecommendationEditorExtension.java | 11 +++++ pom.xml | 7 +--- 7 files changed, 102 insertions(+), 15 deletions(-) create mode 100644 inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationVID.java diff --git a/inception-app-webapp/pom.xml b/inception-app-webapp/pom.xml index 1077394c827..33cee77dd26 100644 --- a/inception-app-webapp/pom.xml +++ b/inception-app-webapp/pom.xml @@ -499,13 +499,14 @@ ${javamelody.version} --> + + org.apache.wicket + wicket-native-websocket-javax + de.tudarmstadt.ukp.inception.app inception-curation 0.11.0-SNAPSHOT - - org.apache.wicket - wicket-native-websocket-javax diff --git a/inception-curation/pom.xml b/inception-curation/pom.xml index 65037ae72e9..11211c416d2 100644 --- a/inception-curation/pom.xml +++ b/inception-curation/pom.xml @@ -70,5 +70,17 @@ de.tudarmstadt.ukp.clarin.webanno webanno-brat + + org.apache.commons + commons-lang3 + + + org.springframework + spring-beans + + + org.slf4j + slf4j-api + \ No newline at end of file diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationEditorExtension.java b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationEditorExtension.java index 1b30a680f1e..f385454ee99 100644 --- a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationEditorExtension.java +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationEditorExtension.java @@ -20,6 +20,8 @@ import java.io.IOException; import java.util.List; import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.apache.uima.cas.CAS; import org.apache.wicket.ajax.AjaxRequestTarget; @@ -36,6 +38,7 @@ import de.tudarmstadt.ukp.clarin.webanno.api.annotation.model.VID; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.rendering.PreRenderer; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.rendering.model.VDocument; +import de.tudarmstadt.ukp.clarin.webanno.api.annotation.rendering.model.VObject; import de.tudarmstadt.ukp.clarin.webanno.brat.message.SpanAnnotationResponse; import de.tudarmstadt.ukp.clarin.webanno.security.model.User; @@ -62,13 +65,13 @@ public void handleAction(AnnotationActionHandler aPanel, AnnotatorState aState, AjaxRequestTarget aTarget, CAS aCas, VID aParamId, String aAction, int aBegin, int aEnd) throws AnnotationException, IOException { - // only process actions relevant curation + // only process actions relevant to curation if (!aParamId.getExtensionId().equals(EXTENSION_ID)) { return; } // Annotation has been selected for gold if (SpanAnnotationResponse.is(aAction)) { //TODO is this action only for spans - //, what about relations + //, what about relations ? // TODO: store annotation in user CAS } @@ -78,7 +81,6 @@ public void handleAction(AnnotationActionHandler aPanel, AnnotatorState aState, public void render(CAS aCas, AnnotatorState aState, VDocument aVdoc, int aWindowBeginOffset, int aWindowEndOffset) { - // TODO Auto-generated method stub String currentUser = aState.getUser().getUsername(); long projectId = aState.getProject().getId(); Optional> selectedUsers = curationService @@ -96,9 +98,17 @@ public void render(CAS aCas, AnnotatorState aState, VDocument aVdoc, int aWindow user.getUsername(), projectId)); continue; } - // FIXME cannot add the same annotations, change VID ? - preRenderer.render(aVdoc, aWindowBeginOffset, aWindowEndOffset, userCas.get(), - aState.getAnnotationLayers()); //TODO: might need to filter the layers + VDocument tmpDoc = new VDocument(); + preRenderer.render(tmpDoc, aWindowBeginOffset, aWindowEndOffset, userCas.get(), + aState.getAnnotationLayers()); //TODO: might need to filter the layers? + // copy all arcs and spans to existing doc with new VID + for (VObject vobj : tmpDoc.vobjects()) { + VID vid = vobj.getVid(); + VID extendedVID = parse(vid, vid.getExtensionPayload()); + vobj.setVid(extendedVID); + aVdoc.add(vobj); + } + // TODO: add comment with username } catch (IOException e) { log.error(String.format("Could not retrieve CAS for user %s and project %d", @@ -109,4 +119,23 @@ public void render(CAS aCas, AnnotatorState aState, VDocument aVdoc, int aWindow } } + @Override + public VID parse(VID aParamId, String aVIDString) + { + // format is : with standard VID format -..@ + Matcher matcher = Pattern.compile("(?:(?\\w+)\\:)" + + "(?\\d+)").matcher(aVIDString); + if (!matcher.matches()) { + return aParamId; + } + + if (matcher.group("VID") == null || + matcher.group("USER") != null ) { + return aParamId; + } + + String vidStr = matcher.group("VID"); + String username = matcher.group("USER"); + return new CurationVID(aParamId.getExtensionId(), username, VID.parse(vidStr)); + } } diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationVID.java b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationVID.java new file mode 100644 index 00000000000..36d8def6820 --- /dev/null +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationVID.java @@ -0,0 +1,36 @@ +package de.tudarmstadt.ukp.inception.curation; + +import de.tudarmstadt.ukp.clarin.webanno.api.annotation.model.VID; + +public class CurationVID + extends VID +{ + private static final long serialVersionUID = -4052847275637346338L; + + private final String username; + + public CurationVID(String aExtId, String aUsername, VID aVID) + { + super(aExtId, aVID.getLayerId(), aVID.getId(), aVID.getSubId(), aVID.getAttribute(), + aVID.getSlot()); + username = aUsername; + } + + public String getUsername() + { + return username; + } + + @Override + public int hashCode() + { + return super.hashCode() * 31 + username.hashCode(); + } + + @Override + public boolean equals(Object aObj) + { + return super.equals(aObj) && ((CurationVID) aObj).getUsername().equals(username); + } + +} diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java index 80ee92f21c0..283f21b657b 100644 --- a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java @@ -18,6 +18,7 @@ package de.tudarmstadt.ukp.inception.curation.sidebar; import static de.tudarmstadt.ukp.clarin.webanno.api.WebAnnoConst.CURATION_USER; +import static de.tudarmstadt.ukp.clarin.webanno.support.lambda.LambdaBehavior.visibleWhen; import java.util.ArrayList; import java.util.Arrays; @@ -76,6 +77,7 @@ public class CurationSidebar private AnnotatorState state; // private AnnotationPage annoPage; + // FIXME: only show to people who are curators public CurationSidebar(String aId, IModel aModel, AnnotationActionHandler aActionHandler, CasProvider aCasProvider, AnnotationPage aAnnotationPage) @@ -153,6 +155,7 @@ protected void populateItem(ListItem aItem) }; selectedUsers.add(users); usersForm.add(selectedUsers); + usersForm.add(visibleWhen(() -> !usersForm.getModelObject().isEmpty())); return usersForm; } diff --git a/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/RecommendationEditorExtension.java b/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/RecommendationEditorExtension.java index 932b65396ee..66e0fc7b4bb 100644 --- a/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/RecommendationEditorExtension.java +++ b/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/RecommendationEditorExtension.java @@ -117,6 +117,11 @@ public void handleAction(AnnotationActionHandler aActionHandler, AnnotatorState AjaxRequestTarget aTarget, CAS aCas, VID aVID, String aAction, int aBegin, int aEnd) throws IOException, AnnotationException { + // only process actions relevant to recommendation + if (!aVID.getExtensionId().equals(BEAN_NAME)) { + return; + } + // Create annotation if (SpanAnnotationResponse.is(aAction)) { actionAcceptRecommendation(aActionHandler, aState, aTarget, aCas, aVID, aBegin, aEnd); @@ -266,4 +271,10 @@ public void render(CAS aCas, AnnotatorState aState, VDocument aVDoc, recommendationService, learningRecordService, fsRegistry, documentService, aWindowBeginOffset, aWindowEndOffset); } + + @Override + public VID parse(VID aParamId, String aVIDString) + { + return VID.parse(aVIDString); + } } diff --git a/pom.xml b/pom.xml index 4bff43f6783..9763d310cda 100644 --- a/pom.xml +++ b/pom.xml @@ -111,17 +111,12 @@ inception-imls-stringmatch inception-imls-dl4j inception-imls-external - inception-log - inception-support - inception-testing - inception-layer-docmetadata - inception-image - inception-scheduling inception-imls-lapps inception-example-imls-data-majority inception-doc + inception-curation From 4f34f189f7d14c3ee09aa29a4e32505ba4528e5d Mon Sep 17 00:00:00 2001 From: uwinch Date: Wed, 14 Aug 2019 12:03:10 +0200 Subject: [PATCH 011/453] #1256 Integrate curation into annotation page - VID unit test --- inception-curation/pom.xml | 9 +++ .../curation/CurationEditorExtension.java | 7 ++- .../ukp/inception/curation/CurationVID.java | 21 ++++++- .../inception/curation/CurationVIDTest.java | 60 +++++++++++++++++++ 4 files changed, 92 insertions(+), 5 deletions(-) create mode 100644 inception-curation/src/test/java/de/tudarmstadt/ukp/inception/curation/CurationVIDTest.java diff --git a/inception-curation/pom.xml b/inception-curation/pom.xml index 11211c416d2..95320988648 100644 --- a/inception-curation/pom.xml +++ b/inception-curation/pom.xml @@ -82,5 +82,14 @@ org.slf4j slf4j-api + + junit + junit + test + + + de.tudarmstadt.ukp.clarin.webanno + webanno-support + \ No newline at end of file diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationEditorExtension.java b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationEditorExtension.java index f385454ee99..d6646802649 100644 --- a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationEditorExtension.java +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationEditorExtension.java @@ -124,18 +124,19 @@ public VID parse(VID aParamId, String aVIDString) { // format is : with standard VID format -..@ Matcher matcher = Pattern.compile("(?:(?\\w+)\\:)" - + "(?\\d+)").matcher(aVIDString); + + "(?.+)").matcher(aVIDString); if (!matcher.matches()) { return aParamId; } if (matcher.group("VID") == null || - matcher.group("USER") != null ) { + matcher.group("USER") == null ) { return aParamId; } String vidStr = matcher.group("VID"); String username = matcher.group("USER"); - return new CurationVID(aParamId.getExtensionId(), username, VID.parse(vidStr)); + return new CurationVID(aParamId.getExtensionId(), aParamId.getExtensionPayload(), username, + VID.parse(vidStr)); } } diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationVID.java b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationVID.java index 36d8def6820..401547a7f53 100644 --- a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationVID.java +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationVID.java @@ -1,3 +1,20 @@ +/* + * Copyright 2019 + * Ubiquitous Knowledge Processing (UKP) Lab + * Technische Universität Darmstadt + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package de.tudarmstadt.ukp.inception.curation; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.model.VID; @@ -9,10 +26,10 @@ public class CurationVID private final String username; - public CurationVID(String aExtId, String aUsername, VID aVID) + public CurationVID(String aExtId, String aExtPayload, String aUsername, VID aVID) { super(aExtId, aVID.getLayerId(), aVID.getId(), aVID.getSubId(), aVID.getAttribute(), - aVID.getSlot()); + aVID.getSlot(), aExtPayload); username = aUsername; } diff --git a/inception-curation/src/test/java/de/tudarmstadt/ukp/inception/curation/CurationVIDTest.java b/inception-curation/src/test/java/de/tudarmstadt/ukp/inception/curation/CurationVIDTest.java new file mode 100644 index 00000000000..c2187cc6815 --- /dev/null +++ b/inception-curation/src/test/java/de/tudarmstadt/ukp/inception/curation/CurationVIDTest.java @@ -0,0 +1,60 @@ +/* + * Copyright 2019 + * Ubiquitous Knowledge Processing (UKP) Lab + * Technische Universität Darmstadt + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.tudarmstadt.ukp.inception.curation; + +import static org.junit.Assert.assertEquals; + +import org.junit.Before; +import org.junit.Test; + +import de.tudarmstadt.ukp.clarin.webanno.api.annotation.model.VID; + +public class CurationVIDTest +{ + private CurationEditorExtension extension; + + @Before + public void setup() { + extension = new CurationEditorExtension(); + } + + @Test + public void testParse() + { + assertParseVid(VID.parse("ext:kevin:10"), "ext", -1, 10, -1, -1, -1, "kevin", "kevin:10"); + assertParseVid(VID.parse("ext:kevin:10.1"), "ext", -1, 10, -1, 1, -1, "kevin", "kevin:10.1"); + assertParseVid(VID.parse("ext:kevin:10.1.2"), "ext", -1, 10, -1, 1, 2, "kevin", "kevin:10.1.2"); + assertParseVid(VID.parse("ext:kevin:10-1.2.3"), "ext", -1, 10, 1, 2, 3, "kevin", "kevin:10-1.2.3"); + assertParseVid(VID.parse("ext:kevin:10-1.2.3@1"), "ext", 1, 10, 1, 2, 3, "kevin", "kevin:10-1.2.3@1"); + } + + private void assertParseVid(VID aVID, String aExtensionId, int aLayerId, int aAnnotationID, + int aSubAnnotationId, int aAttribute, int aSlot, String aUsername, + String aExtensionPayload) + { + VID a = extension.parse(aVID, aExtensionPayload); + assertEquals(aExtensionId, a.getExtensionId()); + assertEquals(aExtensionPayload, a.getExtensionPayload()); + assertEquals(aUsername, ((CurationVID) a).getUsername()); + assertEquals(aLayerId, a.getLayerId()); + assertEquals(aAnnotationID, a.getId()); + assertEquals(aSubAnnotationId, a.getSubId()); + assertEquals(aAttribute, a.getAttribute()); + assertEquals(aSlot, a.getSlot()); + } +} From 82f0c52dd07ac9577c41b0bae4c56629e1395744 Mon Sep 17 00:00:00 2001 From: Marcel de Boer Date: Wed, 14 Aug 2019 18:08:45 +0200 Subject: [PATCH 012/453] 1054 - Dealing with many results in search sidebar - make pagesizes configurable via properties - clean up some code --- .../search/config/SearchProperties.java | 8 ++++ .../search/config/SearchPropertiesImpl.java | 25 ++++++++++++ .../sidebar/SearchAnnotationSidebar.html | 2 +- .../sidebar/SearchAnnotationSidebar.java | 21 ++++------ .../search/sidebar/SearchResultsProvider.java | 39 +++++++++---------- 5 files changed, 60 insertions(+), 35 deletions(-) create mode 100644 inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/config/SearchProperties.java create mode 100644 inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/config/SearchPropertiesImpl.java diff --git a/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/config/SearchProperties.java b/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/config/SearchProperties.java new file mode 100644 index 00000000000..623d25a5b28 --- /dev/null +++ b/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/config/SearchProperties.java @@ -0,0 +1,8 @@ +package de.tudarmstadt.ukp.inception.search.config; + +public interface SearchProperties +{ + int[] getPagesSizes(); + + void setPagesSizes(String[] aPagesSizes); +} diff --git a/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/config/SearchPropertiesImpl.java b/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/config/SearchPropertiesImpl.java new file mode 100644 index 00000000000..854db48d050 --- /dev/null +++ b/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/config/SearchPropertiesImpl.java @@ -0,0 +1,25 @@ +package de.tudarmstadt.ukp.inception.search.config; + +import java.util.Arrays; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Component +@ConfigurationProperties("inception.search") +public class SearchPropertiesImpl implements SearchProperties +{ + int[] pagesSizes = {10, 20, 50, 100, 500, 1000}; + + @Override + public int[] getPagesSizes() + { + return pagesSizes; + } + + @Override + public void setPagesSizes(String[] aPagesSizes) + { + this.pagesSizes = Arrays.stream(aPagesSizes).mapToInt(Integer::parseInt).toArray(); + } +} diff --git a/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebar.html b/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebar.html index 6e235e1d07e..f496f52dac8 100644 --- a/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebar.html +++ b/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebar.html @@ -53,7 +53,7 @@

diff --git a/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebar.java b/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebar.java index 97d689966d9..a491903ab1a 100644 --- a/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebar.java +++ b/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebar.java @@ -24,7 +24,7 @@ import static org.apache.commons.lang3.StringUtils.isBlank; import java.io.IOException; -import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; @@ -96,8 +96,10 @@ import de.tudarmstadt.ukp.inception.search.ResultsGroup; import de.tudarmstadt.ukp.inception.search.SearchResult; import de.tudarmstadt.ukp.inception.search.SearchService; +import de.tudarmstadt.ukp.inception.search.config.SearchProperties; import de.tudarmstadt.ukp.inception.search.event.SearchQueryEvent; + public class SearchAnnotationSidebar extends AnnotationSidebar_ImplBase { @@ -110,6 +112,7 @@ public class SearchAnnotationSidebar private @SpringBean SearchService searchService; private @SpringBean UserDao userRepository; private @SpringBean ApplicationEventPublisherHolder applicationEventPublisher; + private @SpringBean SearchProperties searchProperties; private User currentUser; @@ -199,10 +202,6 @@ protected void populateItem(Item item) result.getGroupKey(), LambdaModel.of(() -> result))); } }; - /*searchResultGroups.setModel(LoadableDetachableModel.of(() -> - groupedSearchResults.getObject().values().stream() - .sorted(Comparator.comparing(ResultsGroup::getGroupKey)) - .collect(Collectors.toList())));*/ searchResultGroups.setItemsPerPage(searchOptions.getObject().getItemsPerPage()); resultsGroupContainer.add(searchResultGroups); mainContainer.add(new PagingNavigator("pagingNavigator", searchResultGroups)); @@ -252,15 +251,11 @@ protected void populateItem(Item item) mainContainer.add(annotationForm); } - private DropDownChoice createResultsPerPageSelection(String aId) + private DropDownChoice createResultsPerPageSelection(String aId) { - List choices = new ArrayList<>(); - // TODO: define the values somewhere else or replace with a numberfield - choices.add(5L); - choices.add(10L); - choices.add(20L); - choices.add(50L); - DropDownChoice itemsPerPageChoice = new BootstrapSelect<>(aId, choices); + List choices = Arrays.stream(searchProperties.getPagesSizes()).boxed().collect( + Collectors.toList()); + DropDownChoice itemsPerPageChoice = new BootstrapSelect<>(aId, choices); return itemsPerPageChoice; } diff --git a/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchResultsProvider.java b/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchResultsProvider.java index 5283031109c..9c09f601ed7 100644 --- a/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchResultsProvider.java +++ b/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchResultsProvider.java @@ -42,6 +42,7 @@ public class SearchResultsProvider implements IDataProvider private SearchService searchService; + // Query settings private User user; private Project project; private String query; @@ -49,11 +50,11 @@ public class SearchResultsProvider implements IDataProvider private AnnotationLayer annotationLayer; private AnnotationFeature annotationFeature; + // Cache private long totalResults = 0; private IModel> currentPageCache; - private long pageCacheState = -1; // -1 if a new query has been initialized, different otherwise - private long currentoffset = -1; // - private long currentcount = -1; + private long currentOffset = -1; + private long currentCount = -1; public SearchResultsProvider(SearchService aSearchService, IModel> aCurrentPageCache) @@ -69,38 +70,34 @@ public Iterator iterator(long first, long count) currentPageCache.setObject(Collections.emptyList()); return currentPageCache.getObject().iterator(); } - // Query if: - // We just initialized a new query (pageCacheState = -1) (might be replaced with - // currentPageCache.getObject() == null) - // or - // we want to retrieve a new page of the current query (currentoffset != first, currentcount - // != count) - if (pageCacheState == -1 || (currentoffset != first || currentcount != count)) { + // Query if we just initialized a new query currentPageCache.getObject() == null or we want + // to retrieve a new page of the current query (currentOffset != first, + // currentCount != count) + if (currentPageCache.getObject() == null || (currentOffset != first + || currentCount != count)) { try { List queryResults = searchService - .query(user, project, query, document, annotationLayer, annotationFeature, - first, count) - .entrySet().stream().map(e -> new ResultsGroup(e.getKey(), e.getValue())) - .collect(Collectors.toList()); + .query(user, project, query, document, annotationLayer, annotationFeature, + first, count).entrySet().stream() + .map(e -> new ResultsGroup(e.getKey(), e.getValue())) + .collect(Collectors.toList()); currentPageCache.setObject(queryResults); - // groupLevelSelectionsCache = initGroupLevelSelections(queryResults.stream().map(rg - // -> rg.getGroupKey()).collect(Collectors.toSet())); - pageCacheState = 1; - currentcount = count; - currentoffset = first; + currentCount = count; + currentOffset = first; return queryResults.iterator(); } catch (IOException e) { e.printStackTrace(); + return null; } catch (ExecutionException e) { e.printStackTrace(); + return null; } } else { return currentPageCache.getObject().iterator(); } - return null; } @Override @@ -145,7 +142,7 @@ public void initializeQuery(User aUser, Project aProject, String aQuery, annotationFeature = aAnnotationFeature; totalResults = -1; // reset size cache - pageCacheState = -1; // reset page cache + currentPageCache.setObject(null); // reset page cache } From c5734f5858c9e10cccbf4768937ad4d89450fbdc Mon Sep 17 00:00:00 2001 From: uwinch Date: Thu, 15 Aug 2019 17:26:24 +0200 Subject: [PATCH 013/453] #1256 Integrate curation into annotation page - VID changes --- .../curation/CurationEditorExtension.java | 49 ++++++++++--------- .../inception/curation/CurationVIDTest.java | 12 ++--- .../api/model/AnnotationSuggestion.java | 4 +- .../RecommendationEditorExtension.java | 15 +++--- 4 files changed, 43 insertions(+), 37 deletions(-) diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationEditorExtension.java b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationEditorExtension.java index d6646802649..c32e9beaae7 100644 --- a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationEditorExtension.java +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationEditorExtension.java @@ -69,6 +69,8 @@ public void handleAction(AnnotationActionHandler aPanel, AnnotatorState aState, if (!aParamId.getExtensionId().equals(EXTENSION_ID)) { return; } + + VID extendedVID = parse(aParamId); // Annotation has been selected for gold if (SpanAnnotationResponse.is(aAction)) { //TODO is this action only for spans //, what about relations ? @@ -77,6 +79,30 @@ public void handleAction(AnnotationActionHandler aPanel, AnnotatorState aState, } + /** + * Parse extension payload of given VID into CurationVID + */ + protected VID parse(VID aParamId) + { + // format of extension payload is : with standard VID format + // -..@ + Matcher matcher = Pattern.compile("(?:(?\\w+)\\:)" + + "(?.+)").matcher(aParamId.getExtensionPayload()); + if (!matcher.matches()) { + return aParamId; + } + + if (matcher.group("VID") == null || + matcher.group("USER") == null ) { + return aParamId; + } + + String vidStr = matcher.group("VID"); + String username = matcher.group("USER"); + return new CurationVID(aParamId.getExtensionId(), aParamId.getExtensionPayload(), username, + VID.parse(vidStr)); + } + @Override public void render(CAS aCas, AnnotatorState aState, VDocument aVdoc, int aWindowBeginOffset, int aWindowEndOffset) @@ -104,7 +130,7 @@ public void render(CAS aCas, AnnotatorState aState, VDocument aVdoc, int aWindow // copy all arcs and spans to existing doc with new VID for (VObject vobj : tmpDoc.vobjects()) { VID vid = vobj.getVid(); - VID extendedVID = parse(vid, vid.getExtensionPayload()); + VID extendedVID = parse(vid); vobj.setVid(extendedVID); aVdoc.add(vobj); } @@ -118,25 +144,4 @@ public void render(CAS aCas, AnnotatorState aState, VDocument aVdoc, int aWindow } } - - @Override - public VID parse(VID aParamId, String aVIDString) - { - // format is : with standard VID format -..@ - Matcher matcher = Pattern.compile("(?:(?\\w+)\\:)" - + "(?.+)").matcher(aVIDString); - if (!matcher.matches()) { - return aParamId; - } - - if (matcher.group("VID") == null || - matcher.group("USER") == null ) { - return aParamId; - } - - String vidStr = matcher.group("VID"); - String username = matcher.group("USER"); - return new CurationVID(aParamId.getExtensionId(), aParamId.getExtensionPayload(), username, - VID.parse(vidStr)); - } } diff --git a/inception-curation/src/test/java/de/tudarmstadt/ukp/inception/curation/CurationVIDTest.java b/inception-curation/src/test/java/de/tudarmstadt/ukp/inception/curation/CurationVIDTest.java index c2187cc6815..eb0375cd6b1 100644 --- a/inception-curation/src/test/java/de/tudarmstadt/ukp/inception/curation/CurationVIDTest.java +++ b/inception-curation/src/test/java/de/tudarmstadt/ukp/inception/curation/CurationVIDTest.java @@ -36,18 +36,18 @@ public void setup() { @Test public void testParse() { - assertParseVid(VID.parse("ext:kevin:10"), "ext", -1, 10, -1, -1, -1, "kevin", "kevin:10"); - assertParseVid(VID.parse("ext:kevin:10.1"), "ext", -1, 10, -1, 1, -1, "kevin", "kevin:10.1"); - assertParseVid(VID.parse("ext:kevin:10.1.2"), "ext", -1, 10, -1, 1, 2, "kevin", "kevin:10.1.2"); - assertParseVid(VID.parse("ext:kevin:10-1.2.3"), "ext", -1, 10, 1, 2, 3, "kevin", "kevin:10-1.2.3"); - assertParseVid(VID.parse("ext:kevin:10-1.2.3@1"), "ext", 1, 10, 1, 2, 3, "kevin", "kevin:10-1.2.3@1"); + assertParseVid(VID.parse("ext:10-kevin:10"), "ext", -1, 10, -1, -1, -1, "kevin", "kevin:10"); + assertParseVid(VID.parse("ext:10-kevin:10.1"), "ext", -1, 10, -1, 1, -1, "kevin", "kevin:10.1"); + assertParseVid(VID.parse("ext:10-kevin:10.1.2"), "ext", -1, 10, -1, 1, 2, "kevin", "kevin:10.1.2"); + assertParseVid(VID.parse("ext:10-kevin:10-1.2.3"), "ext", -1, 10, 1, 2, 3, "kevin", "kevin:10-1.2.3"); + assertParseVid(VID.parse("ext:10-kevin:10-1.2.3@1"), "ext", 1, 10, 1, 2, 3, "kevin", "kevin:10-1.2.3@1"); } private void assertParseVid(VID aVID, String aExtensionId, int aLayerId, int aAnnotationID, int aSubAnnotationId, int aAttribute, int aSlot, String aUsername, String aExtensionPayload) { - VID a = extension.parse(aVID, aExtensionPayload); + VID a = extension.parse(aVID); assertEquals(aExtensionId, a.getExtensionId()); assertEquals(aExtensionPayload, a.getExtensionPayload()); assertEquals(aUsername, ((CurationVID) a).getUsername()); diff --git a/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/AnnotationSuggestion.java b/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/AnnotationSuggestion.java index 453cd3fa399..02483502d78 100644 --- a/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/AnnotationSuggestion.java +++ b/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/AnnotationSuggestion.java @@ -236,7 +236,9 @@ public boolean isVisible() public VID getVID() { - return new VID(EXTENSION_ID, layerId, (int) recommenderId, id, VID.NONE, VID.NONE); + String payload = new VID(layerId, (int) recommenderId, id).toString(); + return new VID(EXTENSION_ID, layerId, (int) recommenderId, id, + payload); } @Override diff --git a/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/RecommendationEditorExtension.java b/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/RecommendationEditorExtension.java index 66e0fc7b4bb..f77f9018904 100644 --- a/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/RecommendationEditorExtension.java +++ b/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/RecommendationEditorExtension.java @@ -121,14 +121,18 @@ public void handleAction(AnnotationActionHandler aActionHandler, AnnotatorState if (!aVID.getExtensionId().equals(BEAN_NAME)) { return; } - + VID vid = VID.parse(aVID.getExtensionPayload()); + VID extendedVID = new VID(aVID.getExtensionId(), vid.getLayerId(), vid.getId(), + vid.getSubId(), vid.getAttribute(), vid.getSlot(), aVID.getExtensionPayload()); // Create annotation if (SpanAnnotationResponse.is(aAction)) { - actionAcceptRecommendation(aActionHandler, aState, aTarget, aCas, aVID, aBegin, aEnd); + actionAcceptRecommendation(aActionHandler, aState, aTarget, aCas, extendedVID, aBegin, + aEnd); } // Reject annotation else if (DoActionResponse.is(aAction)) { - actionRejectRecommendation(aActionHandler, aState, aTarget, aCas, aVID, aBegin, aEnd); + actionRejectRecommendation(aActionHandler, aState, aTarget, aCas, extendedVID, aBegin, + aEnd); } } @@ -272,9 +276,4 @@ public void render(CAS aCas, AnnotatorState aState, VDocument aVDoc, aWindowBeginOffset, aWindowEndOffset); } - @Override - public VID parse(VID aParamId, String aVIDString) - { - return VID.parse(aVIDString); - } } From 09cdfe2f772ea9475f7a0e1c0eaba26472df238e Mon Sep 17 00:00:00 2001 From: uwinch Date: Fri, 16 Aug 2019 16:09:53 +0200 Subject: [PATCH 014/453] #1256 Integrate curation into annotation page -fixed VID creation for curation --- .../curation/CurationEditorExtension.java | 68 +++++++++++++++++-- .../curation/CurationServiceImpl.java | 2 +- .../curation/sidebar/CurationSidebar.java | 3 +- 3 files changed, 67 insertions(+), 6 deletions(-) diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationEditorExtension.java b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationEditorExtension.java index c32e9beaae7..7eab4ebcad0 100644 --- a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationEditorExtension.java +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationEditorExtension.java @@ -30,6 +30,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +import de.tudarmstadt.ukp.clarin.webanno.api.AnnotationSchemaService; +import de.tudarmstadt.ukp.clarin.webanno.api.DocumentService; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.AnnotationEditorExtension; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.AnnotationEditorExtensionImplBase; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.action.AnnotationActionHandler; @@ -37,8 +39,12 @@ import de.tudarmstadt.ukp.clarin.webanno.api.annotation.model.AnnotatorState; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.model.VID; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.rendering.PreRenderer; +import de.tudarmstadt.ukp.clarin.webanno.api.annotation.rendering.model.VArc; +import de.tudarmstadt.ukp.clarin.webanno.api.annotation.rendering.model.VComment; +import de.tudarmstadt.ukp.clarin.webanno.api.annotation.rendering.model.VCommentType; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.rendering.model.VDocument; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.rendering.model.VObject; +import de.tudarmstadt.ukp.clarin.webanno.api.annotation.rendering.model.VSpan; import de.tudarmstadt.ukp.clarin.webanno.brat.message.SpanAnnotationResponse; import de.tudarmstadt.ukp.clarin.webanno.security.model.User; @@ -53,6 +59,8 @@ public class CurationEditorExtension private @Autowired CurationService curationService; private @Autowired PreRenderer preRenderer; + private @Autowired AnnotationSchemaService annotationService; + private @Autowired DocumentService documentService; @Override public String getBeanName() @@ -74,11 +82,52 @@ public void handleAction(AnnotationActionHandler aPanel, AnnotatorState aState, // Annotation has been selected for gold if (SpanAnnotationResponse.is(aAction)) { //TODO is this action only for spans //, what about relations ? - // TODO: store annotation in user CAS + saveAnnotation(aPanel, aState, aTarget, aCas, extendedVID, aBegin, aEnd); } } + /** + * Save annotation identified by aVID from given CAS in curator's CAS + * @throws AnnotationException + * @throws IOException + */ + private void saveAnnotation(AnnotationActionHandler aPanel, AnnotatorState aState, + AjaxRequestTarget aTarget, CAS aCas, VID aVID, int aBegin, int aEnd) + throws IOException, AnnotationException + { + // TODO look at webanno suggestionviewpanel onclientevent -> mergeSpan etc., then save in CAS + + // get curator's CAS +// SourceDocument doc = aState.getDocument(); +// Optional curatorCAS = Optional.empty(); +// // FIXME: aCas should already be curation CAS if it was selected (and opened) +// curatorCAS = curationService.retrieveCurationCAS(aState.getUser().getUsername(), +// aState.getProject().getId(), doc); +// +// if (!curatorCAS.isPresent()) { +// log.error( +// String.format("Curator CAS for %s not found", aState.getUser().getUsername())); +// return; +// } +// // get user CAS +// CAS srcCAS = documentService.readAnnotationCas(doc, ((CurationVID) aVID).getUsername()); +// +// // create/update anno in curator CAS +// AnnotationFS fs = selectByAddr(srcCAS, AnnotationFS.class, aVID.getId()); +// CAS destCAS = curatorCAS.get(); +// CasCopier copier = new CasCopier(srcCAS, destCAS); +// FeatureStructure curatedFs = copier.copyFs(fs); +// destCAS.addFsToIndexes(curatedFs); +// int address = WebAnnoCasUtil.getAddr(curatedFs); +// +// // Set selection to the accepted annotation and select it and load it into the detail editor +// // panel +// aState.getSelection().selectSpan(new VID(address), destCAS, aBegin, aEnd); +// aPanel.actionSelect(aTarget, destCAS); +// aPanel.actionCreateOrUpdate(aTarget, destCAS); + } + /** * Parse extension payload of given VID into CurationVID */ @@ -126,15 +175,26 @@ public void render(CAS aCas, AnnotatorState aState, VDocument aVdoc, int aWindow } VDocument tmpDoc = new VDocument(); preRenderer.render(tmpDoc, aWindowBeginOffset, aWindowEndOffset, userCas.get(), - aState.getAnnotationLayers()); //TODO: might need to filter the layers? + aState.getAnnotationLayers()); // copy all arcs and spans to existing doc with new VID + String username = user.getUsername(); + String color = "#cccccc"; //this is the same color as for recommendations for (VObject vobj : tmpDoc.vobjects()) { VID vid = vobj.getVid(); - VID extendedVID = parse(vid); + VID extendedVID = new CurationVID(EXTENSION_ID, username + ":" + vid.toString(), + username, vid); vobj.setVid(extendedVID); aVdoc.add(vobj); + // change color for other users' annos + if (vobj instanceof VSpan) { + ((VSpan) vobj).setColor(color); + } + else if (vobj instanceof VArc) { + ((VArc) vobj).setColor(color); + } + // set user name as comment + aVdoc.add(new VComment(extendedVID, VCommentType.INFO, username)); } - // TODO: add comment with username } catch (IOException e) { log.error(String.format("Could not retrieve CAS for user %s and project %d", diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationServiceImpl.java b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationServiceImpl.java index a457a3d95a9..7daf7e7ac17 100644 --- a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationServiceImpl.java +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationServiceImpl.java @@ -89,7 +89,7 @@ private CurationState getCurationState(String aUser, long aProjectId) { private class CurationState { private List selectedUsers; - // source document of the curated document + // to find source document of the curated document // the curationdoc can be retrieved from user (CURATION or current) and projectId private String curationUser; diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java index 283f21b657b..bfafaa1966a 100644 --- a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java @@ -112,6 +112,7 @@ protected void onSubmit() private void updateSettings() { + // FIXME: needs to open curation-doc if it is selected as destination String currentUsername = userRepository.getCurrentUser().getUsername(); long project = state.getProject().getId(); @@ -155,7 +156,7 @@ protected void populateItem(ListItem aItem) }; selectedUsers.add(users); usersForm.add(selectedUsers); - usersForm.add(visibleWhen(() -> !usersForm.getModelObject().isEmpty())); + usersForm.add(visibleWhen(() -> !users.getModelObject().isEmpty())); return usersForm; } From d6b24a9c86f3bc76b96bd05c73a3e260e9f5c6d3 Mon Sep 17 00:00:00 2001 From: Marcel de Boer Date: Fri, 16 Aug 2019 17:00:15 +0200 Subject: [PATCH 015/453] 1054 - Dealing with many results in search sidebar - add "all" to possible pagesizes - add doc for search settings --- .../search/config/SearchProperties.java | 4 +- .../search/config/SearchPropertiesImpl.java | 13 ++++-- .../asciidoc/admin-guide/settings_search.adoc | 41 +++++++++++++++++++ .../sidebar/SearchAnnotationSidebar.java | 9 ++-- .../search/sidebar/options/SearchOptions.java | 2 +- 5 files changed, 58 insertions(+), 11 deletions(-) create mode 100644 inception-search-core/src/main/resources/META-INF/asciidoc/admin-guide/settings_search.adoc diff --git a/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/config/SearchProperties.java b/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/config/SearchProperties.java index 623d25a5b28..30be2220cb1 100644 --- a/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/config/SearchProperties.java +++ b/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/config/SearchProperties.java @@ -2,7 +2,7 @@ public interface SearchProperties { - int[] getPagesSizes(); + long[] getPageSizes(); - void setPagesSizes(String[] aPagesSizes); + void setPageSizes(String[] aPagesSizes); } diff --git a/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/config/SearchPropertiesImpl.java b/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/config/SearchPropertiesImpl.java index 854db48d050..d16e5444cb1 100644 --- a/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/config/SearchPropertiesImpl.java +++ b/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/config/SearchPropertiesImpl.java @@ -9,17 +9,22 @@ @ConfigurationProperties("inception.search") public class SearchPropertiesImpl implements SearchProperties { - int[] pagesSizes = {10, 20, 50, 100, 500, 1000}; + long[] pagesSizes = {10, 20, 50, 100, 500, 1000}; @Override - public int[] getPagesSizes() + public long[] getPageSizes() { return pagesSizes; } @Override - public void setPagesSizes(String[] aPagesSizes) + public void setPageSizes(String[] aPageSizes) { - this.pagesSizes = Arrays.stream(aPagesSizes).mapToInt(Integer::parseInt).toArray(); + for (int i = 0; i < aPageSizes.length; i++) { + if (aPageSizes[i] == "all") { + aPageSizes[i] = Long.toString(Long.MAX_VALUE); + } + } + this.pagesSizes = Arrays.stream(aPageSizes).mapToLong(Long::parseLong).toArray(); } } diff --git a/inception-search-core/src/main/resources/META-INF/asciidoc/admin-guide/settings_search.adoc b/inception-search-core/src/main/resources/META-INF/asciidoc/admin-guide/settings_search.adoc new file mode 100644 index 00000000000..533fc031c57 --- /dev/null +++ b/inception-search-core/src/main/resources/META-INF/asciidoc/admin-guide/settings_search.adoc @@ -0,0 +1,41 @@ +// Copyright 2018 +// Ubiquitous Knowledge Processing (UKP) Lab +// Technische Universität Darmstadt +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +[[sect_settings_knowledge-base]] +=== Search Settings + +This section describes the global settings related to the search module. + +.Paging sizes +A list of possible numbers of results per page in the search sidebar. The user can select a number +from this list to determine how many results should be displayed per page when he makes a query. + +If no value for the parameter is specified, its default value is used. The default value is shown as +an example of how the parameter can be configured below: + +.Knowledge base settings overview +[cols="4*", options="header"] +|=== +| Setting +| Description +| Default +| Example + +| inception.search.resultsPerPage +| list of possible numbers of results per page for in-project search +| 10, 20, 50, 100, 500, 1000 +| 100, 1000, 2000, 3000 +|=== \ No newline at end of file diff --git a/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebar.java b/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebar.java index a491903ab1a..33a6f908faf 100644 --- a/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebar.java +++ b/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebar.java @@ -202,6 +202,7 @@ protected void populateItem(Item item) result.getGroupKey(), LambdaModel.of(() -> result))); } }; + searchOptions.getObject().setItemsPerPage(searchProperties.getPagesSizes()[0]); searchResultGroups.setItemsPerPage(searchOptions.getObject().getItemsPerPage()); resultsGroupContainer.add(searchResultGroups); mainContainer.add(new PagingNavigator("pagingNavigator", searchResultGroups)); @@ -246,16 +247,16 @@ protected void populateItem(Item item) })); annotationForm.setDefaultButton(annotateButton); - annotationForm.add(visibleWhen(() -> true));//!groupedSearchResults.getObject().isEmpty())); + annotationForm.add(visibleWhen(() -> !groupedSearchResults.getObject().isEmpty())); mainContainer.add(annotationForm); } - private DropDownChoice createResultsPerPageSelection(String aId) + private DropDownChoice createResultsPerPageSelection(String aId) { - List choices = Arrays.stream(searchProperties.getPagesSizes()).boxed().collect( + List choices = Arrays.stream(searchProperties.getPagesSizes()).boxed().collect( Collectors.toList()); - DropDownChoice itemsPerPageChoice = new BootstrapSelect<>(aId, choices); + DropDownChoice itemsPerPageChoice = new BootstrapSelect<>(aId, choices); return itemsPerPageChoice; } diff --git a/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/options/SearchOptions.java b/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/options/SearchOptions.java index f463da13ef5..810198c9b3f 100644 --- a/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/options/SearchOptions.java +++ b/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/options/SearchOptions.java @@ -30,7 +30,7 @@ public class SearchOptions extends Options private AnnotationFeature groupingFeature; - private long itemsPerPage = 10; + private long itemsPerPage; public boolean isLimitedToCurrentDocument() { From 868ee120bbafd017b7863eef17fe3ed22dd6037f Mon Sep 17 00:00:00 2001 From: Richard Eckart de Castilho Date: Tue, 20 Aug 2019 09:40:22 +0200 Subject: [PATCH 016/453] #1054 - Dealing with many results in search sidebar - Fix NPE when opening search sidebar - Fix typo in code which prevented compiling - Added missing license headers --- .../search/config/SearchProperties.java | 17 +++++++++++++++++ .../search/config/SearchPropertiesImpl.java | 17 +++++++++++++++++ .../search/sidebar/SearchAnnotationSidebar.html | 2 +- .../search/sidebar/SearchAnnotationSidebar.java | 7 ++++--- 4 files changed, 39 insertions(+), 4 deletions(-) diff --git a/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/config/SearchProperties.java b/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/config/SearchProperties.java index 30be2220cb1..067fcc224c7 100644 --- a/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/config/SearchProperties.java +++ b/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/config/SearchProperties.java @@ -1,3 +1,20 @@ +/* + * Copyright 2019 + * Ubiquitous Knowledge Processing (UKP) Lab + * Technische Universität Darmstadt + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package de.tudarmstadt.ukp.inception.search.config; public interface SearchProperties diff --git a/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/config/SearchPropertiesImpl.java b/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/config/SearchPropertiesImpl.java index d16e5444cb1..c3ddc714c85 100644 --- a/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/config/SearchPropertiesImpl.java +++ b/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/config/SearchPropertiesImpl.java @@ -1,3 +1,20 @@ +/* + * Copyright 2019 + * Ubiquitous Knowledge Processing (UKP) Lab + * Technische Universität Darmstadt + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package de.tudarmstadt.ukp.inception.search.config; import java.util.Arrays; diff --git a/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebar.html b/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebar.html index f496f52dac8..823ce490931 100644 --- a/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebar.html +++ b/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebar.html @@ -98,6 +98,7 @@
+ @@ -113,7 +114,6 @@
-
diff --git a/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebar.java b/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebar.java index 33a6f908faf..bed2c9a3cf8 100644 --- a/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebar.java +++ b/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebar.java @@ -202,7 +202,7 @@ protected void populateItem(Item item) result.getGroupKey(), LambdaModel.of(() -> result))); } }; - searchOptions.getObject().setItemsPerPage(searchProperties.getPagesSizes()[0]); + searchOptions.getObject().setItemsPerPage(searchProperties.getPageSizes()[0]); searchResultGroups.setItemsPerPage(searchOptions.getObject().getItemsPerPage()); resultsGroupContainer.add(searchResultGroups); mainContainer.add(new PagingNavigator("pagingNavigator", searchResultGroups)); @@ -247,14 +247,15 @@ protected void populateItem(Item item) })); annotationForm.setDefaultButton(annotateButton); - annotationForm.add(visibleWhen(() -> !groupedSearchResults.getObject().isEmpty())); + annotationForm.add(visibleWhen(() -> groupedSearchResults.getObject() != null + && !groupedSearchResults.getObject().isEmpty())); mainContainer.add(annotationForm); } private DropDownChoice createResultsPerPageSelection(String aId) { - List choices = Arrays.stream(searchProperties.getPagesSizes()).boxed().collect( + List choices = Arrays.stream(searchProperties.getPageSizes()).boxed().collect( Collectors.toList()); DropDownChoice itemsPerPageChoice = new BootstrapSelect<>(aId, choices); return itemsPerPageChoice; From eedbf146905102a9a22eed2eafc4ff3a027f45a1 Mon Sep 17 00:00:00 2001 From: UWinch Date: Wed, 21 Aug 2019 12:04:26 +0200 Subject: [PATCH 017/453] #1256 Integrate curation into annotation page - open page on curator user document - started on adding merge --- .../curation/CurationEditorExtension.java | 134 ++++++++++++------ .../curation/sidebar/CurationSidebar.java | 36 +++-- 2 files changed, 117 insertions(+), 53 deletions(-) diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationEditorExtension.java b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationEditorExtension.java index 7eab4ebcad0..8448dfea9d9 100644 --- a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationEditorExtension.java +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationEditorExtension.java @@ -17,6 +17,8 @@ */ package de.tudarmstadt.ukp.inception.curation; +import static de.tudarmstadt.ukp.clarin.webanno.api.annotation.util.WebAnnoCasUtil.selectAnnotationByAddr; + import java.io.IOException; import java.util.List; import java.util.Optional; @@ -24,6 +26,7 @@ import java.util.regex.Pattern; import org.apache.uima.cas.CAS; +import org.apache.uima.cas.text.AnnotationFS; import org.apache.wicket.ajax.AjaxRequestTarget; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -35,6 +38,7 @@ import de.tudarmstadt.ukp.clarin.webanno.api.annotation.AnnotationEditorExtension; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.AnnotationEditorExtensionImplBase; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.action.AnnotationActionHandler; +import de.tudarmstadt.ukp.clarin.webanno.api.annotation.adapter.TypeAdapter; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.exception.AnnotationException; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.model.AnnotatorState; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.model.VID; @@ -45,7 +49,11 @@ import de.tudarmstadt.ukp.clarin.webanno.api.annotation.rendering.model.VDocument; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.rendering.model.VObject; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.rendering.model.VSpan; -import de.tudarmstadt.ukp.clarin.webanno.brat.message.SpanAnnotationResponse; +import de.tudarmstadt.ukp.clarin.webanno.curation.casmerge.CasMerge; +import de.tudarmstadt.ukp.clarin.webanno.curation.casmerge.CasMergeOperationResult; +import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationFeature; +import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; +import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.clarin.webanno.security.model.User; @Component(CurationEditorExtension.EXTENSION_ID) @@ -55,6 +63,10 @@ public class CurationEditorExtension { public static final String EXTENSION_ID = "curationEditorExtension"; + // actions from the ui as of webanno #1388 + private static final String ACTION_SELECT_ARC_FOR_MERGE = "selectArcForMerge"; + private static final String ACTION_SELECT_SPAN_FOR_MERGE = "selectSpanForMerge"; + private Logger log = LoggerFactory.getLogger(getClass()); private @Autowired CurationService curationService; @@ -77,55 +89,87 @@ public void handleAction(AnnotationActionHandler aPanel, AnnotatorState aState, if (!aParamId.getExtensionId().equals(EXTENSION_ID)) { return; } - VID extendedVID = parse(aParamId); - // Annotation has been selected for gold - if (SpanAnnotationResponse.is(aAction)) { //TODO is this action only for spans - //, what about relations ? - saveAnnotation(aPanel, aState, aTarget, aCas, extendedVID, aBegin, aEnd); - } + String actionDesc = aAction.toString(); + if (!actionDesc.equals(ACTION_SELECT_SPAN_FOR_MERGE) && + !actionDesc.equals(ACTION_SELECT_ARC_FOR_MERGE)) { + return; + } + // Annotation has been selected for gold + saveAnnotation(aAction, aPanel, aState, aTarget, aCas, extendedVID, aBegin, aEnd); } /** - * Save annotation identified by aVID from given CAS in curator's CAS - * @throws AnnotationException - * @throws IOException + * Save annotation identified by aVID from user CAS to given curator's CAS */ - private void saveAnnotation(AnnotationActionHandler aPanel, AnnotatorState aState, - AjaxRequestTarget aTarget, CAS aCas, VID aVID, int aBegin, int aEnd) + private void saveAnnotation(String aAction, AnnotationActionHandler aPanel, + AnnotatorState aState, AjaxRequestTarget aTarget, CAS aTargetCas, VID aVID, int aBegin, + int aEnd) throws IOException, AnnotationException { - // TODO look at webanno suggestionviewpanel onclientevent -> mergeSpan etc., then save in CAS + AnnotationLayer layer = annotationService.getLayer(aVID.getLayerId()); + + // get user CAS and annotation (to be merged into curator's) + SourceDocument doc = aState.getDocument(); + String srcUser = ((CurationVID) aVID).getUsername(); + + if (!documentService.existsAnnotationDocument(doc, srcUser)) { + log.error( + String.format("Source CAS of %s for curation not found", srcUser)); + return; + } + + CAS srcCas = documentService.readAnnotationCas(doc, srcUser); + AnnotationFS sourceAnnotation = selectAnnotationByAddr(srcCas, aVID.getId()); + + // merge into curator's CAS depending on annotation type (span or arc) + CasMerge casMerge = new CasMerge(annotationService); + CasMergeOperationResult mergeResult; + if (ACTION_SELECT_SPAN_FOR_MERGE.equals(aAction.toString())) { + mergeResult = casMerge.mergeSpanAnnotation(doc, srcUser, layer, aTargetCas, + sourceAnnotation, layer.isAllowStacking()); + // open created/updates FS in annotation detail editorpanel + aState.getSelection().selectSpan(new VID(mergeResult.getOriginFSAddress()), aTargetCas, + aBegin, aEnd); + + } + else if (ACTION_SELECT_ARC_FOR_MERGE.equals(aAction.toString())) { + // this is a slot arc + if (aVID.isSlotSet()) { + TypeAdapter adapter = annotationService.getAdapter(layer); + AnnotationFeature feature = adapter.listFeatures().stream().sequential() + .skip(aVID.getAttribute()).findFirst().get(); + + mergeResult = casMerge.mergeSlotFeature(doc, srcUser, layer, aTargetCas, + sourceAnnotation, feature.getName(), aVID.getSlot()); + // open created/updates FS in annotation detail editorpanel + aState.getSelection().selectSpan(new VID(mergeResult.getOriginFSAddress()), + aTargetCas, aBegin, aEnd); + } + // normal relation annotation arc is clicked + else { + mergeResult = casMerge.mergeRelationAnnotation(doc, srcUser, layer, aTargetCas, + sourceAnnotation, layer.isAllowStacking()); + // open created/updates FS in annotation detail editorpanel + AnnotationFS originFS = selectAnnotationByAddr(aTargetCas, + mergeResult.getOriginFSAddress()); + AnnotationFS targetFS = selectAnnotationByAddr(aTargetCas, + mergeResult.getTargetFSAddress()); + aState.getSelection().selectArc(new VID(mergeResult.getOriginFSAddress()), originFS, + targetFS); + } + } + + aPanel.actionSelect(aTarget, aTargetCas); + aPanel.actionCreateOrUpdate(aTarget, aTargetCas); - // get curator's CAS -// SourceDocument doc = aState.getDocument(); -// Optional curatorCAS = Optional.empty(); -// // FIXME: aCas should already be curation CAS if it was selected (and opened) -// curatorCAS = curationService.retrieveCurationCAS(aState.getUser().getUsername(), -// aState.getProject().getId(), doc); -// -// if (!curatorCAS.isPresent()) { -// log.error( -// String.format("Curator CAS for %s not found", aState.getUser().getUsername())); -// return; -// } -// // get user CAS -// CAS srcCAS = documentService.readAnnotationCas(doc, ((CurationVID) aVID).getUsername()); -// -// // create/update anno in curator CAS -// AnnotationFS fs = selectByAddr(srcCAS, AnnotationFS.class, aVID.getId()); -// CAS destCAS = curatorCAS.get(); -// CasCopier copier = new CasCopier(srcCAS, destCAS); -// FeatureStructure curatedFs = copier.copyFs(fs); -// destCAS.addFsToIndexes(curatedFs); -// int address = WebAnnoCasUtil.getAddr(curatedFs); -// -// // Set selection to the accepted annotation and select it and load it into the detail editor -// // panel -// aState.getSelection().selectSpan(new VID(address), destCAS, aBegin, aEnd); -// aPanel.actionSelect(aTarget, destCAS); -// aPanel.actionCreateOrUpdate(aTarget, destCAS); +// // save merged Cas, might not need if using aPanel.actionCreateOrUpdate +// User curator = aState.getUser(); +// documentService.writeAnnotationCas(aTargetCas, doc, curator, true); //??? correct method +// updateDocumentTimestampAfterWrite(aState, documentService +// .getAnnotationCasTimestamp(doc, curator.getUsername())); // why do we also need + //this, we set update to true above? } /** @@ -166,15 +210,15 @@ public void render(CAS aCas, AnnotatorState aState, VDocument aVdoc, int aWindow for (User user : selectedUsers.get()) { try { - Optional userCas = curationService.retrieveCurationCAS(user.getUsername(), - projectId, aState.getDocument()); - if (!userCas.isPresent()) { + CAS userCas = documentService + .readAnnotationCas(aState.getDocument(), user.getUsername()); + if (userCas == null) { log.error(String.format("Could not retrieve CAS for user %s and project %d", user.getUsername(), projectId)); continue; } VDocument tmpDoc = new VDocument(); - preRenderer.render(tmpDoc, aWindowBeginOffset, aWindowEndOffset, userCas.get(), + preRenderer.render(tmpDoc, aWindowBeginOffset, aWindowEndOffset, userCas, aState.getAnnotationLayers()); // copy all arcs and spans to existing doc with new VID String username = user.getUsername(); diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java index bfafaa1966a..3429ee7ce19 100644 --- a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java @@ -27,6 +27,7 @@ import java.util.Optional; import java.util.stream.Collectors; +import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.markup.html.WebMarkupContainer; import org.apache.wicket.markup.html.basic.Label; import org.apache.wicket.markup.html.form.Check; @@ -38,6 +39,7 @@ import org.apache.wicket.model.IModel; import org.apache.wicket.model.LoadableDetachableModel; import org.apache.wicket.model.PropertyModel; +import org.apache.wicket.request.cycle.RequestCycle; import org.apache.wicket.spring.injection.annot.SpringBean; import de.tudarmstadt.ukp.clarin.webanno.api.CasProvider; @@ -47,9 +49,11 @@ import de.tudarmstadt.ukp.clarin.webanno.api.annotation.action.AnnotationActionHandler; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.model.AnnotatorState; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationDocumentState; +import de.tudarmstadt.ukp.clarin.webanno.model.Mode; import de.tudarmstadt.ukp.clarin.webanno.model.PermissionLevel; import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.clarin.webanno.security.UserDao; +import de.tudarmstadt.ukp.clarin.webanno.security.model.Role; import de.tudarmstadt.ukp.clarin.webanno.security.model.User; import de.tudarmstadt.ukp.clarin.webanno.ui.annotation.AnnotationPage; import de.tudarmstadt.ukp.clarin.webanno.ui.annotation.sidebar.AnnotationSidebar_ImplBase; @@ -75,9 +79,9 @@ public class CurationSidebar private String selectedCurationTarget = DEFAULT_CURATION_TARGET; private AnnotatorState state; -// private AnnotationPage annoPage; + private AnnotationPage annoPage; - // FIXME: only show to people who are curators + // TODO: only show to people who are curators public CurationSidebar(String aId, IModel aModel, AnnotationActionHandler aActionHandler, CasProvider aCasProvider, AnnotationPage aAnnotationPage) @@ -101,7 +105,7 @@ public CurationSidebar(String aId, IModel aModel, @Override protected void onSubmit() { - updateSettings(); + updateCurator(); } }; RadioChoice curationTargetBtn = new RadioChoice("curationTargetRadioBtn", @@ -110,20 +114,36 @@ protected void onSubmit() mainContainer.add(targetForm); } - private void updateSettings() + private void updateCurator() { - // FIXME: needs to open curation-doc if it is selected as destination - String currentUsername = userRepository.getCurrentUser().getUsername(); + // no change + if (selectedCurationTarget.equals(state.getUser().getUsername())) { + return; + } + + // update stored curator long project = state.getProject().getId(); - + User curator = userRepository.getCurrentUser(); + String currentUsername = curator.getUsername(); if (selectedCurationTarget.equals(DEFAULT_CURATION_TARGET)) { - curationService.updateCurationName(currentUsername, project, currentUsername); + state.setMode(Mode.ANNOTATION); } else { + if (!userRepository.exists(CURATION_USER)) { + userRepository.create(new User(CURATION_USER, Role.ROLE_USER)); + // TODO: give rights: curator? + } + curator = userRepository.get(CURATION_USER); curationService.updateCurationName(currentUsername, project, CURATION_USER); + state.setMode(Mode.CURATION); } + + // open curation-doc + state.setUser(curator); + RequestCycle.get().find(AjaxRequestTarget.class) + .ifPresent(t -> annoPage.actionRefreshDocument(t)); } private Form> createUserSelection() From fe49cc2bae799db55c3b81bbf61c7f98673564d1 Mon Sep 17 00:00:00 2001 From: uwinch Date: Fri, 23 Aug 2019 17:15:00 +0200 Subject: [PATCH 018/453] #1256 Integrate curation into annotation page - fix merge pom conflict - create merged arc, span --- .../curation/CurationEditorExtension.java | 44 +++++++++++-------- .../ukp/inception/curation/CurationVID.java | 13 ++++-- .../curation/sidebar/CurationSidebar.java | 3 ++ pom.xml | 3 +- 4 files changed, 40 insertions(+), 23 deletions(-) diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationEditorExtension.java b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationEditorExtension.java index 8448dfea9d9..f3e62f0ebb7 100644 --- a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationEditorExtension.java +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationEditorExtension.java @@ -26,6 +26,8 @@ import java.util.regex.Pattern; 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.wicket.ajax.AjaxRequestTarget; import org.slf4j.Logger; @@ -35,6 +37,7 @@ import de.tudarmstadt.ukp.clarin.webanno.api.AnnotationSchemaService; import de.tudarmstadt.ukp.clarin.webanno.api.DocumentService; +import de.tudarmstadt.ukp.clarin.webanno.api.WebAnnoConst; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.AnnotationEditorExtension; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.AnnotationEditorExtensionImplBase; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.action.AnnotationActionHandler; @@ -63,9 +66,9 @@ public class CurationEditorExtension { public static final String EXTENSION_ID = "curationEditorExtension"; - // actions from the ui as of webanno #1388 - private static final String ACTION_SELECT_ARC_FOR_MERGE = "selectArcForMerge"; - private static final String ACTION_SELECT_SPAN_FOR_MERGE = "selectSpanForMerge"; + // actions from the ui when selecting span or arc annotation + private static final String ACTION_SELECT_ARC = "arcOpenDialog"; + private static final String ACTION_SELECT_SPAN = "spanOpenDialog"; private Logger log = LoggerFactory.getLogger(getClass()); @@ -91,9 +94,7 @@ public void handleAction(AnnotationActionHandler aPanel, AnnotatorState aState, } VID extendedVID = parse(aParamId); - String actionDesc = aAction.toString(); - if (!actionDesc.equals(ACTION_SELECT_SPAN_FOR_MERGE) && - !actionDesc.equals(ACTION_SELECT_ARC_FOR_MERGE)) { + if (!aAction.equals(ACTION_SELECT_ARC) && !aAction.equals(ACTION_SELECT_SPAN)) { return; } // Annotation has been selected for gold @@ -126,15 +127,15 @@ private void saveAnnotation(String aAction, AnnotationActionHandler aPanel, // merge into curator's CAS depending on annotation type (span or arc) CasMerge casMerge = new CasMerge(annotationService); CasMergeOperationResult mergeResult; - if (ACTION_SELECT_SPAN_FOR_MERGE.equals(aAction.toString())) { + if (ACTION_SELECT_SPAN.equals(aAction.toString())) { mergeResult = casMerge.mergeSpanAnnotation(doc, srcUser, layer, aTargetCas, sourceAnnotation, layer.isAllowStacking()); // open created/updates FS in annotation detail editorpanel - aState.getSelection().selectSpan(new VID(mergeResult.getOriginFSAddress()), aTargetCas, + aState.getSelection().selectSpan(new VID(mergeResult.getResultFSAddress()), aTargetCas, aBegin, aEnd); } - else if (ACTION_SELECT_ARC_FOR_MERGE.equals(aAction.toString())) { + else if (ACTION_SELECT_ARC.equals(aAction.toString())) { // this is a slot arc if (aVID.isSlotSet()) { TypeAdapter adapter = annotationService.getAdapter(layer); @@ -144,19 +145,23 @@ else if (ACTION_SELECT_ARC_FOR_MERGE.equals(aAction.toString())) { mergeResult = casMerge.mergeSlotFeature(doc, srcUser, layer, aTargetCas, sourceAnnotation, feature.getName(), aVID.getSlot()); // open created/updates FS in annotation detail editorpanel - aState.getSelection().selectSpan(new VID(mergeResult.getOriginFSAddress()), + aState.getSelection().selectSpan(new VID(mergeResult.getResultFSAddress()), aTargetCas, aBegin, aEnd); } // normal relation annotation arc is clicked else { + // FIXME: somewhere origin and target get mixed up mergeResult = casMerge.mergeRelationAnnotation(doc, srcUser, layer, aTargetCas, sourceAnnotation, layer.isAllowStacking()); // open created/updates FS in annotation detail editorpanel - AnnotationFS originFS = selectAnnotationByAddr(aTargetCas, - mergeResult.getOriginFSAddress()); - AnnotationFS targetFS = selectAnnotationByAddr(aTargetCas, - mergeResult.getTargetFSAddress()); - aState.getSelection().selectArc(new VID(mergeResult.getOriginFSAddress()), originFS, + AnnotationFS mergedAnno = selectAnnotationByAddr(aTargetCas, + mergeResult.getResultFSAddress()); + Type depType = mergedAnno.getType(); + Feature originFeat = depType.getFeatureByBaseName(WebAnnoConst.FEAT_REL_SOURCE); + Feature targetFeat = depType.getFeatureByBaseName(WebAnnoConst.FEAT_REL_TARGET); + AnnotationFS originFS = (AnnotationFS) mergedAnno.getFeatureValue(originFeat); + AnnotationFS targetFS = (AnnotationFS) mergedAnno.getFeatureValue(targetFeat); + aState.getSelection().selectArc(new VID(mergeResult.getResultFSAddress()), originFS, targetFS); } } @@ -192,8 +197,8 @@ protected VID parse(VID aParamId) String vidStr = matcher.group("VID"); String username = matcher.group("USER"); - return new CurationVID(aParamId.getExtensionId(), aParamId.getExtensionPayload(), username, - VID.parse(vidStr)); + return new CurationVID(aParamId.getExtensionId(), username, + VID.parse(vidStr), aParamId.getExtensionPayload()); } @Override @@ -225,8 +230,9 @@ public void render(CAS aCas, AnnotatorState aState, VDocument aVdoc, int aWindow String color = "#cccccc"; //this is the same color as for recommendations for (VObject vobj : tmpDoc.vobjects()) { VID vid = vobj.getVid(); - VID extendedVID = new CurationVID(EXTENSION_ID, username + ":" + vid.toString(), - username, vid); + VID extendedVID = new CurationVID(EXTENSION_ID, username, + new VID(vobj.getLayer().getId(), vid.getId(), vid.getSubId(), + vid.getAttribute(), vid.getSlot())); vobj.setVid(extendedVID); aVdoc.add(vobj); // change color for other users' annos diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationVID.java b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationVID.java index 401547a7f53..e0d497d99e7 100644 --- a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationVID.java +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationVID.java @@ -26,13 +26,20 @@ public class CurationVID private final String username; - public CurationVID(String aExtId, String aExtPayload, String aUsername, VID aVID) + public CurationVID(String aExtId, String aUsername, VID aVID) { super(aExtId, aVID.getLayerId(), aVID.getId(), aVID.getSubId(), aVID.getAttribute(), - aVID.getSlot(), aExtPayload); + aVID.getSlot(), aUsername + ":" + aVID.toString()); + username = aUsername; + } + + public CurationVID(String aExtId, String aUsername, VID aVID, String aExtensionPayload) + { + super(aExtId, aVID.getLayerId(), aVID.getId(), aVID.getSubId(), aVID.getAttribute(), + aVID.getSlot(), aExtensionPayload); username = aUsername; } - + public String getUsername() { return username; diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java index 3429ee7ce19..83f37c960ec 100644 --- a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java @@ -136,6 +136,9 @@ private void updateCurator() // TODO: give rights: curator? } curator = userRepository.get(CURATION_USER); + if (curator == null) { + userRepository.create(new User(CURATION_USER)); + } curationService.updateCurationName(currentUsername, project, CURATION_USER); state.setMode(Mode.CURATION); } diff --git a/pom.xml b/pom.xml index 805cab872f7..7728809b72a 100644 --- a/pom.xml +++ b/pom.xml @@ -366,7 +366,7 @@ inception-search-mtas 0.11.0-SNAPSHOT - + de.tudarmstadt.ukp.inception.app inception-curation 0.11.0-SNAPSHOT @@ -631,6 +631,7 @@ com.h2database h2 1.4.197 + From 43974677612064b0912878984f57e968d4d359d7 Mon Sep 17 00:00:00 2001 From: uwinch Date: Mon, 26 Aug 2019 15:59:13 +0200 Subject: [PATCH 019/453] #1256 Integrate curation into annotation page - fixed displaying arcs - soem fixes towards curation user as curator --- .../curation/CurationEditorExtension.java | 64 +++++++++++-------- .../curation/CurationServiceImpl.java | 33 ++++++++++ .../curation/sidebar/CurationSidebar.java | 47 +++++++++----- 3 files changed, 104 insertions(+), 40 deletions(-) diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationEditorExtension.java b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationEditorExtension.java index f3e62f0ebb7..295ddb2a0f4 100644 --- a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationEditorExtension.java +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationEditorExtension.java @@ -20,7 +20,9 @@ import static de.tudarmstadt.ukp.clarin.webanno.api.annotation.util.WebAnnoCasUtil.selectAnnotationByAddr; import java.io.IOException; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -50,7 +52,6 @@ import de.tudarmstadt.ukp.clarin.webanno.api.annotation.rendering.model.VComment; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.rendering.model.VCommentType; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.rendering.model.VDocument; -import de.tudarmstadt.ukp.clarin.webanno.api.annotation.rendering.model.VObject; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.rendering.model.VSpan; import de.tudarmstadt.ukp.clarin.webanno.curation.casmerge.CasMerge; import de.tudarmstadt.ukp.clarin.webanno.curation.casmerge.CasMergeOperationResult; @@ -150,7 +151,6 @@ else if (ACTION_SELECT_ARC.equals(aAction.toString())) { } // normal relation annotation arc is clicked else { - // FIXME: somewhere origin and target get mixed up mergeResult = casMerge.mergeRelationAnnotation(doc, srcUser, layer, aTargetCas, sourceAnnotation, layer.isAllowStacking()); // open created/updates FS in annotation detail editorpanel @@ -168,13 +168,6 @@ else if (ACTION_SELECT_ARC.equals(aAction.toString())) { aPanel.actionSelect(aTarget, aTargetCas); aPanel.actionCreateOrUpdate(aTarget, aTargetCas); - -// // save merged Cas, might not need if using aPanel.actionCreateOrUpdate -// User curator = aState.getUser(); -// documentService.writeAnnotationCas(aTargetCas, doc, curator, true); //??? correct method -// updateDocumentTimestampAfterWrite(aState, documentService -// .getAnnotationCasTimestamp(doc, curator.getUsername())); // why do we also need - //this, we set update to true above? } /** @@ -215,8 +208,8 @@ public void render(CAS aCas, AnnotatorState aState, VDocument aVdoc, int aWindow for (User user : selectedUsers.get()) { try { - CAS userCas = documentService - .readAnnotationCas(aState.getDocument(), user.getUsername()); + CAS userCas = documentService.readAnnotationCas(aState.getDocument(), + user.getUsername()); if (userCas == null) { log.error(String.format("Could not retrieve CAS for user %s and project %d", user.getUsername(), projectId)); @@ -227,24 +220,44 @@ public void render(CAS aCas, AnnotatorState aState, VDocument aVdoc, int aWindow aState.getAnnotationLayers()); // copy all arcs and spans to existing doc with new VID String username = user.getUsername(); - String color = "#cccccc"; //this is the same color as for recommendations - for (VObject vobj : tmpDoc.vobjects()) { - VID vid = vobj.getVid(); - VID extendedVID = new CurationVID(EXTENSION_ID, username, - new VID(vobj.getLayer().getId(), vid.getId(), vid.getSubId(), + String color = "#cccccc"; // this is the same color as for recommendations + + // copy all spans and add to map as possible varc dependents + // spans with new vids identified by their old vid for lookup in varcs + Map newIdSpan = new HashMap<>(); + for (VSpan vspan : tmpDoc.spans()) { + VID aDepVID = vspan.getVid(); + VID prevVID = VID.copyVID(aDepVID); + VID newVID = new CurationVID(EXTENSION_ID, username, + new VID(vspan.getLayer().getId(), aDepVID.getId(), aDepVID.getSubId(), + aDepVID.getAttribute(), aDepVID.getSlot())); + vspan.setVid(newVID); + vspan.setColor(color); + newIdSpan.put(prevVID, vspan); + // set user name as comment + aVdoc.add(new VComment(newVID, VCommentType.INFO, username)); + aVdoc.add(vspan); + } + + // copy arcs to VDoc + for (VArc varc : tmpDoc.arcs()) { + // update varc vid + VID vid = varc.getVid(); + VID extendedVID = new CurationVID(EXTENSION_ID, username, + new VID(varc.getLayer().getId(), vid.getId(), vid.getSubId(), vid.getAttribute(), vid.getSlot())); - vobj.setVid(extendedVID); - aVdoc.add(vobj); - // change color for other users' annos - if (vobj instanceof VSpan) { - ((VSpan) vobj).setColor(color); - } - else if (vobj instanceof VArc) { - ((VArc) vobj).setColor(color); - } + varc.setVid(extendedVID); + // set target and src with new vids for arc + VSpan targetSpan = newIdSpan.get(varc.getTarget()); + VSpan srcSpan = newIdSpan.get(varc.getSource()); + varc.setSource(srcSpan.getVid()); + varc.setTarget(targetSpan.getVid()); + varc.setColor(color); // set user name as comment aVdoc.add(new VComment(extendedVID, VCommentType.INFO, username)); + aVdoc.add(varc); } + } catch (IOException e) { log.error(String.format("Could not retrieve CAS for user %s and project %d", @@ -254,4 +267,5 @@ else if (vobj instanceof VArc) { } } + } diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationServiceImpl.java b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationServiceImpl.java index 7daf7e7ac17..bb133e2e7a3 100644 --- a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationServiceImpl.java +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationServiceImpl.java @@ -29,10 +29,17 @@ import org.apache.commons.lang3.builder.HashCodeBuilder; import org.apache.uima.cas.CAS; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.event.EventListener; +import org.springframework.security.core.session.SessionDestroyedEvent; +import org.springframework.security.core.session.SessionInformation; +import org.springframework.security.core.session.SessionRegistry; import org.springframework.stereotype.Component; import de.tudarmstadt.ukp.clarin.webanno.api.DocumentService; +import de.tudarmstadt.ukp.clarin.webanno.api.ProjectService; +import de.tudarmstadt.ukp.clarin.webanno.model.Project; import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; +import de.tudarmstadt.ukp.clarin.webanno.security.UserDao; import de.tudarmstadt.ukp.clarin.webanno.security.model.User; @Component @@ -42,6 +49,9 @@ public class CurationServiceImpl implements CurationService private ConcurrentMap curationStates; private @Autowired DocumentService documentService; + private @Autowired SessionRegistry sessionRegistry; + private @Autowired ProjectService projectService; + private @Autowired UserDao userRegistry; public CurationServiceImpl() { @@ -165,5 +175,28 @@ public void removeCurrentUserInformation(String aCurrentUser, long aProjectId) curationStates.remove(new CurationStateKey(aCurrentUser, aProjectId)); } } + + @EventListener + public void onSessionDestroyed(SessionDestroyedEvent event) + { + SessionInformation info = sessionRegistry.getSessionInformation(event.getId()); + // Could be an anonymous session without information. + if (info != null) { + String username = (String) info.getPrincipal(); + clearState(username); + } + } + + private void clearState(String aUsername) + { +// projectService.listAccessibleProjects(userRegistry.get(aUsername)).stream() +// .map(Project::getId) +// .forEach(pId -> removeCurrentUserInformation(aUsername, pId)); + User currentUser = userRegistry.get(aUsername); + List projects = projectService.listAccessibleProjects(currentUser); + for (Project project : projects) { + removeCurrentUserInformation(aUsername, project.getId()); + } + } } diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java index 83f37c960ec..086601f4f10 100644 --- a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java @@ -17,9 +17,11 @@ */ package de.tudarmstadt.ukp.inception.curation.sidebar; +import static de.tudarmstadt.ukp.clarin.webanno.api.CasUpgradeMode.FORCE_CAS_UPGRADE; import static de.tudarmstadt.ukp.clarin.webanno.api.WebAnnoConst.CURATION_USER; import static de.tudarmstadt.ukp.clarin.webanno.support.lambda.LambdaBehavior.visibleWhen; +import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -27,7 +29,9 @@ import java.util.Optional; import java.util.stream.Collectors; +import org.apache.uima.cas.CAS; import org.apache.wicket.ajax.AjaxRequestTarget; +import org.apache.wicket.feedback.IFeedback; import org.apache.wicket.markup.html.WebMarkupContainer; import org.apache.wicket.markup.html.basic.Label; import org.apache.wicket.markup.html.form.Check; @@ -41,6 +45,8 @@ import org.apache.wicket.model.PropertyModel; import org.apache.wicket.request.cycle.RequestCycle; import org.apache.wicket.spring.injection.annot.SpringBean; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import de.tudarmstadt.ukp.clarin.webanno.api.CasProvider; import de.tudarmstadt.ukp.clarin.webanno.api.DocumentService; @@ -48,8 +54,8 @@ import de.tudarmstadt.ukp.clarin.webanno.api.annotation.AnnotationEditorExtensionRegistry; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.action.AnnotationActionHandler; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.model.AnnotatorState; +import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationDocument; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationDocumentState; -import de.tudarmstadt.ukp.clarin.webanno.model.Mode; import de.tudarmstadt.ukp.clarin.webanno.model.PermissionLevel; import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.clarin.webanno.security.UserDao; @@ -66,6 +72,8 @@ public class CurationSidebar private static final long serialVersionUID = -4195790451286055737L; private static final String DEFAULT_CURATION_TARGET = "my document"; + private Logger log = LoggerFactory.getLogger(getClass()); + private @SpringBean UserDao userRepository; private @SpringBean ProjectService projectService; private @SpringBean AnnotationEditorExtensionRegistry extensionRegistry; @@ -88,7 +96,6 @@ public CurationSidebar(String aId, IModel aModel, { super(aId, aModel, aActionHandler, aCasProvider, aAnnotationPage); state = aModel.getObject(); -// annoPage = aAnnotationPage; WebMarkupContainer mainContainer = new WebMarkupContainer("mainContainer"); mainContainer.setOutputMarkupId(true); add(mainContainer); @@ -114,13 +121,9 @@ protected void onSubmit() mainContainer.add(targetForm); } + // FIXME: still cannot access annotation detail editor when CURATION_USER is selected private void updateCurator() { - // no change - if (selectedCurationTarget.equals(state.getUser().getUsername())) { - return; - } - // update stored curator long project = state.getProject().getId(); User curator = userRepository.getCurrentUser(); @@ -128,19 +131,33 @@ private void updateCurator() if (selectedCurationTarget.equals(DEFAULT_CURATION_TARGET)) { curationService.updateCurationName(currentUsername, project, currentUsername); - state.setMode(Mode.ANNOTATION); } else { if (!userRepository.exists(CURATION_USER)) { - userRepository.create(new User(CURATION_USER, Role.ROLE_USER)); - // TODO: give rights: curator? + try { + User curationUser = new User(CURATION_USER, Role.ROLE_USER); + userRepository.create(curationUser); + AnnotationDocument annotationDocument = documentService + .createOrGetAnnotationDocument(state.getDocument(), curationUser); + // Update the annotation document CAS + CAS editorCas = documentService.readAnnotationCas(annotationDocument, + FORCE_CAS_UPGRADE); + // After creating an new CAS or upgrading the CAS, we need to save it + documentService.writeAnnotationCas(editorCas, annotationDocument, true); + } + catch (IOException e) { + String errorMsg = String.format("Could not create/read/write " + + "CURATOR_USER Cas: %s", e.getMessage()); + log.error(errorMsg); + error(errorMsg); + + RequestCycle.get().find(AjaxRequestTarget.class) + .ifPresent(t -> t.addChildren(getPage(), IFeedback.class)); + } + } curator = userRepository.get(CURATION_USER); - if (curator == null) { - userRepository.create(new User(CURATION_USER)); - } curationService.updateCurationName(currentUsername, project, CURATION_USER); - state.setMode(Mode.CURATION); } // open curation-doc @@ -223,7 +240,7 @@ private void showUsers() { Collection users = selectedUsers.getModelObject(); curationService.updateUsersSelectedForCuration( - userRepository.getCurrentUser().getUsername(), state.getProject().getId(), users); + state.getUser().getUsername(), state.getProject().getId(), users); // refresh should call render of PreRenderer and render of editor-extensions ? //annoPage.actionRefreshDocument(aTarget); } From ca6c44a8a3bb954c1bdb9aa3166c9481c6e32178 Mon Sep 17 00:00:00 2001 From: UWinch Date: Mon, 9 Sep 2019 08:13:12 +0200 Subject: [PATCH 020/453] #1256 Integrate curation into annotation page - re-added curation menuitem - ui fix for sidebar - fix anno selection error --- .../tudarmstadt/ukp/inception/INCEpTION.java | 1 - .../app/menu/CurationPageMenuItem.java | 86 +++++++++++++++++++ .../curation/sidebar/CurationSidebar.java | 45 ++++------ pom.xml | 4 + 4 files changed, 106 insertions(+), 30 deletions(-) create mode 100644 inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/app/menu/CurationPageMenuItem.java diff --git a/inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/INCEpTION.java b/inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/INCEpTION.java index 00cc7259d8b..f01e9ba5387 100644 --- a/inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/INCEpTION.java +++ b/inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/INCEpTION.java @@ -87,7 +87,6 @@ AutomationTrainingDocumentExporter.class, // INCEpTION uses the original DKPro Core CoNLL-U components ConllUFormatSupport.class, - // exclude curation from dashboard CurationPageMenuItem.class })}) @EntityScan(basePackages = { diff --git a/inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/app/menu/CurationPageMenuItem.java b/inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/app/menu/CurationPageMenuItem.java new file mode 100644 index 00000000000..5bba5316645 --- /dev/null +++ b/inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/app/menu/CurationPageMenuItem.java @@ -0,0 +1,86 @@ +/* + * Copyright 2018 + * Ubiquitous Knowledge Processing (UKP) Lab + * Technische Universität Darmstadt + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.tudarmstadt.ukp.inception.app.menu; + +import org.apache.wicket.Page; +import org.apache.wicket.Session; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import de.tudarmstadt.ukp.clarin.webanno.api.ProjectService; +import de.tudarmstadt.ukp.clarin.webanno.api.WebAnnoConst; +import de.tudarmstadt.ukp.clarin.webanno.model.Project; +import de.tudarmstadt.ukp.clarin.webanno.security.UserDao; +import de.tudarmstadt.ukp.clarin.webanno.security.model.User; +import de.tudarmstadt.ukp.clarin.webanno.ui.core.menu.MenuItem; +import de.tudarmstadt.ukp.clarin.webanno.ui.curation.page.CurationPage; +import de.tudarmstadt.ukp.inception.ui.core.session.SessionMetaData; + +@Component +@Order(200) +public class CurationPageMenuItem implements MenuItem +{ + private @Autowired UserDao userRepo; + private @Autowired ProjectService projectService; + + @Override + public String getPath() + { + return "/curation"; + } + + @Override + public String getIcon() + { + return "images/data_table.png"; + } + + @Override + public String getLabel() + { + return "Curation"; + } + + /** + * Only project admins and curators can see this page + */ + @Override + public boolean applies() + { + Project sessionProject = Session.get().getMetaData(SessionMetaData.CURRENT_PROJECT); + if (sessionProject == null) { + return false; + } + + // The project object stored in the session is detached from the persistence context and + // cannot be used immediately in DB interactions. Fetch a fresh copy from the DB. + Project project = projectService.getProject(sessionProject.getId()); + + // Visible if the current user is a curator + User user = userRepo.getCurrentUser(); + return projectService.isCurator(project, user) + && WebAnnoConst.PROJECT_TYPE_ANNOTATION.equals(project.getMode()); + } + + @Override + public Class getPageClass() + { + return CurationPage.class; + } +} diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java index 086601f4f10..c856e6e92d1 100644 --- a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java @@ -17,11 +17,9 @@ */ package de.tudarmstadt.ukp.inception.curation.sidebar; -import static de.tudarmstadt.ukp.clarin.webanno.api.CasUpgradeMode.FORCE_CAS_UPGRADE; import static de.tudarmstadt.ukp.clarin.webanno.api.WebAnnoConst.CURATION_USER; import static de.tudarmstadt.ukp.clarin.webanno.support.lambda.LambdaBehavior.visibleWhen; -import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -29,9 +27,7 @@ import java.util.Optional; import java.util.stream.Collectors; -import org.apache.uima.cas.CAS; import org.apache.wicket.ajax.AjaxRequestTarget; -import org.apache.wicket.feedback.IFeedback; import org.apache.wicket.markup.html.WebMarkupContainer; import org.apache.wicket.markup.html.basic.Label; import org.apache.wicket.markup.html.form.Check; @@ -54,9 +50,9 @@ import de.tudarmstadt.ukp.clarin.webanno.api.annotation.AnnotationEditorExtensionRegistry; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.action.AnnotationActionHandler; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.model.AnnotatorState; -import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationDocument; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationDocumentState; import de.tudarmstadt.ukp.clarin.webanno.model.PermissionLevel; +import de.tudarmstadt.ukp.clarin.webanno.model.ProjectPermission; import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.clarin.webanno.security.UserDao; import de.tudarmstadt.ukp.clarin.webanno.security.model.Role; @@ -117,14 +113,13 @@ protected void onSubmit() }; RadioChoice curationTargetBtn = new RadioChoice("curationTargetRadioBtn", new PropertyModel(this, "selectedCurationTarget"), curationTargets); + curationTargetBtn.setPrefix("
"); targetForm.add(curationTargetBtn); mainContainer.add(targetForm); } - // FIXME: still cannot access annotation detail editor when CURATION_USER is selected private void updateCurator() { - // update stored curator long project = state.getProject().getId(); User curator = userRepository.getCurrentUser(); String currentUsername = curator.getUsername(); @@ -134,34 +129,26 @@ private void updateCurator() } else { if (!userRepository.exists(CURATION_USER)) { - try { - User curationUser = new User(CURATION_USER, Role.ROLE_USER); - userRepository.create(curationUser); - AnnotationDocument annotationDocument = documentService - .createOrGetAnnotationDocument(state.getDocument(), curationUser); - // Update the annotation document CAS - CAS editorCas = documentService.readAnnotationCas(annotationDocument, - FORCE_CAS_UPGRADE); - // After creating an new CAS or upgrading the CAS, we need to save it - documentService.writeAnnotationCas(editorCas, annotationDocument, true); - } - catch (IOException e) { - String errorMsg = String.format("Could not create/read/write " - + "CURATOR_USER Cas: %s", e.getMessage()); - log.error(errorMsg); - error(errorMsg); - - RequestCycle.get().find(AjaxRequestTarget.class) - .ifPresent(t -> t.addChildren(getPage(), IFeedback.class)); - } - + User curationUser = new User(CURATION_USER, Role.ROLE_USER); + userRepository.create(curationUser); + projectService.createProjectPermission(new ProjectPermission(state.getProject(), + CURATION_USER, PermissionLevel.ANNOTATOR)); + projectService.createProjectPermission(new ProjectPermission(state.getProject(), + CURATION_USER, PermissionLevel.CURATOR)); + curator = userRepository.get(CURATION_USER); + state.setUser(curator); + state.getSelection().clear(); + curationService.updateCurationName(currentUsername, project, CURATION_USER); + RequestCycle.get().find(AjaxRequestTarget.class) + .ifPresent(t -> annoPage.actionLoadDocument(t)); + return; } curator = userRepository.get(CURATION_USER); curationService.updateCurationName(currentUsername, project, CURATION_USER); } - // open curation-doc state.setUser(curator); + state.getSelection().clear(); RequestCycle.get().find(AjaxRequestTarget.class) .ifPresent(t -> annoPage.actionRefreshDocument(t)); } diff --git a/pom.xml b/pom.xml index 7728809b72a..bb0b4349bf1 100644 --- a/pom.xml +++ b/pom.xml @@ -212,6 +212,10 @@ javax.activation-api runtime
+ + de.tudarmstadt.ukp.clarin.webanno + webanno-ui-curation + From e93ac1994ac13e4021d86aa68793a91d20b86299 Mon Sep 17 00:00:00 2001 From: Sibgha360 Date: Mon, 9 Sep 2019 16:33:44 +0200 Subject: [PATCH 021/453] #Issue Hints for new users - Empty list message added --- .../projectlist/ProjectsOverviewPage.html | 1 + .../projectlist/ProjectsOverviewPage.java | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/dashboard/projectlist/ProjectsOverviewPage.html b/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/dashboard/projectlist/ProjectsOverviewPage.html index b310a4f1d3a..c286f3f8b40 100644 --- a/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/dashboard/projectlist/ProjectsOverviewPage.html +++ b/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/dashboard/projectlist/ProjectsOverviewPage.html @@ -48,6 +48,7 @@
+
diff --git a/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/dashboard/projectlist/ProjectsOverviewPage.java b/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/dashboard/projectlist/ProjectsOverviewPage.java index ac982ca5ca4..377848ee3d8 100644 --- a/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/dashboard/projectlist/ProjectsOverviewPage.java +++ b/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/dashboard/projectlist/ProjectsOverviewPage.java @@ -103,6 +103,7 @@ public class ProjectsOverviewPage private static final String MID_PROJECT_ARCHIVE_UPLOAD = "projectArchiveUpload"; private static final String MID_LEAVE_PROJECT = "leaveProject"; private static final String MID_CONFIRM_LEAVE = "confirmLeave"; + private static final String MID_EMPTY_LIST_LABEL = "emptyListLabel"; private static final long serialVersionUID = -2159246322262294746L; @@ -118,6 +119,8 @@ public class ProjectsOverviewPage private IModel> activeRoleFilters; private ConfirmationDialog confirmLeaveDialog; + private Label emptyListLabel; + public ProjectsOverviewPage() { add(projectListContainer = createProjectList()); @@ -128,6 +131,10 @@ public ProjectsOverviewPage() new StringResourceModel("leaveDialog.title", this), new StringResourceModel("leaveDialog.text", this))); activeRoleFilters = Model.ofSet(new HashSet<>()); + + emptyListLabel = new Label(MID_EMPTY_LIST_LABEL, Model.of( + "At the moment there are no projects, To create your first project, click the button 'create new project'. To see how it works, click the button 'Get Started'.")); + projectListContainer.add(emptyListLabel); } private LambdaAjaxLink createNewProjectLink() @@ -202,10 +209,17 @@ protected void onConfigure() if (getModelObject().isEmpty()) { warn("There are no projects accessible to you matching the filter criteria."); + emptyListLabel.setVisible(true); + + } + else { + emptyListLabel.setVisible(false); } } }; + + WebMarkupContainer projectList = new WebMarkupContainer(MID_PROJECTS); projectList.setOutputMarkupPlaceholderTag(true); projectList.add(listview); From 6838ba6f335f567c9c1e300702982c1a863ff539 Mon Sep 17 00:00:00 2001 From: uwinch Date: Mon, 9 Sep 2019 16:53:12 +0200 Subject: [PATCH 022/453] #1256 Integrate curation into annotation page - added clear users button - fixed loading docs when switching between curators --- .../curation/CurationEditorExtension.java | 2 +- .../inception/curation/CurationService.java | 2 + .../curation/CurationServiceImpl.java | 24 +++++-- .../curation/sidebar/CurationSidebar.html | 5 +- .../curation/sidebar/CurationSidebar.java | 70 ++++++++----------- .../sidebar/CurationSidebar.properties | 1 + 6 files changed, 55 insertions(+), 49 deletions(-) diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationEditorExtension.java b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationEditorExtension.java index 295ddb2a0f4..12c0d25c1b5 100644 --- a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationEditorExtension.java +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationEditorExtension.java @@ -167,7 +167,7 @@ else if (ACTION_SELECT_ARC.equals(aAction.toString())) { } aPanel.actionSelect(aTarget, aTargetCas); - aPanel.actionCreateOrUpdate(aTarget, aTargetCas); + aPanel.actionCreateOrUpdate(aTarget, aTargetCas); //should also update timestamps } /** diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationService.java b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationService.java index 4dd28cfc765..a4aed6b4edc 100644 --- a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationService.java +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationService.java @@ -56,4 +56,6 @@ public void updateCurationName(String aCurrentUser, long aProjectId, * Removed stored curation information after user session has ended */ public void removeCurrentUserInformation(String aCurrentUser, long aProjectId); + + public void clearUsersSelectedForCuration(String aUsername, Long aId); } diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationServiceImpl.java b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationServiceImpl.java index bb133e2e7a3..939ebd92f83 100644 --- a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationServiceImpl.java +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationServiceImpl.java @@ -189,14 +189,24 @@ public void onSessionDestroyed(SessionDestroyedEvent event) private void clearState(String aUsername) { -// projectService.listAccessibleProjects(userRegistry.get(aUsername)).stream() -// .map(Project::getId) -// .forEach(pId -> removeCurrentUserInformation(aUsername, pId)); - User currentUser = userRegistry.get(aUsername); - List projects = projectService.listAccessibleProjects(currentUser); - for (Project project : projects) { - removeCurrentUserInformation(aUsername, project.getId()); + projectService.listAccessibleProjects(userRegistry.get(aUsername)).stream() + .map(Project::getId) + .forEach(pId -> removeCurrentUserInformation(aUsername, pId)); +// User currentUser = userRegistry.get(aUsername); +// List projects = projectService.listAccessibleProjects(currentUser); +// for (Project project : projects) { +// removeCurrentUserInformation(aUsername, project.getId()); +// } + } + + @Override + public void clearUsersSelectedForCuration(String aUsername, Long aProjectId) + { + synchronized (curationStates) + { + getCurationState(aUsername, aProjectId).setSelectedUsers(new ArrayList<>()); } + } } diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.html b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.html index eba2470070a..9c9f6bd14f3 100644 --- a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.html +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.html @@ -39,7 +39,8 @@

@@ -51,7 +52,7 @@

diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java index c856e6e92d1..da33b0ca4a7 100644 --- a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java @@ -39,7 +39,6 @@ import org.apache.wicket.model.IModel; import org.apache.wicket.model.LoadableDetachableModel; import org.apache.wicket.model.PropertyModel; -import org.apache.wicket.request.cycle.RequestCycle; import org.apache.wicket.spring.injection.annot.SpringBean; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -57,6 +56,7 @@ import de.tudarmstadt.ukp.clarin.webanno.security.UserDao; import de.tudarmstadt.ukp.clarin.webanno.security.model.Role; import de.tudarmstadt.ukp.clarin.webanno.security.model.User; +import de.tudarmstadt.ukp.clarin.webanno.support.lambda.LambdaAjaxButton; import de.tudarmstadt.ukp.clarin.webanno.ui.annotation.AnnotationPage; import de.tudarmstadt.ukp.clarin.webanno.ui.annotation.sidebar.AnnotationSidebar_ImplBase; import de.tudarmstadt.ukp.inception.curation.CurationService; @@ -66,7 +66,7 @@ public class CurationSidebar extends AnnotationSidebar_ImplBase { private static final long serialVersionUID = -4195790451286055737L; - private static final String DEFAULT_CURATION_TARGET = "my document"; + private static final String DEFAULT_CURATION_TARGET = " my document"; private Logger log = LoggerFactory.getLogger(getClass()); @@ -77,9 +77,10 @@ public class CurationSidebar private @SpringBean DocumentService documentService; private CheckGroup selectedUsers; + private Form> usersForm; private final List curationTargets = Arrays - .asList(new String[] { DEFAULT_CURATION_TARGET, "curation document" }); + .asList(new String[] { DEFAULT_CURATION_TARGET, " curation document" }); private String selectedCurationTarget = DEFAULT_CURATION_TARGET; private AnnotatorState state; @@ -92,25 +93,20 @@ public CurationSidebar(String aId, IModel aModel, { super(aId, aModel, aActionHandler, aCasProvider, aAnnotationPage); state = aModel.getObject(); + annoPage = aAnnotationPage; WebMarkupContainer mainContainer = new WebMarkupContainer("mainContainer"); mainContainer.setOutputMarkupId(true); add(mainContainer); // set up user-checklist - Form> usersForm = createUserSelection(); + usersForm = createUserSelection(); + usersForm.setOutputMarkupId(true); mainContainer.add(usersForm); // set up curation target radio button - Form targetForm = new Form("settingsForm") { - - private static final long serialVersionUID = -5535838955781542216L; - - @Override - protected void onSubmit() - { - updateCurator(); - } - }; + Form targetForm = new Form("settingsForm"); + LambdaAjaxButton applyButton = new LambdaAjaxButton<>("apply", this::updateCurator); + targetForm.add(applyButton); RadioChoice curationTargetBtn = new RadioChoice("curationTargetRadioBtn", new PropertyModel(this, "selectedCurationTarget"), curationTargets); curationTargetBtn.setPrefix("
"); @@ -118,7 +114,7 @@ protected void onSubmit() mainContainer.add(targetForm); } - private void updateCurator() + private void updateCurator(AjaxRequestTarget aTarget, Form aForm) { long project = state.getProject().getId(); User curator = userRepository.getCurrentUser(); @@ -135,38 +131,26 @@ private void updateCurator() CURATION_USER, PermissionLevel.ANNOTATOR)); projectService.createProjectPermission(new ProjectPermission(state.getProject(), CURATION_USER, PermissionLevel.CURATOR)); - curator = userRepository.get(CURATION_USER); - state.setUser(curator); - state.getSelection().clear(); - curationService.updateCurationName(currentUsername, project, CURATION_USER); - RequestCycle.get().find(AjaxRequestTarget.class) - .ifPresent(t -> annoPage.actionLoadDocument(t)); - return; } curator = userRepository.get(CURATION_USER); curationService.updateCurationName(currentUsername, project, CURATION_USER); } - // open curation-doc + state.setUser(curator); state.getSelection().clear(); - RequestCycle.get().find(AjaxRequestTarget.class) - .ifPresent(t -> annoPage.actionRefreshDocument(t)); + updateUsers(aTarget, aForm); + //open curation doc + annoPage.actionLoadDocument(aTarget); } private Form> createUserSelection() { Form> usersForm = new Form>("usersForm", - LoadableDetachableModel.of(this::listSelectedUsers)) - { - private static final long serialVersionUID = 1L; - - @Override - protected void onSubmit() - { - showUsers(); - } - - }; + LoadableDetachableModel.of(this::listSelectedUsers)); + LambdaAjaxButton clearButton = new LambdaAjaxButton<>("clear", this::clearUsers); + LambdaAjaxButton showButton = new LambdaAjaxButton<>("show", this::updateUsers); + usersForm.add(clearButton); + usersForm.add(showButton); selectedUsers = new CheckGroup("selectedUsers", usersForm.getModelObject()); ListView users = new ListView("users", LoadableDetachableModel.of(this::listUsers)) @@ -223,13 +207,21 @@ private boolean hasFinishedDoc(User aUser) } } - private void showUsers() + private void updateUsers(AjaxRequestTarget aTarget, Form aForm) { Collection users = selectedUsers.getModelObject(); curationService.updateUsersSelectedForCuration( state.getUser().getUsername(), state.getProject().getId(), users); - // refresh should call render of PreRenderer and render of editor-extensions ? - //annoPage.actionRefreshDocument(aTarget); + aTarget.add(usersForm); + annoPage.actionRefreshDocument(aTarget); + } + + private void clearUsers(AjaxRequestTarget aTarget, Form aForm) { + selectedUsers.setModelObject(new ArrayList<>()); + curationService.clearUsersSelectedForCuration( + state.getUser().getUsername(), state.getProject().getId()); + aTarget.add(usersForm); + annoPage.actionRefreshDocument(aTarget); } } diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.properties b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.properties index eb3736bbccd..41d9220085b 100644 --- a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.properties +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.properties @@ -18,5 +18,6 @@ usersLabel=Users settingsLabel=Settings show=Show apply=Apply +clear=Clear curationHeader=Curation curationTarget=Save as \ No newline at end of file From 711476e14c62e9db33cfe811be080e9f8a593138 Mon Sep 17 00:00:00 2001 From: uwinch Date: Tue, 10 Sep 2019 18:07:04 +0200 Subject: [PATCH 023/453] #1256 Integrate curation into annotation page - do not persist curation user --- .../curation/sidebar/CurationSidebar.java | 21 ++++++++++--------- .../service/RecommendationServiceImpl.java | 4 ++++ 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java index da33b0ca4a7..c1a70efbf11 100644 --- a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java @@ -51,7 +51,6 @@ import de.tudarmstadt.ukp.clarin.webanno.api.annotation.model.AnnotatorState; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationDocumentState; import de.tudarmstadt.ukp.clarin.webanno.model.PermissionLevel; -import de.tudarmstadt.ukp.clarin.webanno.model.ProjectPermission; import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.clarin.webanno.security.UserDao; import de.tudarmstadt.ukp.clarin.webanno.security.model.Role; @@ -113,6 +112,16 @@ public CurationSidebar(String aId, IModel aModel, targetForm.add(curationTargetBtn); mainContainer.add(targetForm); } + + + @Override + protected void onConfigure() + { + super.onConfigure(); + setEnabled(!documentService + .isAnnotationFinished(state.getDocument(), state.getUser())); + } + private void updateCurator(AjaxRequestTarget aTarget, Form aForm) { @@ -124,15 +133,7 @@ private void updateCurator(AjaxRequestTarget aTarget, Form aForm) project, currentUsername); } else { - if (!userRepository.exists(CURATION_USER)) { - User curationUser = new User(CURATION_USER, Role.ROLE_USER); - userRepository.create(curationUser); - projectService.createProjectPermission(new ProjectPermission(state.getProject(), - CURATION_USER, PermissionLevel.ANNOTATOR)); - projectService.createProjectPermission(new ProjectPermission(state.getProject(), - CURATION_USER, PermissionLevel.CURATOR)); - } - curator = userRepository.get(CURATION_USER); + curator = new User(CURATION_USER, Role.ROLE_USER); curationService.updateCurationName(currentUsername, project, CURATION_USER); } diff --git a/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommendationServiceImpl.java b/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommendationServiceImpl.java index 0e4a370f2e5..d87f4dc0974 100644 --- a/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommendationServiceImpl.java +++ b/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommendationServiceImpl.java @@ -494,6 +494,10 @@ public void triggerTrainingAndClassification(String aUser, Project aProject, Str { User user = userRepository.get(aUser); + if (user == null) { + return; + } + // Update the task count AtomicInteger count = trainingTaskCounter.computeIfAbsent( new RecommendationStateKey(user.getUsername(), aProject), From 8b0a152bd992a8ac4c4b96b0f85921609ae9e98e Mon Sep 17 00:00:00 2001 From: Sibgha360 Date: Tue, 10 Sep 2019 19:34:54 +0200 Subject: [PATCH 024/453] #1270 - Hints for new users - Enjoy hint API webjars included. --- inception-app-webapp/pom.xml | 13 +++++++++++++ .../app/config/InceptionResourcesBehavior.java | 11 +++++++++++ pom.xml | 16 ++++++++++++++++ 3 files changed, 40 insertions(+) diff --git a/inception-app-webapp/pom.xml b/inception-app-webapp/pom.xml index ef548af890e..9041ffe63c0 100644 --- a/inception-app-webapp/pom.xml +++ b/inception-app-webapp/pom.xml @@ -499,6 +499,19 @@ org.apache.wicket wicket-native-websocket-javax + + + org.webjars.bowergithub.xbsoftware + enjoyhint + + + org.webjars.npm + kinetic + + + org.webjars.bower + jquery.scrollTo + diff --git a/inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/app/config/InceptionResourcesBehavior.java b/inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/app/config/InceptionResourcesBehavior.java index f7e9e352652..8dd56e11160 100644 --- a/inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/app/config/InceptionResourcesBehavior.java +++ b/inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/app/config/InceptionResourcesBehavior.java @@ -23,6 +23,8 @@ import org.apache.wicket.markup.head.IHeaderResponse; import org.apache.wicket.markup.head.JavaScriptHeaderItem; +import de.agilecoders.wicket.webjars.request.resource.WebjarsCssResourceReference; +import de.agilecoders.wicket.webjars.request.resource.WebjarsJavaScriptResourceReference; import de.tudarmstadt.ukp.clarin.webanno.ui.core.page.WebAnnoJavascriptReference; import de.tudarmstadt.ukp.inception.app.css.InceptionCssReference; @@ -39,6 +41,15 @@ public void renderHead(Component aComponent, IHeaderResponse aResponse) // Loading WebAnno CSS here so it can override JQuery/Kendo CSS aResponse.render(CssHeaderItem.forReference(InceptionCssReference.get())); aResponse.render(JavaScriptHeaderItem.forReference(WebAnnoJavascriptReference.get())); + + // Loading resources for the tour guide feature for the new users + aResponse.render(JavaScriptHeaderItem + .forReference(new WebjarsJavaScriptResourceReference("enjoyhint/current/enjoyhint.js"))); + aResponse.render(JavaScriptHeaderItem + .forReference(new WebjarsJavaScriptResourceReference("enjoyhint/current/jquery.enjoyhint.js"))); + aResponse.render(CssHeaderItem.forReference(new WebjarsCssResourceReference("enjoyhint/current/jquery.enjoyhint.css"))); + aResponse.render(JavaScriptHeaderItem.forReference(new WebjarsJavaScriptResourceReference("jquery.scrollTo/current/jquery.scrollTo.js"))); + aResponse.render(JavaScriptHeaderItem.forReference(new WebjarsJavaScriptResourceReference("kinetic/current/kinetic.min.js"))); } public static InceptionResourcesBehavior get() diff --git a/pom.xml b/pom.xml index 39cb8fa9725..ad32bcb6090 100644 --- a/pom.xml +++ b/pom.xml @@ -1055,6 +1055,22 @@ d3js 5.5.0 + + + org.webjars.bowergithub.xbsoftware + enjoyhint + 3.1.0 + + + org.webjars.npm + kinetic + 5.2.0 + + + org.webjars.bower + jquery.scrollTo + 2.1.2 + From 2b887834efb9b89f9447c959aa8f78c3409e7754 Mon Sep 17 00:00:00 2001 From: uwinch Date: Mon, 16 Sep 2019 15:58:14 +0200 Subject: [PATCH 025/453] #1256 Integrate curation into annotation page -merge conflicts -show info-box --- inception-app-webapp/pom.xml | 2 +- .../src/main/java/de/tudarmstadt/ukp/inception/INCEpTION.java | 2 +- inception-curation/pom.xml | 2 +- .../ukp/inception/curation/CurationEditorExtension.java | 3 +++ .../ukp/inception/curation/sidebar/CurationSidebarFactory.java | 2 +- pom.xml | 1 + 6 files changed, 8 insertions(+), 4 deletions(-) diff --git a/inception-app-webapp/pom.xml b/inception-app-webapp/pom.xml index ff1d0f54a76..b57f2e010bb 100644 --- a/inception-app-webapp/pom.xml +++ b/inception-app-webapp/pom.xml @@ -494,7 +494,7 @@ de.tudarmstadt.ukp.inception.app inception-curation - 0.11.0-SNAPSHOT + 0.12.0-SNAPSHOT diff --git a/inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/INCEpTION.java b/inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/INCEpTION.java index 689d6c1b514..d8d67bb689e 100644 --- a/inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/INCEpTION.java +++ b/inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/INCEpTION.java @@ -90,7 +90,7 @@ // INCEpTION uses its recommenders, not the WebAnno automation code AutomationService.class, AutomationMiraTemplateExporter.class, - CurationPageMenuItem.class + CurationPageMenuItem.class, AutomationTrainingDocumentExporter.class })}) @EntityScan(basePackages = { diff --git a/inception-curation/pom.xml b/inception-curation/pom.xml index 95320988648..2e40550c252 100644 --- a/inception-curation/pom.xml +++ b/inception-curation/pom.xml @@ -20,7 +20,7 @@ de.tudarmstadt.ukp.inception.app inception-app - 0.11.0-SNAPSHOT + 0.12.0-SNAPSHOT inception-curation INCEpTION - Curation diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationEditorExtension.java b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationEditorExtension.java index 12c0d25c1b5..62eb2c54930 100644 --- a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationEditorExtension.java +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationEditorExtension.java @@ -20,6 +20,7 @@ import static de.tudarmstadt.ukp.clarin.webanno.api.annotation.util.WebAnnoCasUtil.selectAnnotationByAddr; import java.io.IOException; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -233,6 +234,8 @@ public void render(CAS aCas, AnnotatorState aState, VDocument aVdoc, int aWindow aDepVID.getAttribute(), aDepVID.getSlot())); vspan.setVid(newVID); vspan.setColor(color); + // TODO: might be better to change after bugfix #1389 + vspan.setLazyDetails(Collections.emptyList()); newIdSpan.put(prevVID, vspan); // set user name as comment aVdoc.add(new VComment(newVID, VCommentType.INFO, username)); diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebarFactory.java b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebarFactory.java index d01448613f6..c4b81104ec3 100644 --- a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebarFactory.java +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebarFactory.java @@ -29,7 +29,7 @@ import de.tudarmstadt.ukp.clarin.webanno.ui.annotation.sidebar.AnnotationSidebarFactory_ImplBase; import de.tudarmstadt.ukp.clarin.webanno.ui.annotation.sidebar.AnnotationSidebar_ImplBase; -@Component +@Component("curationSidebar") public class CurationSidebarFactory extends AnnotationSidebarFactory_ImplBase { diff --git a/pom.xml b/pom.xml index d5b4a699b0d..614b8541b82 100644 --- a/pom.xml +++ b/pom.xml @@ -118,6 +118,7 @@ inception-doc + inception-curation From 56567e7c1d0687e065072c813d087329d6bf351d Mon Sep 17 00:00:00 2001 From: Richard Eckart de Castilho Date: Fri, 20 Sep 2019 08:06:37 +0200 Subject: [PATCH 026/453] #1054 - Dealing with many results in search sidebar - Better error handling - Formatting --- .../sidebar/SearchAnnotationSidebar.java | 11 ++--- .../search/sidebar/SearchResultsProvider.java | 44 ++++++++++--------- 2 files changed, 28 insertions(+), 27 deletions(-) diff --git a/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebar.java b/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebar.java index 0ae719d1b6d..cf405974db6 100644 --- a/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebar.java +++ b/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebar.java @@ -176,9 +176,7 @@ public SearchAnnotationSidebar(String aId, IModel aModel, // Add link for re-indexing the project searchOptionsForm.add(new LambdaAjaxLink("reindexProject", t -> { - Project project = ((IModel) t.getPage().getDefaultModel()).getObject() - .getProject(); - searchService.reindex(project); + searchService.reindex(getAnnotationPage().getModelObject().getProject()); })); resultsGroupContainer = new WebMarkupContainer("resultsGroupContainer"); @@ -196,8 +194,7 @@ protected void populateItem(Item item) item.add(new Label("groupTitle", LoadableDetachableModel .of(() -> result.getGroupKey() + " (" + result.getResults().size() + ")"))); item.add(createGroupLevelSelectionCheckBox("selectAllInGroup", - - result.getGroupKey())); + result.getGroupKey())); item.add(new SearchResultGroup("group", "resultGroup", SearchAnnotationSidebar.this, result.getGroupKey(), LambdaModel.of(() -> result))); } @@ -322,7 +319,8 @@ protected void onConfigure() return selectAllCheckBox; } - private void actionSearch(AjaxRequestTarget aTarget, Form aForm) { + private void actionSearch(AjaxRequestTarget aTarget, Form aForm) + { selectedResult = null; groupedSearchResults.detach(); searchResultGroups.setItemsPerPage(searchOptions.getObject().getItemsPerPage()); @@ -333,7 +331,6 @@ private void actionSearch(AjaxRequestTarget aTarget, Form aForm) { private void getSearchResultsGrouped() { - if (isBlank(targetQuery.getObject())) { resultsProvider.emptyQuery(); return; diff --git a/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchResultsProvider.java b/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchResultsProvider.java index 9c09f601ed7..5f7e9160912 100644 --- a/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchResultsProvider.java +++ b/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchResultsProvider.java @@ -17,6 +17,8 @@ */ package de.tudarmstadt.ukp.inception.app.ui.search.sidebar; +import static java.util.Collections.emptyIterator; + import java.io.IOException; import java.util.Collections; import java.util.Iterator; @@ -26,6 +28,8 @@ import org.apache.wicket.markup.repeater.data.IDataProvider; import org.apache.wicket.model.IModel; import org.apache.wicket.model.Model; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationFeature; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; @@ -36,10 +40,13 @@ import de.tudarmstadt.ukp.inception.search.ResultsGroup; import de.tudarmstadt.ukp.inception.search.SearchService; -public class SearchResultsProvider implements IDataProvider +public class SearchResultsProvider + implements IDataProvider { private static final long serialVersionUID = -4937781923274074722L; + private static final Logger LOG = LoggerFactory.getLogger(SearchResultsProvider.class); + private SearchService searchService; // Query settings @@ -64,7 +71,7 @@ public SearchResultsProvider(SearchService aSearchService, } @Override - public Iterator iterator(long first, long count) + public Iterator iterator(long first, long count) { if (query == null) { currentPageCache.setObject(Collections.emptyList()); @@ -86,13 +93,9 @@ public Iterator iterator(long first, long count) currentOffset = first; return queryResults.iterator(); } - catch (IOException e) { - e.printStackTrace(); - return null; - } - catch (ExecutionException e) { - e.printStackTrace(); - return null; + catch (IOException | ExecutionException e) { + LOG.error("Unable to retrieve results", e); + return emptyIterator(); } } else { @@ -111,7 +114,7 @@ public long size() return totalResults; } catch (ExecutionException e) { - e.printStackTrace(); + LOG.error("Unable to retrieve results", e); return 0; } } @@ -121,19 +124,20 @@ public long size() } @Override - public IModel model(Object object) + public IModel model(ResultsGroup object) { - return new Model((ResultsGroup) object); + return Model.of(object); } /** - * Sets the query parameters in the SearchResultsProvider. - * Calling the {@link #iterator(long, long)} method of the SearchResultsProvider will then - * execute the query. + * Sets the query parameters in the SearchResultsProvider. Calling the + * {@link #iterator(long, long)} method of the SearchResultsProvider will then execute the + * query. */ public void initializeQuery(User aUser, Project aProject, String aQuery, - SourceDocument aDocument, AnnotationLayer aAnnotationLayer, - AnnotationFeature aAnnotationFeature) { + SourceDocument aDocument, AnnotationLayer aAnnotationLayer, + AnnotationFeature aAnnotationFeature) + { user = aUser; project = aProject; query = aQuery; @@ -141,12 +145,12 @@ public void initializeQuery(User aUser, Project aProject, String aQuery, annotationLayer = aAnnotationLayer; annotationFeature = aAnnotationFeature; - totalResults = -1; // reset size cache + totalResults = -1; // reset size cache currentPageCache.setObject(null); // reset page cache - } - public void emptyQuery() { + public void emptyQuery() + { query = null; totalResults = 0; } From 075cf7ee487052f57160e0163d69ceb4022597dc Mon Sep 17 00:00:00 2001 From: uwinch Date: Mon, 23 Sep 2019 08:17:24 +0200 Subject: [PATCH 027/453] #1256 Integrate curation into annotation page - toggle settings - ui for merge strategies --- .../inception/curation/CurationService.java | 13 +- .../curation/CurationServiceImpl.java | 31 +++- .../merge/AutomaticMergeStrategy.java | 35 +++++ .../curation/merge/ManualMergeStrategy.java | 32 ++++ .../curation/merge/MergeStrategy.java | 15 ++ .../curation/sidebar/CurationSidebar.html | 29 +++- .../curation/sidebar/CurationSidebar.java | 141 ++++++++++++++++-- .../sidebar/CurationSidebar.properties | 3 +- 8 files changed, 269 insertions(+), 30 deletions(-) create mode 100644 inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/AutomaticMergeStrategy.java create mode 100644 inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/ManualMergeStrategy.java create mode 100644 inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/MergeStrategy.java diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationService.java b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationService.java index a4aed6b4edc..54da1303d1b 100644 --- a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationService.java +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationService.java @@ -20,6 +20,7 @@ import java.io.IOException; import java.util.Collection; import java.util.List; +import java.util.Map; import java.util.Optional; import org.apache.uima.cas.CAS; @@ -35,7 +36,7 @@ public interface CurationService public Optional> listUsersSelectedForCuration(String aCurrentUser, long aProjectId); /** - * retrieves CAS associated with curation doc for the given user + * Retrieves CAS associated with curation doc for the given user */ public Optional retrieveCurationCAS(String aUser, long aProjectId, SourceDocument aDoc) throws IOException; @@ -53,9 +54,17 @@ public void updateCurationName(String aCurrentUser, long aProjectId, String aCurationName); /** - * Removed stored curation information after user session has ended + * Remove stored curation information on given user */ public void removeCurrentUserInformation(String aCurrentUser, long aProjectId); + /** + * Remove information on users that were selected to be shown for curation by the given user + */ public void clearUsersSelectedForCuration(String aUsername, Long aId); + + /** + * Retrieve cases for the given document for the given users + */ + public Map retrieveUserCases(Collection aUsers, SourceDocument aDoc); } diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationServiceImpl.java b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationServiceImpl.java index 939ebd92f83..6391ef44700 100644 --- a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationServiceImpl.java +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationServiceImpl.java @@ -20,7 +20,9 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Collection; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -28,6 +30,8 @@ import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; import org.apache.uima.cas.CAS; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.event.EventListener; import org.springframework.security.core.session.SessionDestroyedEvent; @@ -45,6 +49,8 @@ @Component public class CurationServiceImpl implements CurationService { + private Logger log = LoggerFactory.getLogger(getClass()); + // stores info on which users are selected and which doc is the curation-doc private ConcurrentMap curationStates; @@ -180,7 +186,6 @@ public void removeCurrentUserInformation(String aCurrentUser, long aProjectId) public void onSessionDestroyed(SessionDestroyedEvent event) { SessionInformation info = sessionRegistry.getSessionInformation(event.getId()); - // Could be an anonymous session without information. if (info != null) { String username = (String) info.getPrincipal(); clearState(username); @@ -192,11 +197,6 @@ private void clearState(String aUsername) projectService.listAccessibleProjects(userRegistry.get(aUsername)).stream() .map(Project::getId) .forEach(pId -> removeCurrentUserInformation(aUsername, pId)); -// User currentUser = userRegistry.get(aUsername); -// List projects = projectService.listAccessibleProjects(currentUser); -// for (Project project : projects) { -// removeCurrentUserInformation(aUsername, project.getId()); -// } } @Override @@ -209,4 +209,23 @@ public void clearUsersSelectedForCuration(String aUsername, Long aProjectId) } + @Override + public Map retrieveUserCases(Collection aUsers, SourceDocument aDoc) + { + Map casses = new HashMap<>(); + for (User user : aUsers) { + try { + String username = user.getUsername(); + casses.put(username, documentService.readAnnotationCas(aDoc, + username)); + } + catch (IOException e) { + log.warn(String.format("Could not retrieve CAS for user %s and document %d", + user.getUsername(), aDoc.getId())); + e.printStackTrace(); + } + } + return casses; + } + } diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/AutomaticMergeStrategy.java b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/AutomaticMergeStrategy.java new file mode 100644 index 00000000000..e7fdb1a85f1 --- /dev/null +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/AutomaticMergeStrategy.java @@ -0,0 +1,35 @@ +package de.tudarmstadt.ukp.inception.curation.merge; + +import java.util.Map; + +import org.apache.uima.cas.CAS; +import org.springframework.stereotype.Component; + +import de.tudarmstadt.ukp.clarin.webanno.api.annotation.model.AnnotatorState; + +@Component +public class AutomaticMergeStrategy + implements MergeStrategy +{ + + private String uiName = "Automatic"; + + @Override + public void merge(AnnotatorState aState, CAS aTargetCas, Map aUserCases) + { + // TODO Auto-generated method stub + + // write back + // update timestamp + } + + public String getUiName() + { + return uiName; + } + + public void setUiName(String aUiName) + { + uiName = aUiName; + } +} diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/ManualMergeStrategy.java b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/ManualMergeStrategy.java new file mode 100644 index 00000000000..fdc54af7f9b --- /dev/null +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/ManualMergeStrategy.java @@ -0,0 +1,32 @@ +package de.tudarmstadt.ukp.inception.curation.merge; + +import java.util.Map; + +import org.apache.uima.cas.CAS; +import org.springframework.stereotype.Component; + +import de.tudarmstadt.ukp.clarin.webanno.api.annotation.model.AnnotatorState; + +@Component +public class ManualMergeStrategy implements MergeStrategy +{ + + private String uiName = "Manual"; + + @Override + public void merge(AnnotatorState aState, CAS aCas, Map aUserCases) + { + // Do nothing + } + + public String getUiName() + { + return uiName; + } + + public void setUiName(String aUiName) + { + uiName = aUiName; + } + +} diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/MergeStrategy.java b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/MergeStrategy.java new file mode 100644 index 00000000000..db0353d5e2b --- /dev/null +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/MergeStrategy.java @@ -0,0 +1,15 @@ +package de.tudarmstadt.ukp.inception.curation.merge; + +import java.util.Map; + +import org.apache.uima.cas.CAS; + +import de.tudarmstadt.ukp.clarin.webanno.api.annotation.model.AnnotatorState; + +public interface MergeStrategy +{ + + void merge(AnnotatorState aState, CAS aCas, Map aUserCases); + + String getUiName(); +} diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.html b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.html index 9c9f6bd14f3..665a08c2126 100644 --- a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.html +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.html @@ -41,16 +41,35 @@

+ +

-
- - -
+
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java index c1a70efbf11..cabe4f73ca3 100644 --- a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java @@ -20,18 +20,24 @@ import static de.tudarmstadt.ukp.clarin.webanno.api.WebAnnoConst.CURATION_USER; import static de.tudarmstadt.ukp.clarin.webanno.support.lambda.LambdaBehavior.visibleWhen; +import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; +import org.apache.uima.cas.CAS; import org.apache.wicket.ajax.AjaxRequestTarget; +import org.apache.wicket.ajax.markup.html.form.AjaxButton; import org.apache.wicket.markup.html.WebMarkupContainer; import org.apache.wicket.markup.html.basic.Label; import org.apache.wicket.markup.html.form.Check; import org.apache.wicket.markup.html.form.CheckGroup; +import org.apache.wicket.markup.html.form.ChoiceRenderer; +import org.apache.wicket.markup.html.form.DropDownChoice; import org.apache.wicket.markup.html.form.Form; import org.apache.wicket.markup.html.form.RadioChoice; import org.apache.wicket.markup.html.list.ListItem; @@ -39,7 +45,10 @@ import org.apache.wicket.model.IModel; import org.apache.wicket.model.LoadableDetachableModel; import org.apache.wicket.model.PropertyModel; +import org.apache.wicket.model.util.ListModel; import org.apache.wicket.spring.injection.annot.SpringBean; +import org.apache.wicket.util.value.AttributeMap; +import org.apache.wicket.util.value.IValueMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -59,6 +68,8 @@ import de.tudarmstadt.ukp.clarin.webanno.ui.annotation.AnnotationPage; import de.tudarmstadt.ukp.clarin.webanno.ui.annotation.sidebar.AnnotationSidebar_ImplBase; import de.tudarmstadt.ukp.inception.curation.CurationService; +import de.tudarmstadt.ukp.inception.curation.merge.ManualMergeStrategy; +import de.tudarmstadt.ukp.inception.curation.merge.MergeStrategy; public class CurationSidebar @@ -75,25 +86,33 @@ public class CurationSidebar private @SpringBean CurationService curationService; private @SpringBean DocumentService documentService; + private @SpringBean List mergeStrategies; + private @SpringBean ManualMergeStrategy defaultMergeStrategy; + private MergeStrategy selectedMergeStrategy; + private CheckGroup selectedUsers; private Form> usersForm; + private Form settingsForm ; + private WebMarkupContainer mainContainer; + private DropDownChoice mergeChoice; private final List curationTargets = Arrays - .asList(new String[] { DEFAULT_CURATION_TARGET, " curation document" }); + .asList(new String[] { " curation document", DEFAULT_CURATION_TARGET}); private String selectedCurationTarget = DEFAULT_CURATION_TARGET; private AnnotatorState state; private AnnotationPage annoPage; - + // TODO: only show to people who are curators public CurationSidebar(String aId, IModel aModel, AnnotationActionHandler aActionHandler, CasProvider aCasProvider, AnnotationPage aAnnotationPage) { super(aId, aModel, aActionHandler, aCasProvider, aAnnotationPage); + selectedMergeStrategy = defaultMergeStrategy; state = aModel.getObject(); annoPage = aAnnotationPage; - WebMarkupContainer mainContainer = new WebMarkupContainer("mainContainer"); + mainContainer = new WebMarkupContainer("mainContainer"); mainContainer.setOutputMarkupId(true); add(mainContainer); @@ -102,18 +121,68 @@ public CurationSidebar(String aId, IModel aModel, usersForm.setOutputMarkupId(true); mainContainer.add(usersForm); - // set up curation target radio button - Form targetForm = new Form("settingsForm"); - LambdaAjaxButton applyButton = new LambdaAjaxButton<>("apply", this::updateCurator); - targetForm.add(applyButton); - RadioChoice curationTargetBtn = new RadioChoice("curationTargetRadioBtn", - new PropertyModel(this, "selectedCurationTarget"), curationTargets); - curationTargetBtn.setPrefix("
"); - targetForm.add(curationTargetBtn); - mainContainer.add(targetForm); + // set up settings form for curation target, merge op selection + Form settingsForm = createSettingsForm("settingsForm"); + settingsForm.setOutputMarkupId(true); + settingsForm.setVisible(false); + mainContainer.add(settingsForm); } - + private Form createSettingsForm(String aId) + { + settingsForm = new Form(aId); + LambdaAjaxButton applyBtn = new LambdaAjaxButton<>("apply", this::merge); + settingsForm.add(applyBtn); + + // set up selection for merge strategy + mergeChoice = createMergeDropDownChoice("mergeChoice"); + settingsForm.add(mergeChoice); + + // set up curation target selection as radio button + RadioChoice curationTargetChoice = new RadioChoice("curationTargetRadioBtn", + new PropertyModel(this, "selectedCurationTarget"), curationTargets) + { + private static final long serialVersionUID = 1513847274470368949L; + + @Override + protected IValueMap getAdditionalAttributesForLabel(int aIndex, String aChoice) + { + // use normal font for choices + IValueMap attrValMap = super.getAdditionalAttributesForLabel(aIndex, aChoice); + if (attrValMap == null) { + attrValMap = new AttributeMap(); + } + attrValMap.put("style", "font-weight:normal"); + return attrValMap; + } + }; + curationTargetChoice.setPrefix("
"); + settingsForm.add(curationTargetChoice); + + // toggle visibility of settings form + usersForm.add(new AjaxButton("toggleOptionsVisibility") { + + private static final long serialVersionUID = -5535838955781542216L; + + @Override + protected void onSubmit(AjaxRequestTarget aTarget) + { + settingsForm.setVisible(!settingsForm.isVisible()); + aTarget.add(mainContainer); + } + }); + return settingsForm; + } + + private DropDownChoice createMergeDropDownChoice(String aId) + { + DropDownChoice mergeChoice = new DropDownChoice(aId, + new PropertyModel(this, "selectedMergeStrategy"), + new ListModel(mergeStrategies), + new ChoiceRenderer("uiName")); + return mergeChoice; + } + @Override protected void onConfigure() { @@ -122,8 +191,7 @@ protected void onConfigure() .isAnnotationFinished(state.getDocument(), state.getUser())); } - - private void updateCurator(AjaxRequestTarget aTarget, Form aForm) + private void merge(AjaxRequestTarget aTarget, Form aForm) { long project = state.getProject().getId(); User curator = userRepository.getCurrentUser(); @@ -139,7 +207,27 @@ private void updateCurator(AjaxRequestTarget aTarget, Form aForm) state.setUser(curator); state.getSelection().clear(); - updateUsers(aTarget, aForm); + // update selected users + Collection users = selectedUsers.getModelObject(); + curationService.updateUsersSelectedForCuration( + state.getUser().getUsername(), state.getProject().getId(), users); + // merge cases + try { + SourceDocument doc = state.getDocument(); + Map userCases = curationService.retrieveUserCases(users, doc); + Optional targetCas = curationService.retrieveCurationCAS(curator.getUsername(), + state.getProject().getId(), doc); + if (targetCas.isPresent()) { + MergeStrategy mergeStrat = ((MergeStrategy) mergeChoice.getDefaultModelObject()); + mergeStrat.merge(state, targetCas.get(), userCases); + log.info("{} merge done", mergeStrat.getUiName()); //TODO change to debug + } + } + catch (IOException e) { + log.error(String.format("Could not retrieve CAS for user %s and project %d", + curator.getUsername(), state.getProject().getId())); + e.printStackTrace(); + } //open curation doc annoPage.actionLoadDocument(aTarget); } @@ -225,4 +313,25 @@ private void clearUsers(AjaxRequestTarget aTarget, Form aForm) { annoPage.actionRefreshDocument(aTarget); } + //getters, setters needed for property-models + public MergeStrategy getSelectedMergeStrategy() + { + return selectedMergeStrategy; + } + + public void setSelectedMergeStrategy(MergeStrategy aSelectedMergeStrategy) + { + selectedMergeStrategy = aSelectedMergeStrategy; + } + + public String getSelectedCurationTarget() + { + return selectedCurationTarget; + } + + public void setSelectedCurationTarget(String aSelectedCurationTarget) + { + selectedCurationTarget = aSelectedCurationTarget; + } + } diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.properties b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.properties index 41d9220085b..0344de09ab8 100644 --- a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.properties +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.properties @@ -20,4 +20,5 @@ show=Show apply=Apply clear=Clear curationHeader=Curation -curationTarget=Save as \ No newline at end of file +curationTarget=Save as +mergeChoice=Merge \ No newline at end of file From 2e38d4d179476528f67bd2f8be475280d7647d21 Mon Sep 17 00:00:00 2001 From: uwinch Date: Mon, 23 Sep 2019 18:01:47 +0200 Subject: [PATCH 028/453] #1256 Integrate curation into annotation page - fixed selection, so its saved when closing sidebar --- .../inception/curation/CurationService.java | 17 ++++ .../curation/CurationServiceImpl.java | 39 ++++++++ .../merge/AutomaticMergeStrategy.java | 4 +- .../curation/merge/ManualMergeStrategy.java | 4 +- .../curation/merge/MergeStrategy.java | 2 +- .../curation/sidebar/CurationSidebar.java | 92 ++++++++++--------- 6 files changed, 108 insertions(+), 50 deletions(-) diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationService.java b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationService.java index 54da1303d1b..1861aac1c23 100644 --- a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationService.java +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationService.java @@ -27,6 +27,7 @@ import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.clarin.webanno.security.model.User; +import de.tudarmstadt.ukp.inception.curation.merge.MergeStrategy; public interface CurationService { @@ -67,4 +68,20 @@ public void updateCurationName(String aCurrentUser, long aProjectId, * Retrieve cases for the given document for the given users */ public Map retrieveUserCases(Collection aUsers, SourceDocument aDoc); + + /** + * Returns the user corresponding to the CAS used as curation (target) CAS + */ + public String retrieveCurationTarget(String aUser, long aProjectId); + + /** + * Returns the merge strategy that the user previously selected or the manual one as default + */ + public MergeStrategy retrieveMergeStrategy(String aUsername, + long aProjectId); + + /** + * Store the selected merge-strategy for the given user and project + */ + void updateMergeStrategy(String aCurrentUser, long aProjectId, MergeStrategy aStrategy); } diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationServiceImpl.java b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationServiceImpl.java index 6391ef44700..9bbbe23d859 100644 --- a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationServiceImpl.java +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationServiceImpl.java @@ -45,6 +45,7 @@ import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.clarin.webanno.security.UserDao; import de.tudarmstadt.ukp.clarin.webanno.security.model.User; +import de.tudarmstadt.ukp.inception.curation.merge.MergeStrategy; @Component public class CurationServiceImpl implements CurationService @@ -108,6 +109,7 @@ private class CurationState // to find source document of the curated document // the curationdoc can be retrieved from user (CURATION or current) and projectId private String curationUser; + private MergeStrategy selectedStrategy; public CurationState(String aUser) { @@ -133,6 +135,16 @@ public void setCurationName(String aCurationName) { curationUser = aCurationName; } + + public void setMergeStrategy(MergeStrategy aStrategy) + { + selectedStrategy = aStrategy; + } + + public MergeStrategy getMergeStrategy() + { + return selectedStrategy; + } } @Override @@ -172,6 +184,15 @@ public void updateCurationName(String aCurrentUser, long aProjectId, String aUse getCurationState(aCurrentUser, aProjectId).setCurationName(aUserName);; } } + + @Override + public void updateMergeStrategy(String aCurrentUser, long aProjectId, MergeStrategy aStrategy) + { + synchronized (curationStates) + { + getCurationState(aCurrentUser, aProjectId).setMergeStrategy(aStrategy);; + } + } @Override public void removeCurrentUserInformation(String aCurrentUser, long aProjectId) @@ -228,4 +249,22 @@ public Map retrieveUserCases(Collection aUsers, SourceDocumen return casses; } + @Override + public String retrieveCurationTarget(String aUser, long aProjectId) + { + String curationUser = getCurationState(aUser, aProjectId).getCurationName(); + if (curationUser == null) { + // default to user as curation target + return aUser; + } + return curationUser; + } + + @Override + public MergeStrategy retrieveMergeStrategy(String aUsername, + long aProjectId) + { + return getCurationState(aUsername, aProjectId).getMergeStrategy(); + } + } diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/AutomaticMergeStrategy.java b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/AutomaticMergeStrategy.java index e7fdb1a85f1..c7dc5d1ecca 100644 --- a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/AutomaticMergeStrategy.java +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/AutomaticMergeStrategy.java @@ -7,11 +7,10 @@ import de.tudarmstadt.ukp.clarin.webanno.api.annotation.model.AnnotatorState; -@Component +@Component("automaticStartegy") public class AutomaticMergeStrategy implements MergeStrategy { - private String uiName = "Automatic"; @Override @@ -23,6 +22,7 @@ public void merge(AnnotatorState aState, CAS aTargetCas, Map aUserC // update timestamp } + @Override public String getUiName() { return uiName; diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/ManualMergeStrategy.java b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/ManualMergeStrategy.java index fdc54af7f9b..2b98a9fee90 100644 --- a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/ManualMergeStrategy.java +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/ManualMergeStrategy.java @@ -7,10 +7,9 @@ import de.tudarmstadt.ukp.clarin.webanno.api.annotation.model.AnnotatorState; -@Component +@Component("manualStrategy") public class ManualMergeStrategy implements MergeStrategy { - private String uiName = "Manual"; @Override @@ -19,6 +18,7 @@ public void merge(AnnotatorState aState, CAS aCas, Map aUserCases) // Do nothing } + @Override public String getUiName() { return uiName; diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/MergeStrategy.java b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/MergeStrategy.java index db0353d5e2b..da3494fb721 100644 --- a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/MergeStrategy.java +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/MergeStrategy.java @@ -6,7 +6,7 @@ import de.tudarmstadt.ukp.clarin.webanno.api.annotation.model.AnnotatorState; -public interface MergeStrategy +public interface MergeStrategy { void merge(AnnotatorState aState, CAS aCas, Map aUserCases); diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java index cabe4f73ca3..c6524b1645f 100644 --- a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java @@ -44,7 +44,7 @@ import org.apache.wicket.markup.html.list.ListView; import org.apache.wicket.model.IModel; import org.apache.wicket.model.LoadableDetachableModel; -import org.apache.wicket.model.PropertyModel; +import org.apache.wicket.model.Model; import org.apache.wicket.model.util.ListModel; import org.apache.wicket.spring.injection.annot.SpringBean; import org.apache.wicket.util.value.AttributeMap; @@ -68,7 +68,6 @@ import de.tudarmstadt.ukp.clarin.webanno.ui.annotation.AnnotationPage; import de.tudarmstadt.ukp.clarin.webanno.ui.annotation.sidebar.AnnotationSidebar_ImplBase; import de.tudarmstadt.ukp.inception.curation.CurationService; -import de.tudarmstadt.ukp.inception.curation.merge.ManualMergeStrategy; import de.tudarmstadt.ukp.inception.curation.merge.MergeStrategy; @@ -76,7 +75,6 @@ public class CurationSidebar extends AnnotationSidebar_ImplBase { private static final long serialVersionUID = -4195790451286055737L; - private static final String DEFAULT_CURATION_TARGET = " my document"; private Logger log = LoggerFactory.getLogger(getClass()); @@ -87,20 +85,13 @@ public class CurationSidebar private @SpringBean DocumentService documentService; private @SpringBean List mergeStrategies; - private @SpringBean ManualMergeStrategy defaultMergeStrategy; - private MergeStrategy selectedMergeStrategy; private CheckGroup selectedUsers; private Form> usersForm; - private Form settingsForm ; + private RadioChoice curationTargetChoice; private WebMarkupContainer mainContainer; private DropDownChoice mergeChoice; - - private final List curationTargets = Arrays - .asList(new String[] { " curation document", DEFAULT_CURATION_TARGET}); - private String selectedCurationTarget = DEFAULT_CURATION_TARGET; - private AnnotatorState state; private AnnotationPage annoPage; // TODO: only show to people who are curators @@ -109,8 +100,6 @@ public CurationSidebar(String aId, IModel aModel, AnnotationPage aAnnotationPage) { super(aId, aModel, aActionHandler, aCasProvider, aAnnotationPage); - selectedMergeStrategy = defaultMergeStrategy; - state = aModel.getObject(); annoPage = aAnnotationPage; mainContainer = new WebMarkupContainer("mainContainer"); mainContainer.setOutputMarkupId(true); @@ -130,7 +119,7 @@ public CurationSidebar(String aId, IModel aModel, private Form createSettingsForm(String aId) { - settingsForm = new Form(aId); + Form settingsForm = new Form(aId); LambdaAjaxButton applyBtn = new LambdaAjaxButton<>("apply", this::merge); settingsForm.add(applyBtn); @@ -139,8 +128,28 @@ private Form createSettingsForm(String aId) settingsForm.add(mergeChoice); // set up curation target selection as radio button - RadioChoice curationTargetChoice = new RadioChoice("curationTargetRadioBtn", - new PropertyModel(this, "selectedCurationTarget"), curationTargets) + List curationTargets = Arrays.asList( + new String[] { CURATION_USER, userRepository.getCurrentUser().getUsername() }); + ChoiceRenderer choiceRenderer = new ChoiceRenderer() + { + private static final long serialVersionUID = -8165699251116827372L; + + @Override + public Object getDisplayValue(String aUsername) + { + if (aUsername.equals(CURATION_USER)) { + return " curation document"; + } + else { + return " my document"; + } + } + }; + curationTargetChoice = new RadioChoice("curationTargetRadioBtn", + Model.of(curationService.retrieveCurationTarget( + userRepository.getCurrentUser().getUsername(), + getModelObject().getProject().getId())), + curationTargets, choiceRenderer) { private static final long serialVersionUID = 1513847274470368949L; @@ -177,7 +186,9 @@ protected void onSubmit(AjaxRequestTarget aTarget) private DropDownChoice createMergeDropDownChoice(String aId) { DropDownChoice mergeChoice = new DropDownChoice(aId, - new PropertyModel(this, "selectedMergeStrategy"), + LoadableDetachableModel.of(() -> curationService.retrieveMergeStrategy( + userRepository.getCurrentUser().getUsername(), + getModelObject().getProject().getId())), new ListModel(mergeStrategies), new ChoiceRenderer("uiName")); return mergeChoice; @@ -187,16 +198,19 @@ private DropDownChoice createMergeDropDownChoice(String aId) protected void onConfigure() { super.onConfigure(); + AnnotatorState state = getModelObject(); setEnabled(!documentService .isAnnotationFinished(state.getDocument(), state.getUser())); } private void merge(AjaxRequestTarget aTarget, Form aForm) { + AnnotatorState state = getModelObject(); long project = state.getProject().getId(); User curator = userRepository.getCurrentUser(); String currentUsername = curator.getUsername(); - if (selectedCurationTarget.equals(DEFAULT_CURATION_TARGET)) { + if (curationTargetChoice.getModelObject() + .equals(currentUsername)) { curationService.updateCurationName(currentUsername, project, currentUsername); } @@ -208,9 +222,14 @@ private void merge(AjaxRequestTarget aTarget, Form aForm) state.setUser(curator); state.getSelection().clear(); // update selected users + long projectId = state.getProject().getId(); + currentUsername = state.getUser().getUsername(); Collection users = selectedUsers.getModelObject(); curationService.updateUsersSelectedForCuration( - state.getUser().getUsername(), state.getProject().getId(), users); + currentUsername, projectId, users); + // update selected merge strategy + MergeStrategy mergeStrat = ((MergeStrategy) mergeChoice.getDefaultModelObject()); + curationService.updateMergeStrategy(currentUsername, projectId, mergeStrat); // merge cases try { SourceDocument doc = state.getDocument(); @@ -218,7 +237,6 @@ private void merge(AjaxRequestTarget aTarget, Form aForm) Optional targetCas = curationService.retrieveCurationCAS(curator.getUsername(), state.getProject().getId(), doc); if (targetCas.isPresent()) { - MergeStrategy mergeStrat = ((MergeStrategy) mergeChoice.getDefaultModelObject()); mergeStrat.merge(state, targetCas.get(), userCases); log.info("{} merge done", mergeStrat.getUiName()); //TODO change to debug } @@ -263,7 +281,8 @@ protected void populateItem(ListItem aItem) private List listSelectedUsers() { Optional> users = curationService.listUsersSelectedForCuration( - userRepository.getCurrentUser().getUsername(), state.getProject().getId()); + userRepository.getCurrentUser().getUsername(), getModelObject().getProject() + .getId()); if (!users.isPresent()) { return new ArrayList<>(); } @@ -276,7 +295,8 @@ private List listSelectedUsers() private List listUsers() { return projectService - .listProjectUsersWithPermissions(state.getProject(), PermissionLevel.ANNOTATOR) + .listProjectUsersWithPermissions(getModelObject().getProject(), + PermissionLevel.ANNOTATOR) .stream().filter(user -> !user.equals(userRepository.getCurrentUser()) && hasFinishedDoc(user)) .collect(Collectors.toList()); @@ -284,7 +304,7 @@ && hasFinishedDoc(user)) private boolean hasFinishedDoc(User aUser) { - SourceDocument doc = state.getDocument(); + SourceDocument doc = getModelObject().getDocument(); String username = aUser.getUsername(); if (documentService.existsAnnotationDocument(doc, username) && documentService.getAnnotationDocument(doc, username).getState() @@ -298,6 +318,7 @@ private boolean hasFinishedDoc(User aUser) private void updateUsers(AjaxRequestTarget aTarget, Form aForm) { + AnnotatorState state = getModelObject(); Collection users = selectedUsers.getModelObject(); curationService.updateUsersSelectedForCuration( state.getUser().getUsername(), state.getProject().getId(), users); @@ -305,7 +326,9 @@ private void updateUsers(AjaxRequestTarget aTarget, Form aForm) annoPage.actionRefreshDocument(aTarget); } - private void clearUsers(AjaxRequestTarget aTarget, Form aForm) { + private void clearUsers(AjaxRequestTarget aTarget, Form aForm) + { + AnnotatorState state = getModelObject(); selectedUsers.setModelObject(new ArrayList<>()); curationService.clearUsersSelectedForCuration( state.getUser().getUsername(), state.getProject().getId()); @@ -313,25 +336,4 @@ private void clearUsers(AjaxRequestTarget aTarget, Form aForm) { annoPage.actionRefreshDocument(aTarget); } - //getters, setters needed for property-models - public MergeStrategy getSelectedMergeStrategy() - { - return selectedMergeStrategy; - } - - public void setSelectedMergeStrategy(MergeStrategy aSelectedMergeStrategy) - { - selectedMergeStrategy = aSelectedMergeStrategy; - } - - public String getSelectedCurationTarget() - { - return selectedCurationTarget; - } - - public void setSelectedCurationTarget(String aSelectedCurationTarget) - { - selectedCurationTarget = aSelectedCurationTarget; - } - } From 51120953e1ba53da4a253a60aa7e1e904ecedd38 Mon Sep 17 00:00:00 2001 From: uwinch Date: Tue, 24 Sep 2019 14:21:25 +0200 Subject: [PATCH 029/453] #1256 Integrate curation into annotation page - automatic merge --- .../inception/curation/CurationService.java | 6 +++ .../curation/CurationServiceImpl.java | 32 +++++++++++++ .../merge/AutomaticMergeStrategy.java | 45 ++++++++++++++++--- .../curation/sidebar/CurationSidebar.java | 1 + 4 files changed, 79 insertions(+), 5 deletions(-) diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationService.java b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationService.java index 1861aac1c23..3434a525689 100644 --- a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationService.java +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationService.java @@ -25,6 +25,7 @@ import org.apache.uima.cas.CAS; +import de.tudarmstadt.ukp.clarin.webanno.api.annotation.model.AnnotatorState; import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.clarin.webanno.security.model.User; import de.tudarmstadt.ukp.inception.curation.merge.MergeStrategy; @@ -42,6 +43,11 @@ public interface CurationService public Optional retrieveCurationCAS(String aUser, long aProjectId, SourceDocument aDoc) throws IOException; + /** + * Write to CAS associated with curation doc for the given user and update timestamp + */ + public void writeCurationCas(CAS aTargetCas, AnnotatorState aState, long aProjectId); + /** * Store the users that were selected to be shown for curation by the given user */ diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationServiceImpl.java b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationServiceImpl.java index 9bbbe23d859..9fd97378d18 100644 --- a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationServiceImpl.java +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationServiceImpl.java @@ -17,6 +17,8 @@ */ package de.tudarmstadt.ukp.inception.curation; +import static de.tudarmstadt.ukp.clarin.webanno.api.WebAnnoConst.CURATION_USER; + import java.io.IOException; import java.util.ArrayList; import java.util.Collection; @@ -38,9 +40,13 @@ import org.springframework.security.core.session.SessionInformation; import org.springframework.security.core.session.SessionRegistry; import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; +import de.tudarmstadt.ukp.clarin.webanno.api.CasStorageService; import de.tudarmstadt.ukp.clarin.webanno.api.DocumentService; import de.tudarmstadt.ukp.clarin.webanno.api.ProjectService; +import de.tudarmstadt.ukp.clarin.webanno.api.annotation.model.AnnotatorState; +import de.tudarmstadt.ukp.clarin.webanno.api.annotation.model.AnnotatorStateUtils; import de.tudarmstadt.ukp.clarin.webanno.model.Project; import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.clarin.webanno.security.UserDao; @@ -59,6 +65,7 @@ public class CurationServiceImpl implements CurationService private @Autowired SessionRegistry sessionRegistry; private @Autowired ProjectService projectService; private @Autowired UserDao userRegistry; + private @Autowired CasStorageService casStorageService; public CurationServiceImpl() { @@ -165,6 +172,31 @@ public Optional retrieveCurationCAS(String aUser, long aProjectId, SourceDo return Optional.of(documentService .readAnnotationCas(aDoc, curationUser)); } + + @Override + @Transactional + public void writeCurationCas(CAS aTargetCas, AnnotatorState aState, long aProjectId) { + SourceDocument doc = aState.getDocument(); + String curatorName = getCurationState(aState.getUser().getUsername(), aProjectId) + .getCurationName(); + try { + User curator; + if (curatorName.equals(CURATION_USER)) { + curator = new User(CURATION_USER); + } + else { + curator = userRegistry.get(curatorName); + } + documentService.writeAnnotationCas(aTargetCas, doc, curator, true); + AnnotatorStateUtils.updateDocumentTimestampAfterWrite(aState, + casStorageService.getCasTimestamp(doc, curatorName)); + } + catch (IOException e) { + log.warn(String.format("Could not write CAS for user %s and document %d", + curatorName, doc.getId())); + e.printStackTrace(); + } + } @Override public void updateUsersSelectedForCuration(String aCurrentUser, long aProjectId, diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/AutomaticMergeStrategy.java b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/AutomaticMergeStrategy.java index c7dc5d1ecca..4d4e250b4be 100644 --- a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/AutomaticMergeStrategy.java +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/AutomaticMergeStrategy.java @@ -1,25 +1,60 @@ package de.tudarmstadt.ukp.inception.curation.merge; +import static de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.LinkCompareBehavior.LINK_ROLE_AS_LABEL; + +import java.util.List; import java.util.Map; +import org.apache.uima.UIMAException; import org.apache.uima.cas.CAS; +import org.apache.uima.cas.Type; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +import de.tudarmstadt.ukp.clarin.webanno.api.AnnotationSchemaService; +import de.tudarmstadt.ukp.clarin.webanno.api.annotation.exception.AnnotationException; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.model.AnnotatorState; +import de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.CasDiff; +import de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.CasDiff.DiffResult; +import de.tudarmstadt.ukp.clarin.webanno.curation.casmerge.CasMerge; +import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; +import de.tudarmstadt.ukp.clarin.webanno.ui.curation.component.model.SuggestionBuilder; +import de.tudarmstadt.ukp.inception.curation.CurationService; @Component("automaticStartegy") public class AutomaticMergeStrategy implements MergeStrategy { private String uiName = "Automatic"; + + private Logger log = LoggerFactory.getLogger(getClass()); + + private @Autowired CurationService curationService; + private @Autowired AnnotationSchemaService annotationService; @Override - public void merge(AnnotatorState aState, CAS aTargetCas, Map aUserCases) + public void merge(AnnotatorState aState, CAS aTargetCas, Map aUserCasses) { - // TODO Auto-generated method stub - - // write back - // update timestamp + // prepare merged cas + List layers = aState.getAnnotationLayers(); + List entryTypes = SuggestionBuilder.getEntryTypes(aTargetCas, layers, + annotationService); + DiffResult diff = CasDiff.doDiffSingle(annotationService, aState.getProject(), entryTypes, + LINK_ROLE_AS_LABEL, aUserCasses, 0, aTargetCas.getDocumentText().length()); + CasMerge casMerge = new CasMerge(annotationService); + try { + casMerge.reMergeCas(diff, aState.getDocument(), aState.getUser().getUsername(), + aTargetCas, aUserCasses); + } + catch (AnnotationException | UIMAException e) { + log.warn(String.format("Could not merge CAS for user %s and document %d", + aState.getUser().getUsername(), aState.getDocument().getId())); + e.printStackTrace(); + } + // write back and update timestamp + curationService.writeCurationCas(aTargetCas, aState, aState.getProject().getId()); } @Override diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java index c6524b1645f..34fbd5d4a26 100644 --- a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java @@ -209,6 +209,7 @@ private void merge(AjaxRequestTarget aTarget, Form aForm) long project = state.getProject().getId(); User curator = userRepository.getCurrentUser(); String currentUsername = curator.getUsername(); + // update curation target if (curationTargetChoice.getModelObject() .equals(currentUsername)) { curationService.updateCurationName(currentUsername, From f5b2fc2841060d24b0ca33aa8990b1e5de5b9406 Mon Sep 17 00:00:00 2001 From: uwinch Date: Tue, 24 Sep 2019 16:32:07 +0200 Subject: [PATCH 030/453] #1256 Integrate curation into annotation page - dependency issues - documentation --- inception-app-webapp/pom.xml | 1 - inception-curation/pom.xml | 8 ++++++++ .../curation/merge/AutomaticMergeStrategy.java | 17 +++++++++++++++++ .../curation/merge/ManualMergeStrategy.java | 17 +++++++++++++++++ .../curation/merge/MergeStrategy.java | 17 +++++++++++++++++ .../curation/sidebar/CurationSidebar.java | 2 +- .../asciidoc/user-guide/curation-sidebar.adoc | 17 +++++++++++++++++ .../user-guide/images/curation-sidebar.png | Bin 0 -> 134018 bytes .../META-INF/asciidoc/user-guide.adoc | 2 ++ pom.xml | 16 ++++++---------- 10 files changed, 85 insertions(+), 12 deletions(-) create mode 100644 inception-curation/src/main/resources/META-INF/asciidoc/user-guide/curation-sidebar.adoc create mode 100644 inception-curation/src/main/resources/META-INF/asciidoc/user-guide/images/curation-sidebar.png diff --git a/inception-app-webapp/pom.xml b/inception-app-webapp/pom.xml index 180fa4ca680..2d1e02bfada 100644 --- a/inception-app-webapp/pom.xml +++ b/inception-app-webapp/pom.xml @@ -494,7 +494,6 @@ de.tudarmstadt.ukp.inception.app inception-curation - 0.12.0-SNAPSHOT diff --git a/inception-curation/pom.xml b/inception-curation/pom.xml index 2e40550c252..220233b1ad1 100644 --- a/inception-curation/pom.xml +++ b/inception-curation/pom.xml @@ -91,5 +91,13 @@ de.tudarmstadt.ukp.clarin.webanno webanno-support + + de.tudarmstadt.ukp.clarin.webanno + webanno-curation + + + de.tudarmstadt.ukp.clarin.webanno + webanno-ui-curation + \ No newline at end of file diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/AutomaticMergeStrategy.java b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/AutomaticMergeStrategy.java index 4d4e250b4be..93aa775a32c 100644 --- a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/AutomaticMergeStrategy.java +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/AutomaticMergeStrategy.java @@ -1,3 +1,20 @@ +/* + * Copyright 2019 + * Ubiquitous Knowledge Processing (UKP) Lab + * Technische Universität Darmstadt + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package de.tudarmstadt.ukp.inception.curation.merge; import static de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.LinkCompareBehavior.LINK_ROLE_AS_LABEL; diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/ManualMergeStrategy.java b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/ManualMergeStrategy.java index 2b98a9fee90..c7ad3f1576a 100644 --- a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/ManualMergeStrategy.java +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/ManualMergeStrategy.java @@ -1,3 +1,20 @@ +/* + * Copyright 2019 + * Ubiquitous Knowledge Processing (UKP) Lab + * Technische Universität Darmstadt + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package de.tudarmstadt.ukp.inception.curation.merge; import java.util.Map; diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/MergeStrategy.java b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/MergeStrategy.java index da3494fb721..a0bd69e1513 100644 --- a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/MergeStrategy.java +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/MergeStrategy.java @@ -1,3 +1,20 @@ +/* + * Copyright 2019 + * Ubiquitous Knowledge Processing (UKP) Lab + * Technische Universität Darmstadt + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package de.tudarmstadt.ukp.inception.curation.merge; import java.util.Map; diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java index 34fbd5d4a26..09811dd9d81 100644 --- a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java @@ -239,7 +239,7 @@ private void merge(AjaxRequestTarget aTarget, Form aForm) state.getProject().getId(), doc); if (targetCas.isPresent()) { mergeStrat.merge(state, targetCas.get(), userCases); - log.info("{} merge done", mergeStrat.getUiName()); //TODO change to debug + log.debug("{} merge done", mergeStrat.getUiName()); } } catch (IOException e) { diff --git a/inception-curation/src/main/resources/META-INF/asciidoc/user-guide/curation-sidebar.adoc b/inception-curation/src/main/resources/META-INF/asciidoc/user-guide/curation-sidebar.adoc new file mode 100644 index 00000000000..3c8eef7f9f6 --- /dev/null +++ b/inception-curation/src/main/resources/META-INF/asciidoc/user-guide/curation-sidebar.adoc @@ -0,0 +1,17 @@ +== Curation + +Curation i.e. the process of combining finished annotated documents into a final curated document, +can be done via the *Curation Page* in INCEpTION (see <>) +but also via the *Curation Sidebar* on the *Annotation Page*. + +image::curation-sidebar.png[Curation Sidebar] + +The sidebar allows the user to select the users whose annotations she wants to inspect +and merge into the final document. Annotations from other users are shown in gray above the user's annotations. +The user can decide to *manually* or *automatically* merge their +annotations into either her own document or a separate _Curation User Document_ as the final document. + +The *automatic* merge will copy all annotations that all selected users agree on into the curation document. +For *manual* merge, no annotations will be automatically copied. +However, the user can always copy annotations from other users into the curation document by clicking on them. + diff --git a/inception-curation/src/main/resources/META-INF/asciidoc/user-guide/images/curation-sidebar.png b/inception-curation/src/main/resources/META-INF/asciidoc/user-guide/images/curation-sidebar.png new file mode 100644 index 0000000000000000000000000000000000000000..2931d934dd9fc97919dd3633a1c1ce92e10f7d91 GIT binary patch literal 134018 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 literal 0 HcmV?d00001 diff --git a/inception-doc/src/main/resources/META-INF/asciidoc/user-guide.adoc b/inception-doc/src/main/resources/META-INF/asciidoc/user-guide.adoc index 15c714202de..053c6904e45 100644 --- a/inception-doc/src/main/resources/META-INF/asciidoc/user-guide.adoc +++ b/inception-doc/src/main/resources/META-INF/asciidoc/user-guide.adoc @@ -89,6 +89,8 @@ include::{include-dir}search-mtas.adoc[leveloffset=+2] include::{include-dir}annotation_recommendation.adoc[leveloffset=+2] +include::{include-dir}curation-sidebar.adoc[leveloffset=+2] + include::{include-dir}annotation_activeLearning.adoc[leveloffset=+2] include::{include-dir}annotation_concept-linking.adoc[leveloffset=+2] diff --git a/pom.xml b/pom.xml index 614b8541b82..5db4f8be2a8 100644 --- a/pom.xml +++ b/pom.xml @@ -214,10 +214,6 @@ javax.activation-api runtime - - de.tudarmstadt.ukp.clarin.webanno - webanno-ui-curation - @@ -382,11 +378,6 @@ inception-search-mtas 0.12.0-SNAPSHOT - - de.tudarmstadt.ukp.inception.app - inception-curation - 0.11.0-SNAPSHOT - @@ -1078,7 +1069,12 @@ d3js 5.5.0 - + + + de.tudarmstadt.ukp.inception.app + inception-curation + 0.12.0-SNAPSHOT + From 3df898090dc7db9301eba80ced08eb7adec0c110 Mon Sep 17 00:00:00 2001 From: Richard Eckart de Castilho Date: Tue, 24 Sep 2019 20:31:48 +0200 Subject: [PATCH 031/453] #1294 - Add more feature config switches - Extract interface from EntityLinkingProperties - Configure search module using Spring Boot auto-configuration - Configure external search module using Spring Boot auto-configuration - Configure external search ElasticSearch module using Spring Boot auto-configuration - Configure external search PubAnnotation module using Spring Boot auto-configuration - Added switch "external-search.enabled" - Added switch "external-search.elastic-search.enabled" - Added switch "external-search.pub-annotation.enabled" --- .../config/EntityLinkingProperties.java | 68 ++------------ .../config/EntityLinkingPropertiesImpl.java | 89 ++++++++++++++++++ .../service/ConceptLinkingServiceImpl.java | 3 +- .../ConceptLinkingServiceImplTest.java | 4 +- .../config/ExternalSearchProperties.java | 2 +- ...chDocumentRepositoryAutoConfiguration.java | 3 + ...onDocumentRepositoryAutoConfiguration.java | 10 ++ .../PubAnnotationSectionsFormatSupport.java | 10 +- .../FeatureIndexingSupportRegistryImpl.java | 9 +- .../search/PrimitiveUimaIndexingSupport.java | 9 +- .../ukp/inception/search/SearchService.java | 2 - .../inception/search/SearchServiceImpl.java | 9 +- .../SearchServiceAutoConfiguration.java | 92 +++++++++++++++++++ .../config/SearchServiceProperties.java | 23 +++++ .../config/SearchServicePropertiesImpl.java | 44 +++++++++ .../index/PhysicalIndexRegistryImpl.java | 10 +- .../search/log/SearchQueryEventAdapter.java | 9 +- .../search/scheduling/IndexScheduler.java | 7 +- .../main/resources/META-INF/spring.factories | 2 + .../SearchServiceUIAutoConfiguration.java | 39 ++++++++ .../SearchAnnotationSidebarFactory.java | 9 +- .../main/resources/META-INF/spring.factories | 2 + 22 files changed, 372 insertions(+), 83 deletions(-) create mode 100644 inception-concept-linking/src/main/java/de/tudarmstadt/ukp/inception/conceptlinking/config/EntityLinkingPropertiesImpl.java create mode 100644 inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/config/SearchServiceAutoConfiguration.java create mode 100644 inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/config/SearchServiceProperties.java create mode 100644 inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/config/SearchServicePropertiesImpl.java create mode 100644 inception-search-core/src/main/resources/META-INF/spring.factories create mode 100644 inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/config/SearchServiceUIAutoConfiguration.java create mode 100644 inception-ui-search/src/main/resources/META-INF/spring.factories diff --git a/inception-concept-linking/src/main/java/de/tudarmstadt/ukp/inception/conceptlinking/config/EntityLinkingProperties.java b/inception-concept-linking/src/main/java/de/tudarmstadt/ukp/inception/conceptlinking/config/EntityLinkingProperties.java index bc161767ea9..8ce7e9a245b 100644 --- a/inception-concept-linking/src/main/java/de/tudarmstadt/ukp/inception/conceptlinking/config/EntityLinkingProperties.java +++ b/inception-concept-linking/src/main/java/de/tudarmstadt/ukp/inception/conceptlinking/config/EntityLinkingProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 + * Copyright 2019 * Ubiquitous Knowledge Processing (UKP) Lab * Technische Universität Darmstadt * @@ -15,71 +15,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package de.tudarmstadt.ukp.inception.conceptlinking.config; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.stereotype.Component; - -@Component -@ConfigurationProperties("inception.entity-linking") -public class EntityLinkingProperties +public interface EntityLinkingProperties { - private int cacheSize = 1024; - - private int mentionContextSize = 5; - private int candidateQueryLimit = 2500; - private int candidateDisplayLimit = 100; - private int signatureQueryLimit = Integer.MAX_VALUE; - - public int getCacheSize() - { - return cacheSize; - } - - public void setCacheSize(int cacheSize) - { - this.cacheSize = cacheSize; - } + int getCacheSize(); - public int getMentionContextSize() - { - return mentionContextSize; - } + int getMentionContextSize(); - public void setMentionContextSize(int mentionContextSize) - { - this.mentionContextSize = mentionContextSize; - } + int getCandidateQueryLimit(); - public int getCandidateQueryLimit() - { - return candidateQueryLimit; - } + int getCandidateDisplayLimit(); - public void setCandidateQueryLimit(int candidateQueryLimit) - { - this.candidateQueryLimit = candidateQueryLimit; - } - - public int getCandidateDisplayLimit() - { - return candidateDisplayLimit; - } - - public void setCandidateDisplayLimit(int candidateDisplayLimit) - { - this.candidateDisplayLimit = candidateDisplayLimit; - } - - public int getSignatureQueryLimit() - { - return signatureQueryLimit; - } - - public void setSignatureQueryLimit(int signatureQueryLimit) - { - this.signatureQueryLimit = signatureQueryLimit; - } + int getSignatureQueryLimit(); } - diff --git a/inception-concept-linking/src/main/java/de/tudarmstadt/ukp/inception/conceptlinking/config/EntityLinkingPropertiesImpl.java b/inception-concept-linking/src/main/java/de/tudarmstadt/ukp/inception/conceptlinking/config/EntityLinkingPropertiesImpl.java new file mode 100644 index 00000000000..4ebd08b1a06 --- /dev/null +++ b/inception-concept-linking/src/main/java/de/tudarmstadt/ukp/inception/conceptlinking/config/EntityLinkingPropertiesImpl.java @@ -0,0 +1,89 @@ +/* + * Copyright 2018 + * Ubiquitous Knowledge Processing (UKP) Lab + * Technische Universität Darmstadt + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.tudarmstadt.ukp.inception.conceptlinking.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Component +@ConfigurationProperties("inception.entity-linking") +public class EntityLinkingPropertiesImpl implements EntityLinkingProperties +{ + private int cacheSize = 1024; + + private int mentionContextSize = 5; + private int candidateQueryLimit = 2500; + private int candidateDisplayLimit = 100; + private int signatureQueryLimit = Integer.MAX_VALUE; + + @Override + public int getCacheSize() + { + return cacheSize; + } + + public void setCacheSize(int cacheSize) + { + this.cacheSize = cacheSize; + } + + @Override + public int getMentionContextSize() + { + return mentionContextSize; + } + + public void setMentionContextSize(int mentionContextSize) + { + this.mentionContextSize = mentionContextSize; + } + + @Override + public int getCandidateQueryLimit() + { + return candidateQueryLimit; + } + + public void setCandidateQueryLimit(int candidateQueryLimit) + { + this.candidateQueryLimit = candidateQueryLimit; + } + + @Override + public int getCandidateDisplayLimit() + { + return candidateDisplayLimit; + } + + public void setCandidateDisplayLimit(int candidateDisplayLimit) + { + this.candidateDisplayLimit = candidateDisplayLimit; + } + + @Override + public int getSignatureQueryLimit() + { + return signatureQueryLimit; + } + + public void setSignatureQueryLimit(int signatureQueryLimit) + { + this.signatureQueryLimit = signatureQueryLimit; + } +} + diff --git a/inception-concept-linking/src/main/java/de/tudarmstadt/ukp/inception/conceptlinking/service/ConceptLinkingServiceImpl.java b/inception-concept-linking/src/main/java/de/tudarmstadt/ukp/inception/conceptlinking/service/ConceptLinkingServiceImpl.java index 8435d5532a5..882fff3fbff 100644 --- a/inception-concept-linking/src/main/java/de/tudarmstadt/ukp/inception/conceptlinking/service/ConceptLinkingServiceImpl.java +++ b/inception-concept-linking/src/main/java/de/tudarmstadt/ukp/inception/conceptlinking/service/ConceptLinkingServiceImpl.java @@ -56,6 +56,7 @@ import de.tudarmstadt.ukp.clarin.webanno.api.RepositoryProperties; import de.tudarmstadt.ukp.clarin.webanno.model.Project; import de.tudarmstadt.ukp.inception.conceptlinking.config.EntityLinkingProperties; +import de.tudarmstadt.ukp.inception.conceptlinking.config.EntityLinkingPropertiesImpl; import de.tudarmstadt.ukp.inception.conceptlinking.feature.EntityRankingFeatureGenerator; import de.tudarmstadt.ukp.inception.conceptlinking.model.CandidateEntity; import de.tudarmstadt.ukp.inception.conceptlinking.ranking.BaselineRankingStrategy; @@ -85,7 +86,7 @@ public class ConceptLinkingServiceImpl @Autowired public ConceptLinkingServiceImpl(KnowledgeBaseService aKbService, - EntityLinkingProperties aProperties, + EntityLinkingPropertiesImpl aProperties, RepositoryProperties aRepoProperties, @Lazy @Autowired(required = false) List aFeatureGenerators) diff --git a/inception-concept-linking/src/test/java/de/tudarmstadt/ukp/inception/conceptlinking/service/ConceptLinkingServiceImplTest.java b/inception-concept-linking/src/test/java/de/tudarmstadt/ukp/inception/conceptlinking/service/ConceptLinkingServiceImplTest.java index 9bf615913e6..6bc26c13ed1 100644 --- a/inception-concept-linking/src/test/java/de/tudarmstadt/ukp/inception/conceptlinking/service/ConceptLinkingServiceImplTest.java +++ b/inception-concept-linking/src/test/java/de/tudarmstadt/ukp/inception/conceptlinking/service/ConceptLinkingServiceImplTest.java @@ -40,7 +40,7 @@ import de.tudarmstadt.ukp.clarin.webanno.api.RepositoryProperties; import de.tudarmstadt.ukp.clarin.webanno.model.Project; -import de.tudarmstadt.ukp.inception.conceptlinking.config.EntityLinkingProperties; +import de.tudarmstadt.ukp.inception.conceptlinking.config.EntityLinkingPropertiesImpl; import de.tudarmstadt.ukp.inception.conceptlinking.util.TestFixtures; import de.tudarmstadt.ukp.inception.kb.ConceptFeatureValueType; import de.tudarmstadt.ukp.inception.kb.KnowledgeBaseService; @@ -78,7 +78,7 @@ public void setUp() throws Exception EntityManager entityManager = testEntityManager.getEntityManager(); TestFixtures testFixtures = new TestFixtures(testEntityManager); kbService = new KnowledgeBaseServiceImpl(repoProps, entityManager); - sut = new ConceptLinkingServiceImpl(kbService, new EntityLinkingProperties(), repoProps, + sut = new ConceptLinkingServiceImpl(kbService, new EntityLinkingPropertiesImpl(), repoProps, emptyList()); sut.afterPropertiesSet(); sut.init(); diff --git a/inception-external-search-core/src/main/java/de/tudarmstadt/ukp/inception/externalsearch/config/ExternalSearchProperties.java b/inception-external-search-core/src/main/java/de/tudarmstadt/ukp/inception/externalsearch/config/ExternalSearchProperties.java index 3c9f9a153b8..2e183908749 100644 --- a/inception-external-search-core/src/main/java/de/tudarmstadt/ukp/inception/externalsearch/config/ExternalSearchProperties.java +++ b/inception-external-search-core/src/main/java/de/tudarmstadt/ukp/inception/externalsearch/config/ExternalSearchProperties.java @@ -22,7 +22,7 @@ import org.springframework.stereotype.Component; @Component -@ConfigurationProperties("inception.external-search") +@ConfigurationProperties("external-search") public class ExternalSearchProperties { private boolean enabled = false; diff --git a/inception-external-search-elastic/src/main/java/de/tudarmstadt/ukp/inception/externalsearch/elastic/config/ElasticSearchDocumentRepositoryAutoConfiguration.java b/inception-external-search-elastic/src/main/java/de/tudarmstadt/ukp/inception/externalsearch/elastic/config/ElasticSearchDocumentRepositoryAutoConfiguration.java index e2ae8429b98..2642ce70bc7 100644 --- a/inception-external-search-elastic/src/main/java/de/tudarmstadt/ukp/inception/externalsearch/elastic/config/ElasticSearchDocumentRepositoryAutoConfiguration.java +++ b/inception-external-search-elastic/src/main/java/de/tudarmstadt/ukp/inception/externalsearch/elastic/config/ElasticSearchDocumentRepositoryAutoConfiguration.java @@ -19,6 +19,7 @@ import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -31,6 +32,8 @@ */ @Configuration @AutoConfigureAfter(ExternalSearchAutoConfiguration.class) +@ConditionalOnProperty(prefix = "external-search.elastic-search", name = "enabled", + havingValue = "true", matchIfMissing = true) @ConditionalOnBean(ExternalSearchService.class) public class ElasticSearchDocumentRepositoryAutoConfiguration { diff --git a/inception-external-search-pubannotation/src/main/java/de/tudarmstadt/ukp/inception/externalsearch/pubannotation/config/PubAnnotationDocumentRepositoryAutoConfiguration.java b/inception-external-search-pubannotation/src/main/java/de/tudarmstadt/ukp/inception/externalsearch/pubannotation/config/PubAnnotationDocumentRepositoryAutoConfiguration.java index 320b207a973..87aa2141c05 100644 --- a/inception-external-search-pubannotation/src/main/java/de/tudarmstadt/ukp/inception/externalsearch/pubannotation/config/PubAnnotationDocumentRepositoryAutoConfiguration.java +++ b/inception-external-search-pubannotation/src/main/java/de/tudarmstadt/ukp/inception/externalsearch/pubannotation/config/PubAnnotationDocumentRepositoryAutoConfiguration.java @@ -19,18 +19,22 @@ import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import de.tudarmstadt.ukp.inception.externalsearch.ExternalSearchService; import de.tudarmstadt.ukp.inception.externalsearch.config.ExternalSearchAutoConfiguration; import de.tudarmstadt.ukp.inception.externalsearch.pubannotation.PubAnnotationProviderFactory; +import de.tudarmstadt.ukp.inception.externalsearch.pubannotation.format.PubAnnotationSectionsFormatSupport; /** * Provides support for ElasticSearch-based document repositories. */ @Configuration @AutoConfigureAfter(ExternalSearchAutoConfiguration.class) +@ConditionalOnProperty(prefix = "external-search.pub-annotation", name = "enabled", + havingValue = "true", matchIfMissing = true) @ConditionalOnBean(ExternalSearchService.class) public class PubAnnotationDocumentRepositoryAutoConfiguration { @@ -39,4 +43,10 @@ public PubAnnotationProviderFactory pubAnnotationProviderFactory() { return new PubAnnotationProviderFactory(); } + + @Bean + public PubAnnotationSectionsFormatSupport pubAnnotationSectionsFormatSupport() + { + return new PubAnnotationSectionsFormatSupport(); + } } diff --git a/inception-external-search-pubannotation/src/main/java/de/tudarmstadt/ukp/inception/externalsearch/pubannotation/format/PubAnnotationSectionsFormatSupport.java b/inception-external-search-pubannotation/src/main/java/de/tudarmstadt/ukp/inception/externalsearch/pubannotation/format/PubAnnotationSectionsFormatSupport.java index ba2d083fc99..93ff57cffc6 100644 --- a/inception-external-search-pubannotation/src/main/java/de/tudarmstadt/ukp/inception/externalsearch/pubannotation/format/PubAnnotationSectionsFormatSupport.java +++ b/inception-external-search-pubannotation/src/main/java/de/tudarmstadt/ukp/inception/externalsearch/pubannotation/format/PubAnnotationSectionsFormatSupport.java @@ -21,11 +21,17 @@ import org.apache.uima.collection.CollectionReaderDescription; import org.apache.uima.resource.ResourceInitializationException; -import org.springframework.stereotype.Component; import de.tudarmstadt.ukp.clarin.webanno.api.format.FormatSupport; +import de.tudarmstadt.ukp.inception.externalsearch.pubannotation.config.PubAnnotationDocumentRepositoryAutoConfiguration; -@Component +/** + * Support for PubAnnotation JSON format. + *

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

+ */ public class PubAnnotationSectionsFormatSupport implements FormatSupport { diff --git a/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/FeatureIndexingSupportRegistryImpl.java b/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/FeatureIndexingSupportRegistryImpl.java index 5a0905166a5..fb6f34d79da 100644 --- a/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/FeatureIndexingSupportRegistryImpl.java +++ b/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/FeatureIndexingSupportRegistryImpl.java @@ -32,11 +32,16 @@ import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.context.event.EventListener; import org.springframework.core.annotation.AnnotationAwareOrderComparator; -import org.springframework.stereotype.Component; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationFeature; +import de.tudarmstadt.ukp.inception.search.config.SearchServiceAutoConfiguration; -@Component +/** + *

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

+ */ public class FeatureIndexingSupportRegistryImpl implements FeatureIndexingSupportRegistry { diff --git a/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/PrimitiveUimaIndexingSupport.java b/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/PrimitiveUimaIndexingSupport.java index d961dc42b3d..37fc240684a 100644 --- a/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/PrimitiveUimaIndexingSupport.java +++ b/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/PrimitiveUimaIndexingSupport.java @@ -24,13 +24,18 @@ import org.apache.uima.cas.CAS; import org.apache.uima.cas.text.AnnotationFS; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.feature.FeatureSupport; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.feature.FeatureSupportRegistry; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationFeature; +import de.tudarmstadt.ukp.inception.search.config.SearchServiceAutoConfiguration; -@Component +/** + *

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

+ */ public class PrimitiveUimaIndexingSupport implements FeatureIndexingSupport { diff --git a/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/SearchService.java b/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/SearchService.java index 67e58356b57..64067777176 100644 --- a/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/SearchService.java +++ b/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/SearchService.java @@ -33,8 +33,6 @@ public interface SearchService { - static final String SERVICE_NAME = "searchService"; - List query(User aUser, Project aProject, String aQuery) throws IOException, ExecutionException; diff --git a/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/SearchServiceImpl.java b/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/SearchServiceImpl.java index 7a5ce64b4fd..50193bcc945 100644 --- a/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/SearchServiceImpl.java +++ b/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/SearchServiceImpl.java @@ -33,7 +33,6 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.event.EventListener; -import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.event.TransactionalEventListener; @@ -52,13 +51,19 @@ import de.tudarmstadt.ukp.clarin.webanno.model.Project; import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.clarin.webanno.security.model.User; +import de.tudarmstadt.ukp.inception.search.config.SearchServiceAutoConfiguration; import de.tudarmstadt.ukp.inception.search.index.PhysicalIndex; import de.tudarmstadt.ukp.inception.search.index.PhysicalIndexFactory; import de.tudarmstadt.ukp.inception.search.index.PhysicalIndexRegistry; import de.tudarmstadt.ukp.inception.search.model.Index; import de.tudarmstadt.ukp.inception.search.scheduling.IndexScheduler; -@Component(SearchService.SERVICE_NAME) +/** + *

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

+ */ @Transactional public class SearchServiceImpl implements SearchService diff --git a/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/config/SearchServiceAutoConfiguration.java b/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/config/SearchServiceAutoConfiguration.java new file mode 100644 index 00000000000..5deddcb3433 --- /dev/null +++ b/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/config/SearchServiceAutoConfiguration.java @@ -0,0 +1,92 @@ +/* + * Copyright 2019 + * Ubiquitous Knowledge Processing (UKP) Lab + * Technische Universität Darmstadt + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.tudarmstadt.ukp.inception.search.config; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +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 org.springframework.context.annotation.Lazy; + +import de.tudarmstadt.ukp.clarin.webanno.api.annotation.feature.FeatureSupportRegistry; +import de.tudarmstadt.ukp.inception.search.FeatureIndexingSupport; +import de.tudarmstadt.ukp.inception.search.FeatureIndexingSupportRegistry; +import de.tudarmstadt.ukp.inception.search.FeatureIndexingSupportRegistryImpl; +import de.tudarmstadt.ukp.inception.search.PrimitiveUimaIndexingSupport; +import de.tudarmstadt.ukp.inception.search.SearchService; +import de.tudarmstadt.ukp.inception.search.SearchServiceImpl; +import de.tudarmstadt.ukp.inception.search.index.PhysicalIndexFactory; +import de.tudarmstadt.ukp.inception.search.index.PhysicalIndexRegistry; +import de.tudarmstadt.ukp.inception.search.index.PhysicalIndexRegistryImpl; +import de.tudarmstadt.ukp.inception.search.log.SearchQueryEventAdapter; +import de.tudarmstadt.ukp.inception.search.scheduling.IndexScheduler; + +@Configuration +@EnableConfigurationProperties(SearchServicePropertiesImpl.class) +@ConditionalOnProperty(prefix = "search", name = "enabled", havingValue = "true", + matchIfMissing = true) +public class SearchServiceAutoConfiguration +{ + @Bean + public SearchService searchService() + { + return new SearchServiceImpl(); + } + + @Bean + public SearchQueryEventAdapter searchQueryEventAdapter() + { + return new SearchQueryEventAdapter(); + } + + @Bean + public PhysicalIndexRegistry physicalIndexRegistry( + @Lazy @Autowired(required = false) List aExtensions) + { + return new PhysicalIndexRegistryImpl(aExtensions); + } + + @Bean + public FeatureIndexingSupportRegistry featureIndexingSupportRegistry( + @Lazy @Autowired(required = false) List aIndexingSupports) + { + return new FeatureIndexingSupportRegistryImpl(aIndexingSupports); + } + + @Bean + public SearchServiceProperties searchServiceProperties() + { + return new SearchServicePropertiesImpl(); + } + + @Bean + public IndexScheduler indexScheduler() + { + return new IndexScheduler(); + } + + @Bean + public PrimitiveUimaIndexingSupport primitiveUimaIndexingSupport( + @Autowired FeatureSupportRegistry aFeatureSupportRegistry) + { + return new PrimitiveUimaIndexingSupport(aFeatureSupportRegistry); + } +} diff --git a/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/config/SearchServiceProperties.java b/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/config/SearchServiceProperties.java new file mode 100644 index 00000000000..109c595e517 --- /dev/null +++ b/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/config/SearchServiceProperties.java @@ -0,0 +1,23 @@ +/* + * Copyright 2019 + * Ubiquitous Knowledge Processing (UKP) Lab + * Technische Universität Darmstadt + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.tudarmstadt.ukp.inception.search.config; + +public interface SearchServiceProperties +{ + boolean isEnabled(); +} diff --git a/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/config/SearchServicePropertiesImpl.java b/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/config/SearchServicePropertiesImpl.java new file mode 100644 index 00000000000..129b01e5f66 --- /dev/null +++ b/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/config/SearchServicePropertiesImpl.java @@ -0,0 +1,44 @@ +/* + * Copyright 2019 + * Ubiquitous Knowledge Processing (UKP) Lab + * Technische Universität Darmstadt + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.tudarmstadt.ukp.inception.search.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + *

+ * This class is exposed as a Spring Component via + * {@link SearchServiceAutoConfiguration#searchServiceProperties()}. + *

+ */ +@ConfigurationProperties("search") +public class SearchServicePropertiesImpl + implements SearchServiceProperties +{ + private boolean enabled = false; + + @Override + public boolean isEnabled() + { + return enabled; + } + + public void setEnabled(boolean aEnabled) + { + enabled = aEnabled; + } +} diff --git a/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/index/PhysicalIndexRegistryImpl.java b/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/index/PhysicalIndexRegistryImpl.java index f5666925cbd..09a6a6d974e 100644 --- a/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/index/PhysicalIndexRegistryImpl.java +++ b/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/index/PhysicalIndexRegistryImpl.java @@ -30,9 +30,15 @@ import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.context.event.EventListener; import org.springframework.core.annotation.AnnotationAwareOrderComparator; -import org.springframework.stereotype.Component; -@Component +import de.tudarmstadt.ukp.inception.search.config.SearchServiceAutoConfiguration; + +/** + *

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

+ */ public class PhysicalIndexRegistryImpl implements PhysicalIndexRegistry, BeanPostProcessor { diff --git a/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/log/SearchQueryEventAdapter.java b/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/log/SearchQueryEventAdapter.java index 789aaf16b0a..aed4c0dcf4f 100644 --- a/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/log/SearchQueryEventAdapter.java +++ b/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/log/SearchQueryEventAdapter.java @@ -21,14 +21,19 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.stereotype.Component; import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.clarin.webanno.support.JSONUtil; import de.tudarmstadt.ukp.inception.log.adapter.EventLoggingAdapter; +import de.tudarmstadt.ukp.inception.search.config.SearchServiceAutoConfiguration; import de.tudarmstadt.ukp.inception.search.event.SearchQueryEvent; -@Component +/** + *

+ * This class is exposed as a Spring Component via + * {@link SearchServiceAutoConfiguration#searchQueryEventAdapter()}. + *

+ */ public class SearchQueryEventAdapter implements EventLoggingAdapter { diff --git a/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/scheduling/IndexScheduler.java b/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/scheduling/IndexScheduler.java index f822e416019..0d159f584fa 100644 --- a/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/scheduling/IndexScheduler.java +++ b/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/scheduling/IndexScheduler.java @@ -30,11 +30,11 @@ import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; -import org.springframework.stereotype.Component; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationDocument; import de.tudarmstadt.ukp.clarin.webanno.model.Project; import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; +import de.tudarmstadt.ukp.inception.search.config.SearchServiceAutoConfiguration; import de.tudarmstadt.ukp.inception.search.scheduling.tasks.IndexAnnotationDocumentTask; import de.tudarmstadt.ukp.inception.search.scheduling.tasks.IndexSourceDocumentTask; import de.tudarmstadt.ukp.inception.search.scheduling.tasks.ReindexTask; @@ -42,8 +42,11 @@ /** * Indexer scheduler. Does the project re-indexing in an asynchronous way. + *

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

*/ -@Component public class IndexScheduler implements InitializingBean, DisposableBean { diff --git a/inception-search-core/src/main/resources/META-INF/spring.factories b/inception-search-core/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000000..24c1309e596 --- /dev/null +++ b/inception-search-core/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +de.tudarmstadt.ukp.inception.search.config.SearchServiceAutoConfiguration diff --git a/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/config/SearchServiceUIAutoConfiguration.java b/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/config/SearchServiceUIAutoConfiguration.java new file mode 100644 index 00000000000..5c6d09b3c72 --- /dev/null +++ b/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/config/SearchServiceUIAutoConfiguration.java @@ -0,0 +1,39 @@ +/* + * Copyright 2019 + * Ubiquitous Knowledge Processing (UKP) Lab + * Technische Universität Darmstadt + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.tudarmstadt.ukp.inception.app.ui.search.config; + +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import de.tudarmstadt.ukp.inception.app.ui.search.sidebar.SearchAnnotationSidebarFactory; +import de.tudarmstadt.ukp.inception.search.SearchService; +import de.tudarmstadt.ukp.inception.search.config.SearchServiceAutoConfiguration; + +@Configuration +@AutoConfigureAfter(SearchServiceAutoConfiguration.class) +@ConditionalOnBean(SearchService.class) +public class SearchServiceUIAutoConfiguration +{ + @Bean + public SearchAnnotationSidebarFactory searchAnnotationSidebarFactory() + { + return new SearchAnnotationSidebarFactory(); + } +} diff --git a/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebarFactory.java b/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebarFactory.java index 54d3a980e55..0f017e42cd0 100644 --- a/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebarFactory.java +++ b/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebarFactory.java @@ -20,7 +20,6 @@ import org.apache.wicket.model.IModel; import org.apache.wicket.request.resource.PackageResourceReference; import org.apache.wicket.request.resource.ResourceReference; -import org.springframework.stereotype.Component; import de.tudarmstadt.ukp.clarin.webanno.api.CasProvider; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.action.AnnotationActionHandler; @@ -28,8 +27,14 @@ import de.tudarmstadt.ukp.clarin.webanno.ui.annotation.AnnotationPage; import de.tudarmstadt.ukp.clarin.webanno.ui.annotation.sidebar.AnnotationSidebarFactory_ImplBase; import de.tudarmstadt.ukp.clarin.webanno.ui.annotation.sidebar.AnnotationSidebar_ImplBase; +import de.tudarmstadt.ukp.inception.app.ui.search.config.SearchServiceUIAutoConfiguration; -@Component("searchSidebar") +/** + *

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

+ */ public class SearchAnnotationSidebarFactory extends AnnotationSidebarFactory_ImplBase { diff --git a/inception-ui-search/src/main/resources/META-INF/spring.factories b/inception-ui-search/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000000..620d74c4cf1 --- /dev/null +++ b/inception-ui-search/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +de.tudarmstadt.ukp.inception.app.ui.search.config.SearchServiceUIAutoConfiguration From 8435cb7ff3c74e6fe001431c2d6fe0a9b3ec49cc Mon Sep 17 00:00:00 2001 From: Richard Eckart de Castilho Date: Tue, 24 Sep 2019 21:05:28 +0200 Subject: [PATCH 032/453] #1294 - Add more feature config switches - Configure recommendation module using Spring Boot auto-configuration - Added switch "recommenders.enabled" --- .../api/LearningRecordService.java | 2 - .../api/RecommendationService.java | 1 - inception-recommendation/pom.xml | 9 +- .../RecommendationEditorExtension.java | 7 +- .../RecommenderServiceAutoConfiguration.java | 154 ++++++++++++++++++ .../EvaluationSimulationPageMenuItem.java | 9 +- .../exporter/RecommenderExporter.java | 14 +- .../RecommendationAcceptedEventAdapter.java | 9 +- .../RecommendationRejectedEventAdapter.java | 9 +- .../log/RecommenderDeletedEventAdapter.java | 10 +- ...commenderEvaluationResultEventAdapter.java | 9 +- ...ecommenderProjectSettingsPanelFactory.java | 9 +- .../service/LearningRecordServiceImpl.java | 18 +- .../service/RecommendationServiceImpl.java | 22 +-- .../RecommenderFactoryRegistryImpl.java | 9 +- .../sidebar/RecommendationSidebarFactory.java | 9 +- .../main/resources/META-INF/spring.factories | 2 + 17 files changed, 255 insertions(+), 47 deletions(-) create mode 100644 inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/config/RecommenderServiceAutoConfiguration.java create mode 100644 inception-recommendation/src/main/resources/META-INF/spring.factories diff --git a/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/LearningRecordService.java b/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/LearningRecordService.java index 00b00d84e81..05cd8a6840f 100644 --- a/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/LearningRecordService.java +++ b/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/LearningRecordService.java @@ -30,8 +30,6 @@ public interface LearningRecordService { - String SERVICE_NAME = "LearningRecordService"; - List listRecords(String user, AnnotationLayer layer); /** diff --git a/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/RecommendationService.java b/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/RecommendationService.java index 15e0d1e8d65..7b0085ec168 100644 --- a/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/RecommendationService.java +++ b/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/RecommendationService.java @@ -44,7 +44,6 @@ */ public interface RecommendationService { - String SERVICE_NAME = "recommendationService"; String FEATURE_NAME_IS_PREDICTION = "inception_internal_predicted"; String FEATURE_NAME_SCORE_SUFFIX = "_score"; String FEATURE_NAME_SCORE_EXPLANATION_SUFFIX = "_score_explanation"; diff --git a/inception-recommendation/pom.xml b/inception-recommendation/pom.xml index 3b3ed50bfde..5bccb3bd726 100644 --- a/inception-recommendation/pom.xml +++ b/inception-recommendation/pom.xml @@ -122,6 +122,10 @@ org.springframework.boot spring-boot + + org.springframework.boot + spring-boot-autoconfigure + org.springframework.security spring-security-core @@ -214,11 +218,6 @@ spring-test test - - org.springframework.boot - spring-boot-autoconfigure - test - org.springframework.boot spring-boot-test-autoconfigure diff --git a/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/RecommendationEditorExtension.java b/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/RecommendationEditorExtension.java index 932b65396ee..7c107503030 100644 --- a/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/RecommendationEditorExtension.java +++ b/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/RecommendationEditorExtension.java @@ -36,7 +36,6 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationEventPublisher; -import org.springframework.stereotype.Component; import de.tudarmstadt.ukp.clarin.webanno.api.AnnotationSchemaService; import de.tudarmstadt.ukp.clarin.webanno.api.DocumentService; @@ -59,6 +58,7 @@ import de.tudarmstadt.ukp.inception.recommendation.api.model.AnnotationSuggestion; import de.tudarmstadt.ukp.inception.recommendation.api.model.Predictions; import de.tudarmstadt.ukp.inception.recommendation.api.model.Recommender; +import de.tudarmstadt.ukp.inception.recommendation.config.RecommenderServiceAutoConfiguration; import de.tudarmstadt.ukp.inception.recommendation.event.AjaxRecommendationAcceptedEvent; import de.tudarmstadt.ukp.inception.recommendation.event.AjaxRecommendationRejectedEvent; import de.tudarmstadt.ukp.inception.recommendation.event.PredictionsSwitchedEvent; @@ -74,8 +74,11 @@ *
  • Intercept user actions on the annotation suggestions, in particular accepting or rejecting * annotatons.
  • * + *

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

    */ -@Component(RecommendationEditorExtension.BEAN_NAME) public class RecommendationEditorExtension extends AnnotationEditorExtensionImplBase implements AnnotationEditorExtension diff --git a/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/config/RecommenderServiceAutoConfiguration.java b/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/config/RecommenderServiceAutoConfiguration.java new file mode 100644 index 00000000000..4da32811523 --- /dev/null +++ b/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/config/RecommenderServiceAutoConfiguration.java @@ -0,0 +1,154 @@ +/* + * Copyright 2018 + * Ubiquitous Knowledge Processing (UKP) Lab + * Technische Universität Darmstadt + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.tudarmstadt.ukp.inception.recommendation.config; + +import java.util.List; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; +import org.springframework.security.core.session.SessionRegistry; + +import de.tudarmstadt.ukp.clarin.webanno.api.AnnotationSchemaService; +import de.tudarmstadt.ukp.clarin.webanno.api.DocumentService; +import de.tudarmstadt.ukp.clarin.webanno.api.ProjectService; +import de.tudarmstadt.ukp.clarin.webanno.api.annotation.feature.FeatureSupportRegistry; +import de.tudarmstadt.ukp.clarin.webanno.security.UserDao; +import de.tudarmstadt.ukp.inception.recommendation.RecommendationEditorExtension; +import de.tudarmstadt.ukp.inception.recommendation.api.LearningRecordService; +import de.tudarmstadt.ukp.inception.recommendation.api.RecommendationService; +import de.tudarmstadt.ukp.inception.recommendation.api.RecommenderFactoryRegistry; +import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommendationEngineFactory; +import de.tudarmstadt.ukp.inception.recommendation.evaluation.EvaluationSimulationPageMenuItem; +import de.tudarmstadt.ukp.inception.recommendation.exporter.RecommenderExporter; +import de.tudarmstadt.ukp.inception.recommendation.log.RecommendationAcceptedEventAdapter; +import de.tudarmstadt.ukp.inception.recommendation.log.RecommendationRejectedEventAdapter; +import de.tudarmstadt.ukp.inception.recommendation.log.RecommenderDeletedEventAdapter; +import de.tudarmstadt.ukp.inception.recommendation.log.RecommenderEvaluationResultEventAdapter; +import de.tudarmstadt.ukp.inception.recommendation.project.RecommenderProjectSettingsPanelFactory; +import de.tudarmstadt.ukp.inception.recommendation.service.LearningRecordServiceImpl; +import de.tudarmstadt.ukp.inception.recommendation.service.RecommendationServiceImpl; +import de.tudarmstadt.ukp.inception.recommendation.service.RecommenderFactoryRegistryImpl; +import de.tudarmstadt.ukp.inception.recommendation.sidebar.RecommendationSidebarFactory; +import de.tudarmstadt.ukp.inception.scheduling.SchedulingService; + +/** + * Provides all back-end Spring beans for the external search functionality. + */ +@Configuration +@ConditionalOnProperty(prefix = "recommenders", name = "enabled", havingValue = "true", + matchIfMissing = true) +public class RecommenderServiceAutoConfiguration +{ + private @PersistenceContext EntityManager entityManager; + + @Bean + @Autowired + public RecommendationService recommendationService(SessionRegistry aSessionRegistry, + UserDao aUserRepository, RecommenderFactoryRegistry aRecommenderFactoryRegistry, + SchedulingService aSchedulingService, AnnotationSchemaService aAnnoService, + DocumentService aDocumentService, LearningRecordService aLearningRecordService, + ProjectService aProjectService) + { + return new RecommendationServiceImpl(aSessionRegistry, aUserRepository, + aRecommenderFactoryRegistry, aSchedulingService, aAnnoService, aDocumentService, + aLearningRecordService, aProjectService, entityManager); + } + + @Bean + public LearningRecordService learningRecordService() + { + return new LearningRecordServiceImpl(entityManager); + } + + @Bean + public EvaluationSimulationPageMenuItem evaluationSimulationPageMenuItem() + { + return new EvaluationSimulationPageMenuItem(); + } + + @Bean + @Autowired + public RecommenderExporter recommenderExporter(AnnotationSchemaService aAnnotationService, + RecommendationService aRecommendationService) + { + return new RecommenderExporter(aAnnotationService, aRecommendationService); + } + + @Bean + public RecommendationAcceptedEventAdapter recommendationAcceptedEventAdapter() + { + return new RecommendationAcceptedEventAdapter(); + } + + @Bean + public RecommendationRejectedEventAdapter recommendationRejectedEventAdapter() + { + return new RecommendationRejectedEventAdapter(); + } + + @Bean + public RecommenderDeletedEventAdapter recommenderDeletedEventAdapter() + { + return new RecommenderDeletedEventAdapter(); + } + + @Bean + public RecommenderEvaluationResultEventAdapter recommenderEvaluationResultEventAdapter() + { + return new RecommenderEvaluationResultEventAdapter(); + } + + @Bean + public RecommenderProjectSettingsPanelFactory recommenderProjectSettingsPanelFactory() + { + return new RecommenderProjectSettingsPanelFactory(); + } + + @Bean + public RecommendationSidebarFactory recommendationSidebarFactory() + { + return new RecommendationSidebarFactory(); + } + + @Bean(name = RecommendationEditorExtension.BEAN_NAME) + @Autowired + public RecommendationEditorExtension recommendationEditorExtension( + AnnotationSchemaService aAnnotationService, + RecommendationService aRecommendationService, + LearningRecordService aLearningRecordService, + ApplicationEventPublisher aApplicationEventPublisher, + FeatureSupportRegistry aFsRegistry, DocumentService aDocumentService) + { + return new RecommendationEditorExtension(aAnnotationService, aRecommendationService, + aLearningRecordService, aApplicationEventPublisher, aFsRegistry, aDocumentService); + } + + @Bean + public RecommenderFactoryRegistry recommenderFactoryRegistry( + @Lazy @Autowired(required = false) List aExtensions) + { + return new RecommenderFactoryRegistryImpl(aExtensions); + } +} diff --git a/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/evaluation/EvaluationSimulationPageMenuItem.java b/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/evaluation/EvaluationSimulationPageMenuItem.java index 0f422eb2ca3..f4c14a95a8a 100644 --- a/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/evaluation/EvaluationSimulationPageMenuItem.java +++ b/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/evaluation/EvaluationSimulationPageMenuItem.java @@ -20,14 +20,19 @@ import org.apache.wicket.Page; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.annotation.Order; -import org.springframework.stereotype.Component; import de.tudarmstadt.ukp.clarin.webanno.api.ProjectService; import de.tudarmstadt.ukp.clarin.webanno.api.SecurityUtil; import de.tudarmstadt.ukp.clarin.webanno.security.UserDao; import de.tudarmstadt.ukp.clarin.webanno.ui.core.menu.MenuItem; +import de.tudarmstadt.ukp.inception.recommendation.config.RecommenderServiceAutoConfiguration; -@Component +/** + *

    + * This class is exposed as a Spring Component via + * {@link RecommenderServiceAutoConfiguration#evaluationSimulationPageMenuItem()}. + *

    + */ @Order(300) public class EvaluationSimulationPageMenuItem implements MenuItem diff --git a/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/exporter/RecommenderExporter.java b/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/exporter/RecommenderExporter.java index 2c9df6d988a..69554733834 100644 --- a/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/exporter/RecommenderExporter.java +++ b/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/exporter/RecommenderExporter.java @@ -29,7 +29,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; import de.tudarmstadt.ukp.clarin.webanno.api.AnnotationSchemaService; import de.tudarmstadt.ukp.clarin.webanno.api.dao.export.exporters.LayerExporter; @@ -42,10 +41,17 @@ import de.tudarmstadt.ukp.clarin.webanno.model.Project; import de.tudarmstadt.ukp.inception.recommendation.api.RecommendationService; import de.tudarmstadt.ukp.inception.recommendation.api.model.Recommender; +import de.tudarmstadt.ukp.inception.recommendation.config.RecommenderServiceAutoConfiguration; -@Component -public class RecommenderExporter implements ProjectExporter { - +/** + *

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

    + */ +public class RecommenderExporter + implements ProjectExporter +{ private static final String KEY = "recommenders"; private static final Logger LOG = LoggerFactory.getLogger(RecommenderExporter.class); diff --git a/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/log/RecommendationAcceptedEventAdapter.java b/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/log/RecommendationAcceptedEventAdapter.java index 45ca5b4c016..bebfac03a43 100644 --- a/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/log/RecommendationAcceptedEventAdapter.java +++ b/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/log/RecommendationAcceptedEventAdapter.java @@ -21,15 +21,20 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.stereotype.Component; import de.tudarmstadt.ukp.clarin.webanno.support.JSONUtil; import de.tudarmstadt.ukp.inception.log.adapter.EventLoggingAdapter; import de.tudarmstadt.ukp.inception.log.model.AnnotationDetails; import de.tudarmstadt.ukp.inception.log.model.FeatureChangeDetails; +import de.tudarmstadt.ukp.inception.recommendation.config.RecommenderServiceAutoConfiguration; import de.tudarmstadt.ukp.inception.recommendation.event.RecommendationAcceptedEvent; -@Component +/** + *

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

    + */ public class RecommendationAcceptedEventAdapter implements EventLoggingAdapter { diff --git a/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/log/RecommendationRejectedEventAdapter.java b/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/log/RecommendationRejectedEventAdapter.java index 17028af1f3e..c78dcfbc09b 100644 --- a/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/log/RecommendationRejectedEventAdapter.java +++ b/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/log/RecommendationRejectedEventAdapter.java @@ -21,15 +21,20 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.stereotype.Component; import de.tudarmstadt.ukp.clarin.webanno.support.JSONUtil; import de.tudarmstadt.ukp.inception.log.adapter.EventLoggingAdapter; import de.tudarmstadt.ukp.inception.log.model.AnnotationDetails; import de.tudarmstadt.ukp.inception.log.model.FeatureChangeDetails; +import de.tudarmstadt.ukp.inception.recommendation.config.RecommenderServiceAutoConfiguration; import de.tudarmstadt.ukp.inception.recommendation.event.RecommendationRejectedEvent; -@Component +/** + *

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

    + */ public class RecommendationRejectedEventAdapter implements EventLoggingAdapter { diff --git a/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/log/RecommenderDeletedEventAdapter.java b/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/log/RecommenderDeletedEventAdapter.java index 6943f46504c..e2b0e69a575 100644 --- a/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/log/RecommenderDeletedEventAdapter.java +++ b/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/log/RecommenderDeletedEventAdapter.java @@ -18,12 +18,16 @@ package de.tudarmstadt.ukp.inception.recommendation.log; -import org.springframework.stereotype.Component; - import de.tudarmstadt.ukp.inception.log.adapter.EventLoggingAdapter; +import de.tudarmstadt.ukp.inception.recommendation.config.RecommenderServiceAutoConfiguration; import de.tudarmstadt.ukp.inception.recommendation.event.RecommenderDeletedEvent; -@Component +/** + *

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

    + */ public class RecommenderDeletedEventAdapter implements EventLoggingAdapter { diff --git a/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/log/RecommenderEvaluationResultEventAdapter.java b/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/log/RecommenderEvaluationResultEventAdapter.java index afed48c2e38..080d1c7d4da 100644 --- a/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/log/RecommenderEvaluationResultEventAdapter.java +++ b/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/log/RecommenderEvaluationResultEventAdapter.java @@ -21,16 +21,21 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.stereotype.Component; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import de.tudarmstadt.ukp.clarin.webanno.support.JSONUtil; import de.tudarmstadt.ukp.inception.log.adapter.EventLoggingAdapter; import de.tudarmstadt.ukp.inception.recommendation.api.evaluation.EvaluationResult; +import de.tudarmstadt.ukp.inception.recommendation.config.RecommenderServiceAutoConfiguration; import de.tudarmstadt.ukp.inception.recommendation.event.RecommenderEvaluationResultEvent; -@Component +/** + *

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

    + */ public class RecommenderEvaluationResultEventAdapter implements EventLoggingAdapter { diff --git a/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/project/RecommenderProjectSettingsPanelFactory.java b/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/project/RecommenderProjectSettingsPanelFactory.java index 9a82e605919..19f0b1341c7 100644 --- a/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/project/RecommenderProjectSettingsPanelFactory.java +++ b/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/project/RecommenderProjectSettingsPanelFactory.java @@ -20,12 +20,17 @@ import org.apache.wicket.markup.html.panel.Panel; import org.apache.wicket.model.IModel; import org.springframework.core.annotation.Order; -import org.springframework.stereotype.Component; import de.tudarmstadt.ukp.clarin.webanno.model.Project; import de.tudarmstadt.ukp.clarin.webanno.ui.core.settings.ProjectSettingsPanelFactory; +import de.tudarmstadt.ukp.inception.recommendation.config.RecommenderServiceAutoConfiguration; -@Component +/** + *

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

    + */ @Order(400) public class RecommenderProjectSettingsPanelFactory implements ProjectSettingsPanelFactory diff --git a/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/LearningRecordServiceImpl.java b/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/LearningRecordServiceImpl.java index a76e90aff1e..8a0789e04c6 100644 --- a/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/LearningRecordServiceImpl.java +++ b/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/LearningRecordServiceImpl.java @@ -20,11 +20,9 @@ import java.util.List; import javax.persistence.EntityManager; -import javax.persistence.PersistenceContext; import javax.persistence.TypedQuery; import org.springframework.context.event.EventListener; -import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; import de.tudarmstadt.ukp.clarin.webanno.api.event.AfterDocumentResetEvent; @@ -37,14 +35,24 @@ import de.tudarmstadt.ukp.inception.recommendation.api.model.LearningRecord; import de.tudarmstadt.ukp.inception.recommendation.api.model.LearningRecordChangeLocation; import de.tudarmstadt.ukp.inception.recommendation.api.model.LearningRecordType; +import de.tudarmstadt.ukp.inception.recommendation.config.RecommenderServiceAutoConfiguration; -@Component(LearningRecordService.SERVICE_NAME) +/** + *

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

    + */ public class LearningRecordServiceImpl implements LearningRecordService { - @PersistenceContext - private EntityManager entityManager; + private final EntityManager entityManager; + public LearningRecordServiceImpl(EntityManager aEntityManager) + { + entityManager = aEntityManager; + } + @Transactional @EventListener public void afterDocumentReset(AfterDocumentResetEvent aEvent) { diff --git a/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommendationServiceImpl.java b/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommendationServiceImpl.java index 5b426ca6568..faacdb9548d 100644 --- a/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommendationServiceImpl.java +++ b/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommendationServiceImpl.java @@ -43,7 +43,6 @@ import javax.persistence.EntityManager; import javax.persistence.NoResultException; -import javax.persistence.PersistenceContext; import org.apache.commons.collections4.MapIterator; import org.apache.commons.collections4.MultiValuedMap; @@ -75,7 +74,6 @@ import org.springframework.security.core.session.SessionDestroyedEvent; import org.springframework.security.core.session.SessionInformation; import org.springframework.security.core.session.SessionRegistry; -import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; import de.tudarmstadt.ukp.clarin.webanno.api.AnnotationSchemaService; @@ -111,6 +109,7 @@ import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommendationEngine; import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommendationEngineFactory; import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommenderContext; +import de.tudarmstadt.ukp.inception.recommendation.config.RecommenderServiceAutoConfiguration; import de.tudarmstadt.ukp.inception.recommendation.event.RecommenderDeletedEvent; import de.tudarmstadt.ukp.inception.recommendation.tasks.SelectionTask; import de.tudarmstadt.ukp.inception.recommendation.tasks.TrainingTask; @@ -120,8 +119,11 @@ /** * The implementation of the RecommendationService. + *

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

    */ -@Component(RecommendationService.SERVICE_NAME) public class RecommendationServiceImpl implements RecommendationService { @@ -129,7 +131,7 @@ public class RecommendationServiceImpl private static final int TRAININGS_PER_SELECTION = 5; - private @PersistenceContext EntityManager entityManager; + private final EntityManager entityManager; private final SessionRegistry sessionRegistry; private final UserDao userRepository; @@ -165,7 +167,7 @@ public RecommendationServiceImpl(SessionRegistry aSessionRegistry, UserDao aUser RecommenderFactoryRegistry aRecommenderFactoryRegistry, SchedulingService aSchedulingService, AnnotationSchemaService aAnnoService, DocumentService aDocumentService, LearningRecordService aLearningRecordService, - ProjectService aProjectService) + ProjectService aProjectService, EntityManager aEntityManager) { sessionRegistry = aSessionRegistry; userRepository = aUserRepository; @@ -175,6 +177,7 @@ public RecommendationServiceImpl(SessionRegistry aSessionRegistry, UserDao aUser documentService = aDocumentService; learningRecordService = aLearningRecordService; projectService = aProjectService; + entityManager = aEntityManager; trainingTaskCounter = new ConcurrentHashMap<>(); states = new ConcurrentHashMap<>(); @@ -187,16 +190,13 @@ public RecommendationServiceImpl(SessionRegistry aSessionRegistry, UserDao aUser EntityManager aEntityManager) { this(aSessionRegistry, aUserRepository, aRecommenderFactoryRegistry, aSchedulingService, - aAnnoService, aDocumentService, aLearningRecordService, (ProjectService) null); - - entityManager = aEntityManager; + aAnnoService, aDocumentService, aLearningRecordService, (ProjectService) null, + aEntityManager); } public RecommendationServiceImpl(EntityManager aEntityManager) { - this(null, null, null, null, null, null, null, (ProjectService) null); - - entityManager = aEntityManager; + this(null, null, null, null, null, null, null, (ProjectService) null, aEntityManager); } @Override diff --git a/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommenderFactoryRegistryImpl.java b/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommenderFactoryRegistryImpl.java index 7ae9b1301c3..23959c9c0f4 100644 --- a/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommenderFactoryRegistryImpl.java +++ b/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommenderFactoryRegistryImpl.java @@ -32,14 +32,19 @@ import org.springframework.context.annotation.Lazy; import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.context.event.EventListener; -import org.springframework.stereotype.Component; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationFeature; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; import de.tudarmstadt.ukp.inception.recommendation.api.RecommenderFactoryRegistry; import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommendationEngineFactory; +import de.tudarmstadt.ukp.inception.recommendation.config.RecommenderServiceAutoConfiguration; -@Component +/** + *

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

    + */ public class RecommenderFactoryRegistryImpl implements RecommenderFactoryRegistry { diff --git a/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/sidebar/RecommendationSidebarFactory.java b/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/sidebar/RecommendationSidebarFactory.java index 97a96642c6f..c487bf849a1 100644 --- a/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/sidebar/RecommendationSidebarFactory.java +++ b/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/sidebar/RecommendationSidebarFactory.java @@ -20,7 +20,6 @@ import org.apache.wicket.model.IModel; import org.apache.wicket.request.resource.PackageResourceReference; import org.apache.wicket.request.resource.ResourceReference; -import org.springframework.stereotype.Component; import de.tudarmstadt.ukp.clarin.webanno.api.CasProvider; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.action.AnnotationActionHandler; @@ -28,8 +27,14 @@ import de.tudarmstadt.ukp.clarin.webanno.ui.annotation.AnnotationPage; import de.tudarmstadt.ukp.clarin.webanno.ui.annotation.sidebar.AnnotationSidebarFactory_ImplBase; import de.tudarmstadt.ukp.clarin.webanno.ui.annotation.sidebar.AnnotationSidebar_ImplBase; +import de.tudarmstadt.ukp.inception.recommendation.config.RecommenderServiceAutoConfiguration; -@Component("recommendationSidebar") +/** + *

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

    + */ public class RecommendationSidebarFactory extends AnnotationSidebarFactory_ImplBase { diff --git a/inception-recommendation/src/main/resources/META-INF/spring.factories b/inception-recommendation/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000000..5dc31fda6a5 --- /dev/null +++ b/inception-recommendation/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +de.tudarmstadt.ukp.inception.recommendation.config.RecommenderServiceAutoConfiguration \ No newline at end of file From f9fb848aeb6df2416ee916a0e45c7e473e59f62c Mon Sep 17 00:00:00 2001 From: Richard Eckart de Castilho Date: Tue, 24 Sep 2019 22:46:26 +0200 Subject: [PATCH 033/453] #1294 - Add more feature config switches - Configure concept linnking module using Spring Boot auto-configuration - Configure knowledge base modules using Spring Boot auto-configuration - Added switch "knowledge-base.enabled" - Added switch "knowledge-base.fact-linking.enabled" - Added switch "knowledge-base.entity-linking.enabled" --- inception-concept-linking/pom.xml | 9 +- .../config/EntityLinkingPropertiesImpl.java | 13 +- ...EntityLinkingServiceAutoConfiguration.java | 106 ++++++++++++ .../feature/FrequencyFeatureGenerator.java | 6 +- .../feature/LevenshteinFeatureGenerator.java | 9 +- .../SemanticSignatureFeatureGenerator.java | 5 +- .../WikidataIdRankFeatureGenerator.java | 17 +- .../recommender/NamedEntityLinkerFactory.java | 24 ++- .../service/ConceptLinkingServiceImpl.java | 9 +- .../main/resources/META-INF/spring.factories | 2 + inception-kb/pom.xml | 9 +- .../inception/kb/KnowledgeBaseService.java | 8 +- .../kb/KnowledgeBaseServiceImpl.java | 10 +- .../config/KnowledgeBasePropertiesImpl.java | 7 +- ...KnowledgeBaseServiceAutoConfiguration.java | 57 +++++++ .../kb/exporter/KnowledgeBaseExporter.java | 12 +- .../main/resources/META-INF/spring.factories | 2 + .../ui/kb/KnowledgeBasePageMenuItem.java | 27 ++- .../config/FactLinkingAutoConfiguration.java | 77 +++++++++ ...owledgeBaseServiceUIAutoConfiguration.java | 158 ++++++++++++++++++ .../ui/kb/feature/ConceptFeatureSupport.java | 7 +- .../ui/kb/feature/FactLinkingService.java | 2 - .../ui/kb/feature/FactLinkingServiceImpl.java | 12 +- .../ui/kb/feature/PropertyFeatureSupport.java | 9 +- .../feature/SubjectObjectFeatureSupport.java | 7 +- .../kb/initializers/FactLayerInitializer.java | 9 +- ...medEntityIdentifierFeatureInitializer.java | 7 +- ...wledgeBaseProjectSettingsPanelFactory.java | 9 +- .../search/ConceptFeatureIndexingSupport.java | 13 +- .../coloring/DefaultColoringStrategyImpl.java | 10 +- .../DescriptionColoringStrategyImpl.java | 10 +- .../coloring/LabelColoringStrategyImpl.java | 10 +- .../StatementColoringRegistryImpl.java | 12 +- .../SubclassOfColoringStrategyImpl.java | 10 +- .../coloring/TypeColoringStrategyImpl.java | 10 +- .../kb/value/BooleanLiteralValueSupport.java | 9 +- .../ui/kb/value/IriValueSupport.java | 9 +- .../kb/value/NumericLiteralValueSupport.java | 9 +- .../kb/value/StringLiteralValueSupport.java | 9 +- .../value/ValueTypeSupportRegistryImpl.java | 9 +- .../main/resources/META-INF/spring.factories | 2 + 41 files changed, 651 insertions(+), 100 deletions(-) create mode 100644 inception-concept-linking/src/main/java/de/tudarmstadt/ukp/inception/conceptlinking/config/EntityLinkingServiceAutoConfiguration.java create mode 100644 inception-concept-linking/src/main/resources/META-INF/spring.factories create mode 100644 inception-kb/src/main/java/de/tudarmstadt/ukp/inception/kb/config/KnowledgeBaseServiceAutoConfiguration.java create mode 100644 inception-kb/src/main/resources/META-INF/spring.factories create mode 100644 inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/config/FactLinkingAutoConfiguration.java create mode 100644 inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/config/KnowledgeBaseServiceUIAutoConfiguration.java create mode 100644 inception-ui-kb/src/main/resources/META-INF/spring.factories diff --git a/inception-concept-linking/pom.xml b/inception-concept-linking/pom.xml index e158f7dac01..9cc443c9602 100644 --- a/inception-concept-linking/pom.xml +++ b/inception-concept-linking/pom.xml @@ -127,6 +127,10 @@ org.springframework.boot spring-boot
    + + org.springframework.boot + spring-boot-autoconfigure + org.springframework spring-beans @@ -177,11 +181,6 @@ - - org.springframework.boot - spring-boot-autoconfigure - test - org.springframework.boot spring-boot-test-autoconfigure diff --git a/inception-concept-linking/src/main/java/de/tudarmstadt/ukp/inception/conceptlinking/config/EntityLinkingPropertiesImpl.java b/inception-concept-linking/src/main/java/de/tudarmstadt/ukp/inception/conceptlinking/config/EntityLinkingPropertiesImpl.java index 4ebd08b1a06..356c82bedec 100644 --- a/inception-concept-linking/src/main/java/de/tudarmstadt/ukp/inception/conceptlinking/config/EntityLinkingPropertiesImpl.java +++ b/inception-concept-linking/src/main/java/de/tudarmstadt/ukp/inception/conceptlinking/config/EntityLinkingPropertiesImpl.java @@ -18,11 +18,16 @@ package de.tudarmstadt.ukp.inception.conceptlinking.config; import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.stereotype.Component; -@Component -@ConfigurationProperties("inception.entity-linking") -public class EntityLinkingPropertiesImpl implements EntityLinkingProperties +/** + *

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

    + */ +@ConfigurationProperties("knowledge-base.entity-linking") +public class EntityLinkingPropertiesImpl + implements EntityLinkingProperties { private int cacheSize = 1024; diff --git a/inception-concept-linking/src/main/java/de/tudarmstadt/ukp/inception/conceptlinking/config/EntityLinkingServiceAutoConfiguration.java b/inception-concept-linking/src/main/java/de/tudarmstadt/ukp/inception/conceptlinking/config/EntityLinkingServiceAutoConfiguration.java new file mode 100644 index 00000000000..42b9088d135 --- /dev/null +++ b/inception-concept-linking/src/main/java/de/tudarmstadt/ukp/inception/conceptlinking/config/EntityLinkingServiceAutoConfiguration.java @@ -0,0 +1,106 @@ +/* + * Copyright 2019 + * Ubiquitous Knowledge Processing (UKP) Lab + * Technische Universität Darmstadt + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.tudarmstadt.ukp.inception.conceptlinking.config; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +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 org.springframework.context.annotation.Lazy; + +import de.tudarmstadt.ukp.clarin.webanno.api.RepositoryProperties; +import de.tudarmstadt.ukp.clarin.webanno.api.annotation.feature.FeatureSupportRegistry; +import de.tudarmstadt.ukp.inception.conceptlinking.feature.EntityRankingFeatureGenerator; +import de.tudarmstadt.ukp.inception.conceptlinking.feature.FrequencyFeatureGenerator; +import de.tudarmstadt.ukp.inception.conceptlinking.feature.LevenshteinFeatureGenerator; +import de.tudarmstadt.ukp.inception.conceptlinking.feature.SemanticSignatureFeatureGenerator; +import de.tudarmstadt.ukp.inception.conceptlinking.feature.WikidataIdRankFeatureGenerator; +import de.tudarmstadt.ukp.inception.conceptlinking.recommender.NamedEntityLinkerFactory; +import de.tudarmstadt.ukp.inception.conceptlinking.service.ConceptLinkingService; +import de.tudarmstadt.ukp.inception.conceptlinking.service.ConceptLinkingServiceImpl; +import de.tudarmstadt.ukp.inception.kb.KnowledgeBaseService; +import de.tudarmstadt.ukp.inception.recommendation.api.RecommendationService; + +@Configuration +@ConditionalOnBean(KnowledgeBaseService.class) +@ConditionalOnProperty(prefix = "knowledge-base.entity-linking", name = "enabled", + havingValue = "true", matchIfMissing = true) +@EnableConfigurationProperties(EntityLinkingPropertiesImpl.class) +public class EntityLinkingServiceAutoConfiguration +{ + @Bean + @Autowired + public ConceptLinkingService conceptLinkingService(KnowledgeBaseService aKbService, + EntityLinkingPropertiesImpl aProperties, + RepositoryProperties aRepoProperties, + @Lazy @Autowired(required = false) List + aFeatureGenerators) + { + return new ConceptLinkingServiceImpl(aKbService, aProperties, aRepoProperties, + aFeatureGenerators); + } + + @Bean + public EntityLinkingProperties entityLinkingProperties() + { + return new EntityLinkingPropertiesImpl(); + } + + @Bean + public LevenshteinFeatureGenerator levenshteinFeatureGenerator() + { + return new LevenshteinFeatureGenerator(); + } + + @Bean + @Autowired + public WikidataIdRankFeatureGenerator wikidataIdRankFeatureGenerator( + KnowledgeBaseService aKbService) + { + return new WikidataIdRankFeatureGenerator(aKbService); + } + + @ConditionalOnBean(RecommendationService.class) + @Bean + @Autowired + public NamedEntityLinkerFactory namedEntityLinkerFactory(KnowledgeBaseService aKbService, + ConceptLinkingService aClService, FeatureSupportRegistry aFsRegistry) + { + return new NamedEntityLinkerFactory(aKbService, aClService, aFsRegistry); + } + +// @Bean +// @Autowired + public FrequencyFeatureGenerator frequencyFeatureGenerator(RepositoryProperties aRepoProperties) + { + return new FrequencyFeatureGenerator(aRepoProperties); + } + +// @Bean +// @Autowired + public SemanticSignatureFeatureGenerator semanticSignatureFeatureGenerator( + KnowledgeBaseService aKbService, RepositoryProperties aRepoProperties, + EntityLinkingProperties aProperties) + { + return new SemanticSignatureFeatureGenerator(aKbService, aRepoProperties, aProperties); + } +} diff --git a/inception-concept-linking/src/main/java/de/tudarmstadt/ukp/inception/conceptlinking/feature/FrequencyFeatureGenerator.java b/inception-concept-linking/src/main/java/de/tudarmstadt/ukp/inception/conceptlinking/feature/FrequencyFeatureGenerator.java index 8b04196677f..8bc9975d4e9 100644 --- a/inception-concept-linking/src/main/java/de/tudarmstadt/ukp/inception/conceptlinking/feature/FrequencyFeatureGenerator.java +++ b/inception-concept-linking/src/main/java/de/tudarmstadt/ukp/inception/conceptlinking/feature/FrequencyFeatureGenerator.java @@ -24,13 +24,17 @@ import java.util.Map; import de.tudarmstadt.ukp.clarin.webanno.api.RepositoryProperties; +import de.tudarmstadt.ukp.inception.conceptlinking.config.EntityLinkingServiceAutoConfiguration; import de.tudarmstadt.ukp.inception.conceptlinking.model.CandidateEntity; import de.tudarmstadt.ukp.inception.conceptlinking.util.FileUtils; /** * Assigns frequency priors from a pre-defined dictionary. + *

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

    */ -// @Component public class FrequencyFeatureGenerator implements EntityRankingFeatureGenerator { diff --git a/inception-concept-linking/src/main/java/de/tudarmstadt/ukp/inception/conceptlinking/feature/LevenshteinFeatureGenerator.java b/inception-concept-linking/src/main/java/de/tudarmstadt/ukp/inception/conceptlinking/feature/LevenshteinFeatureGenerator.java index 29e9f5248d0..607b2e61bbb 100644 --- a/inception-concept-linking/src/main/java/de/tudarmstadt/ukp/inception/conceptlinking/feature/LevenshteinFeatureGenerator.java +++ b/inception-concept-linking/src/main/java/de/tudarmstadt/ukp/inception/conceptlinking/feature/LevenshteinFeatureGenerator.java @@ -26,11 +26,16 @@ import org.apache.commons.lang3.StringUtils; import org.apache.commons.text.similarity.LevenshteinDistance; -import org.springframework.stereotype.Component; +import de.tudarmstadt.ukp.inception.conceptlinking.config.EntityLinkingServiceAutoConfiguration; import de.tudarmstadt.ukp.inception.conceptlinking.model.CandidateEntity; -@Component +/** + *

    + * This class is exposed as a Spring Component via + * {@link EntityLinkingServiceAutoConfiguration#levenshteinFeatureGenerator()}. + *

    + */ public class LevenshteinFeatureGenerator implements EntityRankingFeatureGenerator { diff --git a/inception-concept-linking/src/main/java/de/tudarmstadt/ukp/inception/conceptlinking/feature/SemanticSignatureFeatureGenerator.java b/inception-concept-linking/src/main/java/de/tudarmstadt/ukp/inception/conceptlinking/feature/SemanticSignatureFeatureGenerator.java index 2e2b13c1918..26b5c21edc4 100644 --- a/inception-concept-linking/src/main/java/de/tudarmstadt/ukp/inception/conceptlinking/feature/SemanticSignatureFeatureGenerator.java +++ b/inception-concept-linking/src/main/java/de/tudarmstadt/ukp/inception/conceptlinking/feature/SemanticSignatureFeatureGenerator.java @@ -67,8 +67,11 @@ * so we can compare the mention context to the KB context. However, the present implementation * is very slow and doesn't help much. Before reenabling this we need to properly evaluate this, * test this, and see how performance can be improved. + *

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

    */ -//@Component public class SemanticSignatureFeatureGenerator implements EntityRankingFeatureGenerator { diff --git a/inception-concept-linking/src/main/java/de/tudarmstadt/ukp/inception/conceptlinking/feature/WikidataIdRankFeatureGenerator.java b/inception-concept-linking/src/main/java/de/tudarmstadt/ukp/inception/conceptlinking/feature/WikidataIdRankFeatureGenerator.java index ee18c9ee8c8..afd2e8061ba 100644 --- a/inception-concept-linking/src/main/java/de/tudarmstadt/ukp/inception/conceptlinking/feature/WikidataIdRankFeatureGenerator.java +++ b/inception-concept-linking/src/main/java/de/tudarmstadt/ukp/inception/conceptlinking/feature/WikidataIdRankFeatureGenerator.java @@ -26,18 +26,29 @@ import org.eclipse.rdf4j.repository.config.RepositoryImplConfig; import org.eclipse.rdf4j.repository.sparql.config.SPARQLRepositoryConfig; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; +import de.tudarmstadt.ukp.inception.conceptlinking.config.EntityLinkingServiceAutoConfiguration; import de.tudarmstadt.ukp.inception.conceptlinking.model.CandidateEntity; import de.tudarmstadt.ukp.inception.kb.KnowledgeBaseService; import de.tudarmstadt.ukp.inception.kb.model.KnowledgeBase; -@Component +/** + *

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

    + */ public class WikidataIdRankFeatureGenerator implements EntityRankingFeatureGenerator { - private @Autowired KnowledgeBaseService kbService; + private final KnowledgeBaseService kbService; + @Autowired + public WikidataIdRankFeatureGenerator(KnowledgeBaseService aKbService) + { + kbService = aKbService; + } + @Override public void apply(CandidateEntity aCandidate) { diff --git a/inception-concept-linking/src/main/java/de/tudarmstadt/ukp/inception/conceptlinking/recommender/NamedEntityLinkerFactory.java b/inception-concept-linking/src/main/java/de/tudarmstadt/ukp/inception/conceptlinking/recommender/NamedEntityLinkerFactory.java index f1d44670d9f..6eaf98d8c40 100644 --- a/inception-concept-linking/src/main/java/de/tudarmstadt/ukp/inception/conceptlinking/recommender/NamedEntityLinkerFactory.java +++ b/inception-concept-linking/src/main/java/de/tudarmstadt/ukp/inception/conceptlinking/recommender/NamedEntityLinkerFactory.java @@ -25,12 +25,12 @@ import org.apache.wicket.model.IModel; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.feature.FeatureSupport; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.feature.FeatureSupportRegistry; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationFeature; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; +import de.tudarmstadt.ukp.inception.conceptlinking.config.EntityLinkingServiceAutoConfiguration; import de.tudarmstadt.ukp.inception.conceptlinking.service.ConceptLinkingService; import de.tudarmstadt.ukp.inception.kb.ConceptFeatureTraits; import de.tudarmstadt.ukp.inception.kb.KnowledgeBaseService; @@ -38,7 +38,12 @@ import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommendationEngine; import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommendationEngineFactoryImplBase; -@Component +/** + *

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

    + */ public class NamedEntityLinkerFactory extends RecommendationEngineFactoryImplBase { @@ -49,9 +54,18 @@ public class NamedEntityLinkerFactory private static final String PREFIX = "kb:"; - private @Autowired KnowledgeBaseService kbService; - private @Autowired ConceptLinkingService clService; - private @Autowired FeatureSupportRegistry fsRegistry; + private final KnowledgeBaseService kbService; + private final ConceptLinkingService clService; + private final FeatureSupportRegistry fsRegistry; + + @Autowired + public NamedEntityLinkerFactory(KnowledgeBaseService aKbService, + ConceptLinkingService aClService, FeatureSupportRegistry aFsRegistry) + { + kbService = aKbService; + clService = aClService; + fsRegistry = aFsRegistry; + } @Override public String getId() diff --git a/inception-concept-linking/src/main/java/de/tudarmstadt/ukp/inception/conceptlinking/service/ConceptLinkingServiceImpl.java b/inception-concept-linking/src/main/java/de/tudarmstadt/ukp/inception/conceptlinking/service/ConceptLinkingServiceImpl.java index 882fff3fbff..ebd30e8f674 100644 --- a/inception-concept-linking/src/main/java/de/tudarmstadt/ukp/inception/conceptlinking/service/ConceptLinkingServiceImpl.java +++ b/inception-concept-linking/src/main/java/de/tudarmstadt/ukp/inception/conceptlinking/service/ConceptLinkingServiceImpl.java @@ -51,12 +51,12 @@ import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.context.event.EventListener; import org.springframework.core.annotation.AnnotationAwareOrderComparator; -import org.springframework.stereotype.Component; import de.tudarmstadt.ukp.clarin.webanno.api.RepositoryProperties; import de.tudarmstadt.ukp.clarin.webanno.model.Project; import de.tudarmstadt.ukp.inception.conceptlinking.config.EntityLinkingProperties; import de.tudarmstadt.ukp.inception.conceptlinking.config.EntityLinkingPropertiesImpl; +import de.tudarmstadt.ukp.inception.conceptlinking.config.EntityLinkingServiceAutoConfiguration; import de.tudarmstadt.ukp.inception.conceptlinking.feature.EntityRankingFeatureGenerator; import de.tudarmstadt.ukp.inception.conceptlinking.model.CandidateEntity; import de.tudarmstadt.ukp.inception.conceptlinking.ranking.BaselineRankingStrategy; @@ -69,7 +69,12 @@ import de.tudarmstadt.ukp.inception.kb.querybuilder.SPARQLQueryBuilder; import de.tudarmstadt.ukp.inception.kb.querybuilder.SPARQLQueryPrimaryConditions; -@Component +/** + *

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

    + */ public class ConceptLinkingServiceImpl implements InitializingBean, ConceptLinkingService { diff --git a/inception-concept-linking/src/main/resources/META-INF/spring.factories b/inception-concept-linking/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000000..34d87b95dbf --- /dev/null +++ b/inception-concept-linking/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +de.tudarmstadt.ukp.inception.conceptlinking.config.EntityLinkingServiceAutoConfiguration \ No newline at end of file diff --git a/inception-kb/pom.xml b/inception-kb/pom.xml index 2f25f3dac9d..5f26512fb10 100644 --- a/inception-kb/pom.xml +++ b/inception-kb/pom.xml @@ -158,6 +158,10 @@ org.springframework.boot spring-boot
    + + org.springframework.boot + spring-boot-autoconfigure + com.fasterxml.jackson.dataformat @@ -239,11 +243,6 @@ - - org.springframework.boot - spring-boot-autoconfigure - test - org.springframework.boot spring-boot-test-autoconfigure diff --git a/inception-kb/src/main/java/de/tudarmstadt/ukp/inception/kb/KnowledgeBaseService.java b/inception-kb/src/main/java/de/tudarmstadt/ukp/inception/kb/KnowledgeBaseService.java index 04af8255153..9b6a571e47c 100644 --- a/inception-kb/src/main/java/de/tudarmstadt/ukp/inception/kb/KnowledgeBaseService.java +++ b/inception-kb/src/main/java/de/tudarmstadt/ukp/inception/kb/KnowledgeBaseService.java @@ -45,13 +45,11 @@ public interface KnowledgeBaseService { - String SERVICE_NAME = "knowledgeBaseService"; - /** - * Reads knowledgebase profiles from a YAML file and stores them in a HashMap with the key that + * Reads knowledge base profiles from a YAML file and stores them in a HashMap with the key that * is defined in the file and a corresponding {@link KnowledgeBaseProfile} object as value * - * @return a HashMap with the knowledgebase profiles + * @return a HashMap with the knowledge base profiles * @throws IOException * if an error occurs when reading the file */ @@ -61,7 +59,7 @@ public interface KnowledgeBaseService /** * Writes the contents of a knowledge base of type {@link RepositoryType#LOCAL} to a given - * {@link OutputStream} in a specificable format.
    + * {@link OutputStream} in a specifiable format.
    * No action will be taken if the given knowledge base is not of type * {@link RepositoryType#LOCAL} (nothing will be written to the output stream). * diff --git a/inception-kb/src/main/java/de/tudarmstadt/ukp/inception/kb/KnowledgeBaseServiceImpl.java b/inception-kb/src/main/java/de/tudarmstadt/ukp/inception/kb/KnowledgeBaseServiceImpl.java index 0631be64ed5..052a3973b88 100644 --- a/inception-kb/src/main/java/de/tudarmstadt/ukp/inception/kb/KnowledgeBaseServiceImpl.java +++ b/inception-kb/src/main/java/de/tudarmstadt/ukp/inception/kb/KnowledgeBaseServiceImpl.java @@ -89,7 +89,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.context.event.EventListener; -import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; import com.fasterxml.jackson.core.type.TypeReference; @@ -101,6 +100,7 @@ import de.tudarmstadt.ukp.clarin.webanno.model.Project; import de.tudarmstadt.ukp.clarin.webanno.support.SettingsUtil; import de.tudarmstadt.ukp.clarin.webanno.support.StopWatch; +import de.tudarmstadt.ukp.inception.kb.config.KnowledgeBaseServiceAutoConfiguration; import de.tudarmstadt.ukp.inception.kb.graph.KBConcept; import de.tudarmstadt.ukp.inception.kb.graph.KBHandle; import de.tudarmstadt.ukp.inception.kb.graph.KBInstance; @@ -116,8 +116,12 @@ import de.tudarmstadt.ukp.inception.kb.reification.WikiDataReification; import de.tudarmstadt.ukp.inception.kb.yaml.KnowledgeBaseProfile; - -@Component(KnowledgeBaseService.SERVICE_NAME) +/** + *

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

    + */ public class KnowledgeBaseServiceImpl implements KnowledgeBaseService, DisposableBean { diff --git a/inception-kb/src/main/java/de/tudarmstadt/ukp/inception/kb/config/KnowledgeBasePropertiesImpl.java b/inception-kb/src/main/java/de/tudarmstadt/ukp/inception/kb/config/KnowledgeBasePropertiesImpl.java index ba522468cfb..b994ec604fb 100644 --- a/inception-kb/src/main/java/de/tudarmstadt/ukp/inception/kb/config/KnowledgeBasePropertiesImpl.java +++ b/inception-kb/src/main/java/de/tudarmstadt/ukp/inception/kb/config/KnowledgeBasePropertiesImpl.java @@ -18,11 +18,10 @@ package de.tudarmstadt.ukp.inception.kb.config; import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.stereotype.Component; -@Component -@ConfigurationProperties("inception.knowledge-base") -public class KnowledgeBasePropertiesImpl implements KnowledgeBaseProperties +@ConfigurationProperties("knowledge-base") +public class KnowledgeBasePropertiesImpl + implements KnowledgeBaseProperties { public static final int HARD_MIN_RESULTS = 10; diff --git a/inception-kb/src/main/java/de/tudarmstadt/ukp/inception/kb/config/KnowledgeBaseServiceAutoConfiguration.java b/inception-kb/src/main/java/de/tudarmstadt/ukp/inception/kb/config/KnowledgeBaseServiceAutoConfiguration.java new file mode 100644 index 00000000000..46b74c4ccc8 --- /dev/null +++ b/inception-kb/src/main/java/de/tudarmstadt/ukp/inception/kb/config/KnowledgeBaseServiceAutoConfiguration.java @@ -0,0 +1,57 @@ +/* + * Copyright 2019 + * Ubiquitous Knowledge Processing (UKP) Lab + * Technische Universität Darmstadt + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.tudarmstadt.ukp.inception.kb.config; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; + +import org.springframework.beans.factory.annotation.Autowired; +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.clarin.webanno.api.AnnotationSchemaService; +import de.tudarmstadt.ukp.clarin.webanno.api.RepositoryProperties; +import de.tudarmstadt.ukp.inception.kb.KnowledgeBaseService; +import de.tudarmstadt.ukp.inception.kb.KnowledgeBaseServiceImpl; +import de.tudarmstadt.ukp.inception.kb.exporter.KnowledgeBaseExporter; + +@Configuration +@ConditionalOnProperty(prefix = "knowledge-base", name = "enabled", + havingValue = "true", matchIfMissing = true) +@EnableConfigurationProperties(KnowledgeBasePropertiesImpl.class) +public class KnowledgeBaseServiceAutoConfiguration +{ + private @PersistenceContext EntityManager entityManager; + + @Autowired + @Bean + public KnowledgeBaseExporter knowledgeBaseExporter(KnowledgeBaseService aKbService, + KnowledgeBaseProperties aKbProperties, AnnotationSchemaService aSchemaService) + { + return new KnowledgeBaseExporter(aKbService, aKbProperties, aSchemaService); + } + + @Bean + @Autowired + public KnowledgeBaseService knowledgeBaseService(RepositoryProperties aRepoProperties) + { + return new KnowledgeBaseServiceImpl(aRepoProperties, entityManager); + } +} diff --git a/inception-kb/src/main/java/de/tudarmstadt/ukp/inception/kb/exporter/KnowledgeBaseExporter.java b/inception-kb/src/main/java/de/tudarmstadt/ukp/inception/kb/exporter/KnowledgeBaseExporter.java index 82e3d694bf5..1b2854d206f 100644 --- a/inception-kb/src/main/java/de/tudarmstadt/ukp/inception/kb/exporter/KnowledgeBaseExporter.java +++ b/inception-kb/src/main/java/de/tudarmstadt/ukp/inception/kb/exporter/KnowledgeBaseExporter.java @@ -39,7 +39,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; import de.tudarmstadt.ukp.clarin.webanno.api.AnnotationSchemaService; import de.tudarmstadt.ukp.clarin.webanno.api.dao.export.exporters.LayerExporter; @@ -57,11 +56,18 @@ import de.tudarmstadt.ukp.inception.kb.SchemaProfile; import de.tudarmstadt.ukp.inception.kb.config.KnowledgeBaseProperties; import de.tudarmstadt.ukp.inception.kb.config.KnowledgeBasePropertiesImpl; +import de.tudarmstadt.ukp.inception.kb.config.KnowledgeBaseServiceAutoConfiguration; import de.tudarmstadt.ukp.inception.kb.model.KnowledgeBase; import de.tudarmstadt.ukp.inception.kb.reification.Reification; -@Component -public class KnowledgeBaseExporter implements ProjectExporter +/** + *

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

    + */ +public class KnowledgeBaseExporter + implements ProjectExporter { private static final String KEY = "knowledge_bases"; private static final Logger LOG = LoggerFactory.getLogger(KnowledgeBaseExporter.class); diff --git a/inception-kb/src/main/resources/META-INF/spring.factories b/inception-kb/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000000..e45e0300f8a --- /dev/null +++ b/inception-kb/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +de.tudarmstadt.ukp.inception.kb.config.KnowledgeBaseServiceAutoConfiguration \ No newline at end of file diff --git a/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/KnowledgeBasePageMenuItem.java b/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/KnowledgeBasePageMenuItem.java index b1ff93fc0c0..fc4280370ee 100644 --- a/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/KnowledgeBasePageMenuItem.java +++ b/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/KnowledgeBasePageMenuItem.java @@ -21,7 +21,6 @@ import org.apache.wicket.Session; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.annotation.Order; -import org.springframework.stereotype.Component; import de.tudarmstadt.ukp.clarin.webanno.api.ProjectService; import de.tudarmstadt.ukp.clarin.webanno.api.WebAnnoConst; @@ -31,14 +30,30 @@ import de.tudarmstadt.ukp.clarin.webanno.ui.core.menu.MenuItem; import de.tudarmstadt.ukp.inception.kb.KnowledgeBaseService; import de.tudarmstadt.ukp.inception.ui.core.session.SessionMetaData; +import de.tudarmstadt.ukp.inception.ui.kb.config.KnowledgeBaseServiceUIAutoConfiguration; -@Component +/** + *

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

    + */ @Order(220) -public class KnowledgeBasePageMenuItem implements MenuItem +public class KnowledgeBasePageMenuItem + implements MenuItem { - private @Autowired UserDao userRepo; - private @Autowired ProjectService projectService; - private @Autowired KnowledgeBaseService kbService; + private final UserDao userRepo; + private final ProjectService projectService; + private final KnowledgeBaseService kbService; + + @Autowired + public KnowledgeBasePageMenuItem(UserDao aUserRepo, ProjectService aProjectService, + KnowledgeBaseService aKbService) + { + userRepo = aUserRepo; + projectService = aProjectService; + kbService = aKbService; + } @Override public String getPath() diff --git a/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/config/FactLinkingAutoConfiguration.java b/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/config/FactLinkingAutoConfiguration.java new file mode 100644 index 00000000000..aee009765c1 --- /dev/null +++ b/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/config/FactLinkingAutoConfiguration.java @@ -0,0 +1,77 @@ +/* + * Copyright 2019 + * Ubiquitous Knowledge Processing (UKP) Lab + * Technische Universität Darmstadt + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.tudarmstadt.ukp.inception.ui.kb.config; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import de.tudarmstadt.ukp.clarin.webanno.api.AnnotationSchemaService; +import de.tudarmstadt.ukp.inception.kb.KnowledgeBaseService; +import de.tudarmstadt.ukp.inception.kb.config.KnowledgeBaseServiceAutoConfiguration; +import de.tudarmstadt.ukp.inception.ui.kb.feature.ConceptFeatureSupport; +import de.tudarmstadt.ukp.inception.ui.kb.feature.FactLinkingService; +import de.tudarmstadt.ukp.inception.ui.kb.feature.FactLinkingServiceImpl; +import de.tudarmstadt.ukp.inception.ui.kb.feature.PropertyFeatureSupport; +import de.tudarmstadt.ukp.inception.ui.kb.feature.SubjectObjectFeatureSupport; +import de.tudarmstadt.ukp.inception.ui.kb.initializers.FactLayerInitializer; + +@Configuration +@AutoConfigureAfter(KnowledgeBaseServiceAutoConfiguration.class) +@ConditionalOnBean(KnowledgeBaseService.class) +@ConditionalOnProperty(prefix = "knowledge-base.fact-linking", name = "enabled", + havingValue = "true", matchIfMissing = false) +public class FactLinkingAutoConfiguration +{ + @Bean + @Autowired + public ConceptFeatureSupport conceptFeatureSupport(KnowledgeBaseService aKbService) + { + return new ConceptFeatureSupport(aKbService); + } + + @Bean + @Autowired + public PropertyFeatureSupport propertyFeatureSupport(KnowledgeBaseService aKbService) + { + return new PropertyFeatureSupport(aKbService); + } + + @Bean + public SubjectObjectFeatureSupport subjectObjectFeatureSupport() + { + return new SubjectObjectFeatureSupport(); + } + + @Bean + public FactLinkingService factLinkingService() + { + return new FactLinkingServiceImpl(); + } + + @Bean + @Autowired + public FactLayerInitializer factLayerInitializer( + AnnotationSchemaService aAnnotationSchemaService) + { + return new FactLayerInitializer(aAnnotationSchemaService); + } +} diff --git a/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/config/KnowledgeBaseServiceUIAutoConfiguration.java b/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/config/KnowledgeBaseServiceUIAutoConfiguration.java new file mode 100644 index 00000000000..f2949011b24 --- /dev/null +++ b/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/config/KnowledgeBaseServiceUIAutoConfiguration.java @@ -0,0 +1,158 @@ +/* + * Copyright 2019 + * Ubiquitous Knowledge Processing (UKP) Lab + * Technische Universität Darmstadt + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.tudarmstadt.ukp.inception.ui.kb.config; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; + +import de.tudarmstadt.ukp.clarin.webanno.api.AnnotationSchemaService; +import de.tudarmstadt.ukp.clarin.webanno.api.ProjectService; +import de.tudarmstadt.ukp.clarin.webanno.api.annotation.feature.FeatureSupportRegistry; +import de.tudarmstadt.ukp.clarin.webanno.security.UserDao; +import de.tudarmstadt.ukp.inception.kb.KnowledgeBaseService; +import de.tudarmstadt.ukp.inception.kb.config.KnowledgeBaseServiceAutoConfiguration; +import de.tudarmstadt.ukp.inception.ui.kb.KnowledgeBasePageMenuItem; +import de.tudarmstadt.ukp.inception.ui.kb.initializers.NamedEntityIdentifierFeatureInitializer; +import de.tudarmstadt.ukp.inception.ui.kb.project.KnowledgeBaseProjectSettingsPanelFactory; +import de.tudarmstadt.ukp.inception.ui.kb.search.ConceptFeatureIndexingSupport; +import de.tudarmstadt.ukp.inception.ui.kb.stmt.coloring.DefaultColoringStrategyImpl; +import de.tudarmstadt.ukp.inception.ui.kb.stmt.coloring.DescriptionColoringStrategyImpl; +import de.tudarmstadt.ukp.inception.ui.kb.stmt.coloring.LabelColoringStrategyImpl; +import de.tudarmstadt.ukp.inception.ui.kb.stmt.coloring.StatementColoringRegistry; +import de.tudarmstadt.ukp.inception.ui.kb.stmt.coloring.StatementColoringRegistryImpl; +import de.tudarmstadt.ukp.inception.ui.kb.stmt.coloring.StatementColoringStrategy; +import de.tudarmstadt.ukp.inception.ui.kb.stmt.coloring.SubclassOfColoringStrategyImpl; +import de.tudarmstadt.ukp.inception.ui.kb.stmt.coloring.TypeColoringStrategyImpl; +import de.tudarmstadt.ukp.inception.ui.kb.value.BooleanLiteralValueSupport; +import de.tudarmstadt.ukp.inception.ui.kb.value.IriValueSupport; +import de.tudarmstadt.ukp.inception.ui.kb.value.NumericLiteralValueSupport; +import de.tudarmstadt.ukp.inception.ui.kb.value.StringLiteralValueSupport; +import de.tudarmstadt.ukp.inception.ui.kb.value.ValueTypeSupport; +import de.tudarmstadt.ukp.inception.ui.kb.value.ValueTypeSupportRegistry; +import de.tudarmstadt.ukp.inception.ui.kb.value.ValueTypeSupportRegistryImpl; + +@Configuration +@AutoConfigureAfter(KnowledgeBaseServiceAutoConfiguration.class) +@ConditionalOnBean(KnowledgeBaseService.class) +public class KnowledgeBaseServiceUIAutoConfiguration +{ + @Bean + @Autowired + public NamedEntityIdentifierFeatureInitializer namedEntityIdentifierFeatureInitializer( + AnnotationSchemaService aAnnotationSchemaService) + { + return new NamedEntityIdentifierFeatureInitializer(aAnnotationSchemaService); + } + + @Bean + public KnowledgeBaseProjectSettingsPanelFactory knowledgeBaseProjectSettingsPanelFactory() + { + return new KnowledgeBaseProjectSettingsPanelFactory(); + } + + @Bean + @Autowired + public ConceptFeatureIndexingSupport conceptFeatureIndexingSupport( + FeatureSupportRegistry aFeatureSupportRegistry, KnowledgeBaseService aKbService) + { + return new ConceptFeatureIndexingSupport(aFeatureSupportRegistry, aKbService); + } + + @Bean + public DefaultColoringStrategyImpl defaultColoringStrategy() + { + return new DefaultColoringStrategyImpl(); + } + + @Bean + public DescriptionColoringStrategyImpl descriptionColoringStrategy() + { + return new DescriptionColoringStrategyImpl(); + } + + @Bean + public LabelColoringStrategyImpl labelColoringStrategy() + { + return new LabelColoringStrategyImpl(); + } + + @Bean + public SubclassOfColoringStrategyImpl subclassOfColoringStrategy() + { + return new SubclassOfColoringStrategyImpl(); + } + + @Bean + public TypeColoringStrategyImpl typeColoringStrategy() + { + return new TypeColoringStrategyImpl(); + } + + @Bean + public StatementColoringRegistry statementColoringRegistry( + @Lazy @Autowired(required = false) List + aStatementColoringStrategies) + { + return new StatementColoringRegistryImpl(aStatementColoringStrategies); + } + + @Bean + public BooleanLiteralValueSupport booleanLiteralValueSupport() + { + return new BooleanLiteralValueSupport(); + } + + @Bean + public NumericLiteralValueSupport numericLiteralValueSupport() + { + return new NumericLiteralValueSupport(); + } + + @Bean + public StringLiteralValueSupport stringLiteralValueSupport() + { + return new StringLiteralValueSupport(); + } + + @Bean + public IriValueSupport iriValueSupport() + { + return new IriValueSupport(); + } + + @Bean + public ValueTypeSupportRegistry valueTypeSupportRegistry( + @Lazy @Autowired(required = false) List aValueTypeSupports) + { + return new ValueTypeSupportRegistryImpl(aValueTypeSupports); + } + + @Bean + @Autowired + public KnowledgeBasePageMenuItem knowledgeBasePageMenuItem(UserDao aUserRepo, + ProjectService aProjectService, KnowledgeBaseService aKbService) + { + return new KnowledgeBasePageMenuItem(aUserRepo, aProjectService, aKbService); + } +} diff --git a/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/feature/ConceptFeatureSupport.java b/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/feature/ConceptFeatureSupport.java index d1164dfd15c..6d9a041d7be 100644 --- a/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/feature/ConceptFeatureSupport.java +++ b/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/feature/ConceptFeatureSupport.java @@ -40,7 +40,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; import com.github.benmanes.caffeine.cache.Caffeine; import com.github.benmanes.caffeine.cache.LoadingCache; @@ -61,11 +60,15 @@ import de.tudarmstadt.ukp.inception.kb.graph.KBErrorHandle; import de.tudarmstadt.ukp.inception.kb.graph.KBHandle; import de.tudarmstadt.ukp.inception.kb.graph.KBObject; +import de.tudarmstadt.ukp.inception.ui.kb.config.KnowledgeBaseServiceUIAutoConfiguration; /** * Extension providing knowledge-base-related features for annotations. + *

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

    */ -@Component public class ConceptFeatureSupport implements FeatureSupport { diff --git a/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/feature/FactLinkingService.java b/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/feature/FactLinkingService.java index a60809acb8d..a3e6f5b2933 100644 --- a/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/feature/FactLinkingService.java +++ b/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/feature/FactLinkingService.java @@ -30,8 +30,6 @@ public interface FactLinkingService { - String SERVICE_NAME = "factLinkingService"; - List listProperties(Project aProject, ConceptFeatureTraits traits); KBHandle getKBHandleFromCasByAddr(CAS aCas, int targetAddr, Project aProject, diff --git a/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/feature/FactLinkingServiceImpl.java b/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/feature/FactLinkingServiceImpl.java index 0e00a0f8e0e..97a9bdcb0f4 100644 --- a/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/feature/FactLinkingServiceImpl.java +++ b/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/feature/FactLinkingServiceImpl.java @@ -30,7 +30,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; import de.tudarmstadt.ukp.clarin.webanno.api.AnnotationSchemaService; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.feature.FeatureSupport; @@ -46,9 +45,16 @@ import de.tudarmstadt.ukp.inception.kb.graph.KBInstance; import de.tudarmstadt.ukp.inception.kb.graph.KBProperty; import de.tudarmstadt.ukp.inception.kb.model.KnowledgeBase; +import de.tudarmstadt.ukp.inception.ui.kb.config.KnowledgeBaseServiceUIAutoConfiguration; -@Component(FactLinkingService.SERVICE_NAME) -public class FactLinkingServiceImpl implements FactLinkingService +/** + *

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

    + */ +public class FactLinkingServiceImpl + implements FactLinkingService { @Autowired private KnowledgeBaseService kbService; @Autowired private AnnotationSchemaService annotationService; diff --git a/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/feature/PropertyFeatureSupport.java b/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/feature/PropertyFeatureSupport.java index 2a9d716ac42..6afe4be7c8f 100644 --- a/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/feature/PropertyFeatureSupport.java +++ b/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/feature/PropertyFeatureSupport.java @@ -33,7 +33,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; import com.github.benmanes.caffeine.cache.Caffeine; import com.github.benmanes.caffeine.cache.LoadingCache; @@ -48,8 +47,14 @@ import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; import de.tudarmstadt.ukp.inception.kb.KnowledgeBaseService; import de.tudarmstadt.ukp.inception.kb.graph.KBProperty; +import de.tudarmstadt.ukp.inception.ui.kb.config.KnowledgeBaseServiceUIAutoConfiguration; -@Component +/** + *

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

    + */ public class PropertyFeatureSupport implements FeatureSupport { diff --git a/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/feature/SubjectObjectFeatureSupport.java b/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/feature/SubjectObjectFeatureSupport.java index b204ad773d6..fa9c07571d9 100644 --- a/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/feature/SubjectObjectFeatureSupport.java +++ b/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/feature/SubjectObjectFeatureSupport.java @@ -38,7 +38,6 @@ import org.apache.wicket.model.IModel; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; -import org.springframework.stereotype.Component; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.action.AnnotationActionHandler; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.feature.FeatureSupport; @@ -50,12 +49,16 @@ import de.tudarmstadt.ukp.clarin.webanno.api.annotation.util.WebAnnoCasUtil; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationFeature; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; +import de.tudarmstadt.ukp.inception.ui.kb.config.KnowledgeBaseServiceUIAutoConfiguration; /** * To create feature support for subject and object of the fact layer + *

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

    */ @Order(Ordered.HIGHEST_PRECEDENCE) -@Component public class SubjectObjectFeatureSupport implements FeatureSupport { diff --git a/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/initializers/FactLayerInitializer.java b/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/initializers/FactLayerInitializer.java index e76e56533a1..48c5f1b858f 100644 --- a/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/initializers/FactLayerInitializer.java +++ b/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/initializers/FactLayerInitializer.java @@ -25,7 +25,6 @@ import java.util.List; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; import de.tudarmstadt.ukp.clarin.webanno.api.AnnotationSchemaService; import de.tudarmstadt.ukp.clarin.webanno.api.dao.initializers.LayerInitializer; @@ -38,10 +37,16 @@ import de.tudarmstadt.ukp.clarin.webanno.model.OverlapMode; import de.tudarmstadt.ukp.clarin.webanno.model.Project; import de.tudarmstadt.ukp.dkpro.core.api.ner.type.NamedEntity; +import de.tudarmstadt.ukp.inception.ui.kb.config.KnowledgeBaseServiceUIAutoConfiguration; import de.tudarmstadt.ukp.inception.ui.kb.feature.FactLinkingConstants; import de.tudarmstadt.ukp.inception.ui.kb.feature.PropertyFeatureSupport; -@Component +/** + *

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

    + */ public class FactLayerInitializer implements LayerInitializer { diff --git a/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/initializers/NamedEntityIdentifierFeatureInitializer.java b/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/initializers/NamedEntityIdentifierFeatureInitializer.java index bb6cb1586a5..c0b3ec2b4ee 100644 --- a/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/initializers/NamedEntityIdentifierFeatureInitializer.java +++ b/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/initializers/NamedEntityIdentifierFeatureInitializer.java @@ -23,7 +23,6 @@ import java.util.List; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; import de.tudarmstadt.ukp.clarin.webanno.api.AnnotationSchemaService; import de.tudarmstadt.ukp.clarin.webanno.api.dao.initializers.LayerInitializer; @@ -34,12 +33,16 @@ import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; import de.tudarmstadt.ukp.clarin.webanno.model.Project; import de.tudarmstadt.ukp.dkpro.core.api.ner.type.NamedEntity; +import de.tudarmstadt.ukp.inception.ui.kb.config.KnowledgeBaseServiceUIAutoConfiguration; import de.tudarmstadt.ukp.inception.ui.kb.feature.ConceptFeatureSupport; /** * Adds the {@code identifier} feature provided since DKPro Core 1.9.0 as a concept feature. + *

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

    */ -@Component public class NamedEntityIdentifierFeatureInitializer implements LayerInitializer { diff --git a/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/project/KnowledgeBaseProjectSettingsPanelFactory.java b/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/project/KnowledgeBaseProjectSettingsPanelFactory.java index a73ba1693c9..3b1ab1dc1a0 100644 --- a/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/project/KnowledgeBaseProjectSettingsPanelFactory.java +++ b/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/project/KnowledgeBaseProjectSettingsPanelFactory.java @@ -20,12 +20,17 @@ import org.apache.wicket.markup.html.panel.Panel; import org.apache.wicket.model.IModel; import org.springframework.core.annotation.Order; -import org.springframework.stereotype.Component; import de.tudarmstadt.ukp.clarin.webanno.model.Project; import de.tudarmstadt.ukp.clarin.webanno.ui.core.settings.ProjectSettingsPanelFactory; +import de.tudarmstadt.ukp.inception.ui.kb.config.KnowledgeBaseServiceUIAutoConfiguration; -@Component +/** + *

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

    + */ @Order(350) public class KnowledgeBaseProjectSettingsPanelFactory implements ProjectSettingsPanelFactory diff --git a/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/search/ConceptFeatureIndexingSupport.java b/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/search/ConceptFeatureIndexingSupport.java index 68b53ace890..72a12f33948 100644 --- a/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/search/ConceptFeatureIndexingSupport.java +++ b/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/search/ConceptFeatureIndexingSupport.java @@ -25,10 +25,7 @@ import org.apache.commons.collections4.MultiValuedMap; import org.apache.commons.collections4.multimap.HashSetValuedHashMap; import org.apache.uima.cas.text.AnnotationFS; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.feature.FeatureSupport; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.feature.FeatureSupportRegistry; @@ -38,13 +35,17 @@ import de.tudarmstadt.ukp.inception.kb.graph.KBHandle; import de.tudarmstadt.ukp.inception.kb.graph.KBObject; import de.tudarmstadt.ukp.inception.search.FeatureIndexingSupport; +import de.tudarmstadt.ukp.inception.ui.kb.config.KnowledgeBaseServiceUIAutoConfiguration; -@Component +/** + *

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

    + */ public class ConceptFeatureIndexingSupport implements FeatureIndexingSupport { - private final Logger log = LoggerFactory.getLogger(getClass()); - public static final String KB_ENTITY = "KB" + SPECIAL_SEP + "Entity"; public static final String INDEX_KB_CONCEPT = "class"; public static final String INDEX_KB_INSTANCE = "instance"; diff --git a/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/stmt/coloring/DefaultColoringStrategyImpl.java b/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/stmt/coloring/DefaultColoringStrategyImpl.java index 0a8a4e9e355..d95e6298d6b 100644 --- a/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/stmt/coloring/DefaultColoringStrategyImpl.java +++ b/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/stmt/coloring/DefaultColoringStrategyImpl.java @@ -19,11 +19,15 @@ import java.util.List; -import org.springframework.stereotype.Component; - import de.tudarmstadt.ukp.inception.kb.model.KnowledgeBase; +import de.tudarmstadt.ukp.inception.ui.kb.config.KnowledgeBaseServiceUIAutoConfiguration; -@Component +/** + *

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

    + */ public class DefaultColoringStrategyImpl implements StatementColoringStrategy { diff --git a/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/stmt/coloring/DescriptionColoringStrategyImpl.java b/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/stmt/coloring/DescriptionColoringStrategyImpl.java index c811b657625..d2da2730bc4 100644 --- a/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/stmt/coloring/DescriptionColoringStrategyImpl.java +++ b/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/stmt/coloring/DescriptionColoringStrategyImpl.java @@ -19,11 +19,15 @@ import java.util.List; -import org.springframework.stereotype.Component; - import de.tudarmstadt.ukp.inception.kb.model.KnowledgeBase; +import de.tudarmstadt.ukp.inception.ui.kb.config.KnowledgeBaseServiceUIAutoConfiguration; -@Component +/** + *

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

    + */ public class DescriptionColoringStrategyImpl implements StatementColoringStrategy { diff --git a/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/stmt/coloring/LabelColoringStrategyImpl.java b/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/stmt/coloring/LabelColoringStrategyImpl.java index 705961cf5ca..5f6e9bf733e 100644 --- a/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/stmt/coloring/LabelColoringStrategyImpl.java +++ b/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/stmt/coloring/LabelColoringStrategyImpl.java @@ -19,11 +19,15 @@ import java.util.List; -import org.springframework.stereotype.Component; - import de.tudarmstadt.ukp.inception.kb.model.KnowledgeBase; +import de.tudarmstadt.ukp.inception.ui.kb.config.KnowledgeBaseServiceUIAutoConfiguration; -@Component +/** + *

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

    + */ public class LabelColoringStrategyImpl implements StatementColoringStrategy { diff --git a/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/stmt/coloring/StatementColoringRegistryImpl.java b/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/stmt/coloring/StatementColoringRegistryImpl.java index 776cb15c2dd..8a242e565d9 100644 --- a/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/stmt/coloring/StatementColoringRegistryImpl.java +++ b/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/stmt/coloring/StatementColoringRegistryImpl.java @@ -29,12 +29,18 @@ import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.context.event.EventListener; import org.springframework.core.annotation.AnnotationAwareOrderComparator; -import org.springframework.stereotype.Component; import de.tudarmstadt.ukp.inception.kb.model.KnowledgeBase; +import de.tudarmstadt.ukp.inception.ui.kb.config.KnowledgeBaseServiceUIAutoConfiguration; -@Component -public class StatementColoringRegistryImpl implements StatementColoringRegistry +/** + *

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

    + */ +public class StatementColoringRegistryImpl + implements StatementColoringRegistry { private final Logger log = LoggerFactory.getLogger(getClass()); diff --git a/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/stmt/coloring/SubclassOfColoringStrategyImpl.java b/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/stmt/coloring/SubclassOfColoringStrategyImpl.java index 12387143e89..95127521994 100644 --- a/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/stmt/coloring/SubclassOfColoringStrategyImpl.java +++ b/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/stmt/coloring/SubclassOfColoringStrategyImpl.java @@ -19,11 +19,15 @@ import java.util.List; -import org.springframework.stereotype.Component; - import de.tudarmstadt.ukp.inception.kb.model.KnowledgeBase; +import de.tudarmstadt.ukp.inception.ui.kb.config.KnowledgeBaseServiceUIAutoConfiguration; -@Component +/** + *

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

    + */ public class SubclassOfColoringStrategyImpl implements StatementColoringStrategy { diff --git a/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/stmt/coloring/TypeColoringStrategyImpl.java b/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/stmt/coloring/TypeColoringStrategyImpl.java index 3d64384d285..04010cfb081 100644 --- a/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/stmt/coloring/TypeColoringStrategyImpl.java +++ b/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/stmt/coloring/TypeColoringStrategyImpl.java @@ -19,11 +19,15 @@ import java.util.List; -import org.springframework.stereotype.Component; - import de.tudarmstadt.ukp.inception.kb.model.KnowledgeBase; +import de.tudarmstadt.ukp.inception.ui.kb.config.KnowledgeBaseServiceUIAutoConfiguration; -@Component +/** + *

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

    + */ public class TypeColoringStrategyImpl implements StatementColoringStrategy { diff --git a/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/value/BooleanLiteralValueSupport.java b/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/value/BooleanLiteralValueSupport.java index d419e0d3ba1..b9ddfacb4e8 100644 --- a/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/value/BooleanLiteralValueSupport.java +++ b/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/value/BooleanLiteralValueSupport.java @@ -26,18 +26,23 @@ import org.cyberborean.rdfbeans.datatype.DefaultDatatypeMapper; import org.eclipse.rdf4j.model.IRI; import org.eclipse.rdf4j.model.vocabulary.XMLSchema; -import org.springframework.stereotype.Component; import de.tudarmstadt.ukp.inception.kb.graph.KBObject; import de.tudarmstadt.ukp.inception.kb.graph.KBProperty; import de.tudarmstadt.ukp.inception.kb.graph.KBStatement; import de.tudarmstadt.ukp.inception.kb.model.KnowledgeBase; +import de.tudarmstadt.ukp.inception.ui.kb.config.KnowledgeBaseServiceUIAutoConfiguration; import de.tudarmstadt.ukp.inception.ui.kb.value.editor.BooleanLiteralValueEditor; import de.tudarmstadt.ukp.inception.ui.kb.value.editor.BooleanLiteralValuePresenter; import de.tudarmstadt.ukp.inception.ui.kb.value.editor.ValueEditor; import de.tudarmstadt.ukp.inception.ui.kb.value.editor.ValuePresenter; -@Component +/** + *

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

    + */ public class BooleanLiteralValueSupport implements ValueTypeSupport { diff --git a/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/value/IriValueSupport.java b/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/value/IriValueSupport.java index 75923a5836a..ba5e5627f5e 100644 --- a/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/value/IriValueSupport.java +++ b/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/value/IriValueSupport.java @@ -25,18 +25,23 @@ import org.apache.wicket.model.IModel; import org.eclipse.rdf4j.model.IRI; import org.eclipse.rdf4j.model.vocabulary.XMLSchema; -import org.springframework.stereotype.Component; import de.tudarmstadt.ukp.inception.kb.graph.KBObject; import de.tudarmstadt.ukp.inception.kb.graph.KBProperty; import de.tudarmstadt.ukp.inception.kb.graph.KBStatement; import de.tudarmstadt.ukp.inception.kb.model.KnowledgeBase; +import de.tudarmstadt.ukp.inception.ui.kb.config.KnowledgeBaseServiceUIAutoConfiguration; import de.tudarmstadt.ukp.inception.ui.kb.value.editor.IRIValueEditor; import de.tudarmstadt.ukp.inception.ui.kb.value.editor.IRIValuePresenter; import de.tudarmstadt.ukp.inception.ui.kb.value.editor.ValueEditor; import de.tudarmstadt.ukp.inception.ui.kb.value.editor.ValuePresenter; -@Component +/** + *

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

    + */ public class IriValueSupport implements ValueTypeSupport { diff --git a/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/value/NumericLiteralValueSupport.java b/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/value/NumericLiteralValueSupport.java index 29290d6fa75..48fd62ae348 100644 --- a/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/value/NumericLiteralValueSupport.java +++ b/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/value/NumericLiteralValueSupport.java @@ -41,18 +41,23 @@ import org.eclipse.rdf4j.model.IRI; import org.eclipse.rdf4j.model.impl.SimpleValueFactory; import org.eclipse.rdf4j.model.vocabulary.XMLSchema; -import org.springframework.stereotype.Component; import de.tudarmstadt.ukp.inception.kb.graph.KBObject; import de.tudarmstadt.ukp.inception.kb.graph.KBProperty; import de.tudarmstadt.ukp.inception.kb.graph.KBStatement; import de.tudarmstadt.ukp.inception.kb.model.KnowledgeBase; +import de.tudarmstadt.ukp.inception.ui.kb.config.KnowledgeBaseServiceUIAutoConfiguration; import de.tudarmstadt.ukp.inception.ui.kb.value.editor.NumericLiteralValueEditor; import de.tudarmstadt.ukp.inception.ui.kb.value.editor.NumericLiteralValuePresenter; import de.tudarmstadt.ukp.inception.ui.kb.value.editor.ValueEditor; import de.tudarmstadt.ukp.inception.ui.kb.value.editor.ValuePresenter; -@Component +/** + *

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

    + */ public class NumericLiteralValueSupport implements ValueTypeSupport { diff --git a/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/value/StringLiteralValueSupport.java b/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/value/StringLiteralValueSupport.java index c837051f258..feec1951b21 100644 --- a/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/value/StringLiteralValueSupport.java +++ b/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/value/StringLiteralValueSupport.java @@ -26,18 +26,23 @@ import org.cyberborean.rdfbeans.datatype.DefaultDatatypeMapper; import org.eclipse.rdf4j.model.IRI; import org.eclipse.rdf4j.model.vocabulary.XMLSchema; -import org.springframework.stereotype.Component; import de.tudarmstadt.ukp.inception.kb.graph.KBObject; import de.tudarmstadt.ukp.inception.kb.graph.KBProperty; import de.tudarmstadt.ukp.inception.kb.graph.KBStatement; import de.tudarmstadt.ukp.inception.kb.model.KnowledgeBase; +import de.tudarmstadt.ukp.inception.ui.kb.config.KnowledgeBaseServiceUIAutoConfiguration; import de.tudarmstadt.ukp.inception.ui.kb.value.editor.StringLiteralValueEditor; import de.tudarmstadt.ukp.inception.ui.kb.value.editor.StringLiteralValuePresenter; import de.tudarmstadt.ukp.inception.ui.kb.value.editor.ValueEditor; import de.tudarmstadt.ukp.inception.ui.kb.value.editor.ValuePresenter; -@Component +/** + *

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

    + */ public class StringLiteralValueSupport implements ValueTypeSupport { diff --git a/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/value/ValueTypeSupportRegistryImpl.java b/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/value/ValueTypeSupportRegistryImpl.java index da6d17c40fb..6ae673fe6ce 100644 --- a/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/value/ValueTypeSupportRegistryImpl.java +++ b/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/value/ValueTypeSupportRegistryImpl.java @@ -34,12 +34,17 @@ import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.context.event.EventListener; import org.springframework.core.annotation.AnnotationAwareOrderComparator; -import org.springframework.stereotype.Component; import de.tudarmstadt.ukp.inception.kb.graph.KBProperty; import de.tudarmstadt.ukp.inception.kb.graph.KBStatement; +import de.tudarmstadt.ukp.inception.ui.kb.config.KnowledgeBaseServiceUIAutoConfiguration; -@Component +/** + *

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

    + */ public class ValueTypeSupportRegistryImpl implements ValueTypeSupportRegistry { diff --git a/inception-ui-kb/src/main/resources/META-INF/spring.factories b/inception-ui-kb/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000000..8b265e03e1a --- /dev/null +++ b/inception-ui-kb/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +de.tudarmstadt.ukp.inception.ui.kb.config.KnowledgeBaseServiceUIAutoConfiguration \ No newline at end of file From 9e611b1351dafe5d79ee140d80a6f819208cd5fd Mon Sep 17 00:00:00 2001 From: Richard Eckart de Castilho Date: Tue, 24 Sep 2019 23:06:26 +0200 Subject: [PATCH 034/453] #1294 - Add more feature config switches - Configure string matching recommender through Spring Boot auto configuration --- inception-imls-stringmatch/pom.xml | 9 +-- .../StringMatchingRecommenderFactory.java | 9 ++- ...gMatchingRecommenderAutoConfiguration.java | 77 +++++++++++++++++++ .../exporter/GazeteerExporter.java | 9 ++- .../gazeteer/GazeteerServiceImpl.java | 20 +++-- ...gMatchingNerClassificationToolFactory.java | 9 ++- ...gMatchingPosClassificationToolFactory.java | 9 ++- .../main/resources/META-INF/spring.factories | 2 + 8 files changed, 120 insertions(+), 24 deletions(-) create mode 100644 inception-imls-stringmatch/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/stringmatch/config/StringMatchingRecommenderAutoConfiguration.java create mode 100644 inception-imls-stringmatch/src/main/resources/META-INF/spring.factories diff --git a/inception-imls-stringmatch/pom.xml b/inception-imls-stringmatch/pom.xml index d8ce846d2e6..2d9a3cfec59 100644 --- a/inception-imls-stringmatch/pom.xml +++ b/inception-imls-stringmatch/pom.xml @@ -76,6 +76,10 @@ org.springframework.boot spring-boot
    + + org.springframework.boot + spring-boot-autoconfigure + org.apache.uima @@ -191,11 +195,6 @@ - - org.springframework.boot - spring-boot-autoconfigure - test - org.springframework.boot spring-boot-test-autoconfigure diff --git a/inception-imls-stringmatch/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/stringmatch/StringMatchingRecommenderFactory.java b/inception-imls-stringmatch/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/stringmatch/StringMatchingRecommenderFactory.java index d9dd138c3ad..dd1644f9902 100644 --- a/inception-imls-stringmatch/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/stringmatch/StringMatchingRecommenderFactory.java +++ b/inception-imls-stringmatch/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/stringmatch/StringMatchingRecommenderFactory.java @@ -24,17 +24,22 @@ import org.apache.uima.cas.CAS; import org.apache.wicket.model.IModel; -import org.springframework.stereotype.Component; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationFeature; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; 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.stringmatch.config.StringMatchingRecommenderAutoConfiguration; import de.tudarmstadt.ukp.inception.recommendation.imls.stringmatch.gazeteer.GazeteerService; import de.tudarmstadt.ukp.inception.recommendation.imls.stringmatch.settings.StringMatchingRecommenderTraitsEditor; -@Component +/** + *

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

    + */ public class StringMatchingRecommenderFactory extends RecommendationEngineFactoryImplBase { diff --git a/inception-imls-stringmatch/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/stringmatch/config/StringMatchingRecommenderAutoConfiguration.java b/inception-imls-stringmatch/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/stringmatch/config/StringMatchingRecommenderAutoConfiguration.java new file mode 100644 index 00000000000..43fa8bd85ae --- /dev/null +++ b/inception-imls-stringmatch/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/stringmatch/config/StringMatchingRecommenderAutoConfiguration.java @@ -0,0 +1,77 @@ +/* + * Copyright 2019 + * Ubiquitous Knowledge Processing (UKP) Lab + * Technische Universität Darmstadt + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.tudarmstadt.ukp.inception.recommendation.imls.stringmatch.config; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import de.tudarmstadt.ukp.clarin.webanno.api.RepositoryProperties; +import de.tudarmstadt.ukp.inception.recommendation.api.RecommendationService; +import de.tudarmstadt.ukp.inception.recommendation.imls.stringmatch.StringMatchingRecommenderFactory; +import de.tudarmstadt.ukp.inception.recommendation.imls.stringmatch.exporter.GazeteerExporter; +import de.tudarmstadt.ukp.inception.recommendation.imls.stringmatch.gazeteer.GazeteerService; +import de.tudarmstadt.ukp.inception.recommendation.imls.stringmatch.gazeteer.GazeteerServiceImpl; +import de.tudarmstadt.ukp.inception.recommendation.imls.stringmatch.ner.StringMatchingNerClassificationToolFactory; +import de.tudarmstadt.ukp.inception.recommendation.imls.stringmatch.pos.StringMatchingPosClassificationToolFactory; + +@Configuration +@ConditionalOnBean(RecommendationService.class) +public class StringMatchingRecommenderAutoConfiguration +{ + private @PersistenceContext EntityManager entityManager; + + @Bean + @Autowired + public GazeteerExporter gazeteerExporter(RecommendationService aRecommendationService, + GazeteerService aGazeteerService) + { + return new GazeteerExporter(aRecommendationService, aGazeteerService); + } + + @Bean + @Autowired + public GazeteerService gazeteerService(RepositoryProperties aRepositoryProperties) + { + return new GazeteerServiceImpl(aRepositoryProperties, entityManager); + } + + @Bean + public StringMatchingNerClassificationToolFactory stringMatchingNerClassificationToolFactory() + { + return new StringMatchingNerClassificationToolFactory(); + } + + @Bean + public StringMatchingPosClassificationToolFactory stringMatchingPosClassificationToolFactory() + { + return new StringMatchingPosClassificationToolFactory(); + } + + @Bean + @Autowired + public StringMatchingRecommenderFactory stringMatchingRecommenderFactory( + GazeteerService aGazeteerService) + { + return new StringMatchingRecommenderFactory(aGazeteerService); + } +} diff --git a/inception-imls-stringmatch/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/stringmatch/exporter/GazeteerExporter.java b/inception-imls-stringmatch/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/stringmatch/exporter/GazeteerExporter.java index 162595cf82d..4b7879cb721 100644 --- a/inception-imls-stringmatch/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/stringmatch/exporter/GazeteerExporter.java +++ b/inception-imls-stringmatch/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/stringmatch/exporter/GazeteerExporter.java @@ -30,7 +30,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; import de.tudarmstadt.ukp.clarin.webanno.api.export.ProjectExportRequest; import de.tudarmstadt.ukp.clarin.webanno.api.export.ProjectExporter; @@ -40,10 +39,16 @@ import de.tudarmstadt.ukp.inception.recommendation.api.RecommendationService; import de.tudarmstadt.ukp.inception.recommendation.api.model.Recommender; import de.tudarmstadt.ukp.inception.recommendation.exporter.RecommenderExporter; +import de.tudarmstadt.ukp.inception.recommendation.imls.stringmatch.config.StringMatchingRecommenderAutoConfiguration; import de.tudarmstadt.ukp.inception.recommendation.imls.stringmatch.gazeteer.GazeteerService; import de.tudarmstadt.ukp.inception.recommendation.imls.stringmatch.model.Gazeteer; -@Component +/** + *

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

    + */ public class GazeteerExporter implements ProjectExporter { diff --git a/inception-imls-stringmatch/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/stringmatch/gazeteer/GazeteerServiceImpl.java b/inception-imls-stringmatch/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/stringmatch/gazeteer/GazeteerServiceImpl.java index da6f04748ac..22f8d04daf8 100644 --- a/inception-imls-stringmatch/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/stringmatch/gazeteer/GazeteerServiceImpl.java +++ b/inception-imls-stringmatch/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/stringmatch/gazeteer/GazeteerServiceImpl.java @@ -30,7 +30,6 @@ import java.util.List; import javax.persistence.EntityManager; -import javax.persistence.PersistenceContext; import org.apache.commons.io.IOUtils; import org.apache.commons.io.LineIterator; @@ -39,36 +38,35 @@ import org.slf4j.LoggerFactory; import org.slf4j.MDC; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; import de.tudarmstadt.ukp.clarin.webanno.api.RepositoryProperties; import de.tudarmstadt.ukp.clarin.webanno.support.logging.Logging; import de.tudarmstadt.ukp.inception.recommendation.api.model.Recommender; +import de.tudarmstadt.ukp.inception.recommendation.imls.stringmatch.config.StringMatchingRecommenderAutoConfiguration; import de.tudarmstadt.ukp.inception.recommendation.imls.stringmatch.model.Gazeteer; import de.tudarmstadt.ukp.inception.recommendation.imls.stringmatch.model.GazeteerEntry; -@Component +/** + *

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

    + */ public class GazeteerServiceImpl implements GazeteerService { private final Logger log = LoggerFactory.getLogger(getClass()); - @PersistenceContext - private EntityManager entityManager; + private final EntityManager entityManager; private final RepositoryProperties repositoryProperties; @Autowired - public GazeteerServiceImpl(RepositoryProperties aRepositoryProperties) - { - repositoryProperties = aRepositoryProperties; - } - public GazeteerServiceImpl(RepositoryProperties aRepositoryProperties, EntityManager aEntityManager) { - this(aRepositoryProperties); + repositoryProperties = aRepositoryProperties; entityManager = aEntityManager; } diff --git a/inception-imls-stringmatch/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/stringmatch/ner/StringMatchingNerClassificationToolFactory.java b/inception-imls-stringmatch/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/stringmatch/ner/StringMatchingNerClassificationToolFactory.java index f037858f3fc..807a063b572 100644 --- a/inception-imls-stringmatch/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/stringmatch/ner/StringMatchingNerClassificationToolFactory.java +++ b/inception-imls-stringmatch/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/stringmatch/ner/StringMatchingNerClassificationToolFactory.java @@ -21,7 +21,6 @@ import static java.util.Arrays.asList; import org.apache.uima.cas.CAS; -import org.springframework.stereotype.Component; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationFeature; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; @@ -30,8 +29,14 @@ import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommendationEngineFactoryImplBase; import de.tudarmstadt.ukp.inception.recommendation.imls.stringmatch.StringMatchingRecommender; import de.tudarmstadt.ukp.inception.recommendation.imls.stringmatch.StringMatchingRecommenderTraits; +import de.tudarmstadt.ukp.inception.recommendation.imls.stringmatch.config.StringMatchingRecommenderAutoConfiguration; -@Component +/** + *

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

    + */ public class StringMatchingNerClassificationToolFactory extends RecommendationEngineFactoryImplBase { diff --git a/inception-imls-stringmatch/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/stringmatch/pos/StringMatchingPosClassificationToolFactory.java b/inception-imls-stringmatch/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/stringmatch/pos/StringMatchingPosClassificationToolFactory.java index baae15f94fe..58efa68551a 100644 --- a/inception-imls-stringmatch/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/stringmatch/pos/StringMatchingPosClassificationToolFactory.java +++ b/inception-imls-stringmatch/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/stringmatch/pos/StringMatchingPosClassificationToolFactory.java @@ -21,7 +21,6 @@ import static de.tudarmstadt.ukp.clarin.webanno.model.AnchoringMode.SINGLE_TOKEN; import org.apache.uima.cas.CAS; -import org.springframework.stereotype.Component; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationFeature; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; @@ -30,8 +29,14 @@ import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommendationEngineFactoryImplBase; import de.tudarmstadt.ukp.inception.recommendation.imls.stringmatch.StringMatchingRecommender; import de.tudarmstadt.ukp.inception.recommendation.imls.stringmatch.StringMatchingRecommenderTraits; +import de.tudarmstadt.ukp.inception.recommendation.imls.stringmatch.config.StringMatchingRecommenderAutoConfiguration; -@Component +/** + *

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

    + */ public class StringMatchingPosClassificationToolFactory extends RecommendationEngineFactoryImplBase { diff --git a/inception-imls-stringmatch/src/main/resources/META-INF/spring.factories b/inception-imls-stringmatch/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000000..0e86bc6c21d --- /dev/null +++ b/inception-imls-stringmatch/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +de.tudarmstadt.ukp.inception.recommendation.imls.stringmatch.config.StringMatchingRecommenderAutoConfiguration \ No newline at end of file From ec5ca7e44b91fed436f6560b210285d14acefd0f Mon Sep 17 00:00:00 2001 From: uwinch Date: Wed, 25 Sep 2019 11:45:43 +0200 Subject: [PATCH 035/453] #1256 Integrate curation into annotation page - fix merging into curation user doc when no anno cas exists yet - disable curation suggestions when on curationpage - disable curation sidebar when admin views others --- .../curation/CurationEditorExtension.java | 5 +++++ .../inception/curation/CurationServiceImpl.java | 15 +++++++++------ .../curation/sidebar/CurationSidebar.java | 8 ++++++-- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationEditorExtension.java b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationEditorExtension.java index 62eb2c54930..2d5021fd208 100644 --- a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationEditorExtension.java +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationEditorExtension.java @@ -58,6 +58,7 @@ import de.tudarmstadt.ukp.clarin.webanno.curation.casmerge.CasMergeOperationResult; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationFeature; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; +import de.tudarmstadt.ukp.clarin.webanno.model.Mode; import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.clarin.webanno.security.model.User; @@ -199,6 +200,10 @@ protected VID parse(VID aParamId) public void render(CAS aCas, AnnotatorState aState, VDocument aVdoc, int aWindowBeginOffset, int aWindowEndOffset) { + if (!aState.getMode().equals(Mode.ANNOTATION)) { + return; + } + String currentUser = aState.getUser().getUsername(); long projectId = aState.getProject().getId(); Optional> selectedUsers = curationService diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationServiceImpl.java b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationServiceImpl.java index 9fd97378d18..10688d2925c 100644 --- a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationServiceImpl.java +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/CurationServiceImpl.java @@ -40,13 +40,13 @@ import org.springframework.security.core.session.SessionInformation; import org.springframework.security.core.session.SessionRegistry; import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Transactional; import de.tudarmstadt.ukp.clarin.webanno.api.CasStorageService; import de.tudarmstadt.ukp.clarin.webanno.api.DocumentService; import de.tudarmstadt.ukp.clarin.webanno.api.ProjectService; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.model.AnnotatorState; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.model.AnnotatorStateUtils; +import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationDocument; import de.tudarmstadt.ukp.clarin.webanno.model.Project; import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.clarin.webanno.security.UserDao; @@ -174,8 +174,9 @@ public Optional retrieveCurationCAS(String aUser, long aProjectId, SourceDo } @Override - @Transactional - public void writeCurationCas(CAS aTargetCas, AnnotatorState aState, long aProjectId) { + public synchronized void writeCurationCas(CAS aTargetCas, AnnotatorState aState, + long aProjectId) + { SourceDocument doc = aState.getDocument(); String curatorName = getCurationState(aState.getUser().getUsername(), aProjectId) .getCurationName(); @@ -187,13 +188,15 @@ public void writeCurationCas(CAS aTargetCas, AnnotatorState aState, long aProjec else { curator = userRegistry.get(curatorName); } - documentService.writeAnnotationCas(aTargetCas, doc, curator, true); + AnnotationDocument annoDoc = documentService.createOrGetAnnotationDocument(doc, + curator); + documentService.writeAnnotationCas(aTargetCas, annoDoc, true); AnnotatorStateUtils.updateDocumentTimestampAfterWrite(aState, casStorageService.getCasTimestamp(doc, curatorName)); } catch (IOException e) { - log.warn(String.format("Could not write CAS for user %s and document %d", - curatorName, doc.getId())); + log.warn(String.format("Could not write CAS for user %s and document %d", curatorName, + doc.getId())); e.printStackTrace(); } } diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java index 09811dd9d81..dbd032ffd92 100644 --- a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java @@ -199,8 +199,12 @@ protected void onConfigure() { super.onConfigure(); AnnotatorState state = getModelObject(); - setEnabled(!documentService - .isAnnotationFinished(state.getDocument(), state.getUser())); + // check that document is not already finished + // and user is curating not just viewing doc as admin + User user = state.getUser(); + setEnabled((user.equals(userRepository.getCurrentUser()) || + user.getUsername().equals(CURATION_USER)) && + !documentService.isAnnotationFinished(state.getDocument(), user)); } private void merge(AjaxRequestTarget aTarget, Form aForm) From a796e1c65e84c4ac53110186b8e37c3e3e75a182 Mon Sep 17 00:00:00 2001 From: Richard Eckart de Castilho Date: Wed, 25 Sep 2019 18:53:13 +0200 Subject: [PATCH 036/453] #1294 - Add more feature config switches - Add fact linking autoconfig class to spring.factories - Move ConceptFeatureSupport from the fact linking autoconfig to the KB autoconfig --- .../ui/kb/config/FactLinkingAutoConfiguration.java | 8 -------- .../config/KnowledgeBaseServiceUIAutoConfiguration.java | 8 ++++++++ .../src/main/resources/META-INF/spring.factories | 3 ++- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/config/FactLinkingAutoConfiguration.java b/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/config/FactLinkingAutoConfiguration.java index aee009765c1..9543258e0d8 100644 --- a/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/config/FactLinkingAutoConfiguration.java +++ b/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/config/FactLinkingAutoConfiguration.java @@ -27,7 +27,6 @@ import de.tudarmstadt.ukp.clarin.webanno.api.AnnotationSchemaService; import de.tudarmstadt.ukp.inception.kb.KnowledgeBaseService; import de.tudarmstadt.ukp.inception.kb.config.KnowledgeBaseServiceAutoConfiguration; -import de.tudarmstadt.ukp.inception.ui.kb.feature.ConceptFeatureSupport; import de.tudarmstadt.ukp.inception.ui.kb.feature.FactLinkingService; import de.tudarmstadt.ukp.inception.ui.kb.feature.FactLinkingServiceImpl; import de.tudarmstadt.ukp.inception.ui.kb.feature.PropertyFeatureSupport; @@ -41,13 +40,6 @@ havingValue = "true", matchIfMissing = false) public class FactLinkingAutoConfiguration { - @Bean - @Autowired - public ConceptFeatureSupport conceptFeatureSupport(KnowledgeBaseService aKbService) - { - return new ConceptFeatureSupport(aKbService); - } - @Bean @Autowired public PropertyFeatureSupport propertyFeatureSupport(KnowledgeBaseService aKbService) diff --git a/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/config/KnowledgeBaseServiceUIAutoConfiguration.java b/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/config/KnowledgeBaseServiceUIAutoConfiguration.java index f2949011b24..7b8078b6f0d 100644 --- a/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/config/KnowledgeBaseServiceUIAutoConfiguration.java +++ b/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/config/KnowledgeBaseServiceUIAutoConfiguration.java @@ -33,6 +33,7 @@ import de.tudarmstadt.ukp.inception.kb.KnowledgeBaseService; import de.tudarmstadt.ukp.inception.kb.config.KnowledgeBaseServiceAutoConfiguration; import de.tudarmstadt.ukp.inception.ui.kb.KnowledgeBasePageMenuItem; +import de.tudarmstadt.ukp.inception.ui.kb.feature.ConceptFeatureSupport; import de.tudarmstadt.ukp.inception.ui.kb.initializers.NamedEntityIdentifierFeatureInitializer; import de.tudarmstadt.ukp.inception.ui.kb.project.KnowledgeBaseProjectSettingsPanelFactory; import de.tudarmstadt.ukp.inception.ui.kb.search.ConceptFeatureIndexingSupport; @@ -71,6 +72,13 @@ public KnowledgeBaseProjectSettingsPanelFactory knowledgeBaseProjectSettingsPane return new KnowledgeBaseProjectSettingsPanelFactory(); } + @Bean + @Autowired + public ConceptFeatureSupport conceptFeatureSupport(KnowledgeBaseService aKbService) + { + return new ConceptFeatureSupport(aKbService); + } + @Bean @Autowired public ConceptFeatureIndexingSupport conceptFeatureIndexingSupport( diff --git a/inception-ui-kb/src/main/resources/META-INF/spring.factories b/inception-ui-kb/src/main/resources/META-INF/spring.factories index 8b265e03e1a..14cb51853ea 100644 --- a/inception-ui-kb/src/main/resources/META-INF/spring.factories +++ b/inception-ui-kb/src/main/resources/META-INF/spring.factories @@ -1,2 +1,3 @@ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ -de.tudarmstadt.ukp.inception.ui.kb.config.KnowledgeBaseServiceUIAutoConfiguration \ No newline at end of file +de.tudarmstadt.ukp.inception.ui.kb.config.KnowledgeBaseServiceUIAutoConfiguration,\ +de.tudarmstadt.ukp.inception.ui.kb.config.FactLinkingAutoConfiguration \ No newline at end of file From 1cb73d735eb1851c4661814cbe5953ad97575e94 Mon Sep 17 00:00:00 2001 From: Richard Eckart de Castilho Date: Wed, 25 Sep 2019 19:27:35 +0200 Subject: [PATCH 037/453] #1294 - Add more feature config switches - Configure entity linking after recommender and KB --- inception-concept-linking/pom.xml | 4 ++++ .../config/EntityLinkingServiceAutoConfiguration.java | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/inception-concept-linking/pom.xml b/inception-concept-linking/pom.xml index 9cc443c9602..b676e250591 100644 --- a/inception-concept-linking/pom.xml +++ b/inception-concept-linking/pom.xml @@ -69,6 +69,10 @@ de.tudarmstadt.ukp.inception.app inception-recommendation-api
    + + de.tudarmstadt.ukp.inception.app + inception-recommendation + de.tudarmstadt.ukp.clarin.webanno diff --git a/inception-concept-linking/src/main/java/de/tudarmstadt/ukp/inception/conceptlinking/config/EntityLinkingServiceAutoConfiguration.java b/inception-concept-linking/src/main/java/de/tudarmstadt/ukp/inception/conceptlinking/config/EntityLinkingServiceAutoConfiguration.java index 42b9088d135..16a8a9694b8 100644 --- a/inception-concept-linking/src/main/java/de/tudarmstadt/ukp/inception/conceptlinking/config/EntityLinkingServiceAutoConfiguration.java +++ b/inception-concept-linking/src/main/java/de/tudarmstadt/ukp/inception/conceptlinking/config/EntityLinkingServiceAutoConfiguration.java @@ -20,6 +20,7 @@ import java.util.List; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -38,9 +39,13 @@ import de.tudarmstadt.ukp.inception.conceptlinking.service.ConceptLinkingService; import de.tudarmstadt.ukp.inception.conceptlinking.service.ConceptLinkingServiceImpl; import de.tudarmstadt.ukp.inception.kb.KnowledgeBaseService; +import de.tudarmstadt.ukp.inception.kb.config.KnowledgeBaseServiceAutoConfiguration; import de.tudarmstadt.ukp.inception.recommendation.api.RecommendationService; +import de.tudarmstadt.ukp.inception.recommendation.config.RecommenderServiceAutoConfiguration; @Configuration +@AutoConfigureAfter({ KnowledgeBaseServiceAutoConfiguration.class, + RecommenderServiceAutoConfiguration.class }) @ConditionalOnBean(KnowledgeBaseService.class) @ConditionalOnProperty(prefix = "knowledge-base.entity-linking", name = "enabled", havingValue = "true", matchIfMissing = true) From 01bc6578aa3a4bbda7f04f4466e69563b88ae7e0 Mon Sep 17 00:00:00 2001 From: Sibgha360 Date: Wed, 25 Sep 2019 21:55:10 +0200 Subject: [PATCH 038/453] #1270 - Hints for new users - hint.js added. Some hints shown on the UI. --- .../config/InceptionResourcesBehavior.java | 3 + .../app/css/HintJavascriptReference.java | 28 +++ .../tudarmstadt/ukp/inception/app/css/hint.js | 167 ++++++++++++++++++ inception-ui-core/pom.xml | 6 + .../inception/ui/core/menubar/MenuBar.html | 6 + .../inception/ui/core/menubar/MenuBar.java | 28 +++ .../ui/core/menubar/MenuBar.properties | 1 + 7 files changed, 239 insertions(+) create mode 100644 inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/app/css/HintJavascriptReference.java create mode 100644 inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/app/css/hint.js diff --git a/inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/app/config/InceptionResourcesBehavior.java b/inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/app/config/InceptionResourcesBehavior.java index 8dd56e11160..93b3df9b39a 100644 --- a/inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/app/config/InceptionResourcesBehavior.java +++ b/inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/app/config/InceptionResourcesBehavior.java @@ -27,6 +27,7 @@ import de.agilecoders.wicket.webjars.request.resource.WebjarsJavaScriptResourceReference; import de.tudarmstadt.ukp.clarin.webanno.ui.core.page.WebAnnoJavascriptReference; import de.tudarmstadt.ukp.inception.app.css.InceptionCssReference; +import de.tudarmstadt.ukp.inception.app.css.HintJavascriptReference; public class InceptionResourcesBehavior extends Behavior @@ -50,6 +51,8 @@ public void renderHead(Component aComponent, IHeaderResponse aResponse) aResponse.render(CssHeaderItem.forReference(new WebjarsCssResourceReference("enjoyhint/current/jquery.enjoyhint.css"))); aResponse.render(JavaScriptHeaderItem.forReference(new WebjarsJavaScriptResourceReference("jquery.scrollTo/current/jquery.scrollTo.js"))); aResponse.render(JavaScriptHeaderItem.forReference(new WebjarsJavaScriptResourceReference("kinetic/current/kinetic.min.js"))); + + aResponse.render(JavaScriptHeaderItem.forReference(HintJavascriptReference.get())); } public static InceptionResourcesBehavior get() diff --git a/inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/app/css/HintJavascriptReference.java b/inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/app/css/HintJavascriptReference.java new file mode 100644 index 00000000000..0fcaadf0d6a --- /dev/null +++ b/inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/app/css/HintJavascriptReference.java @@ -0,0 +1,28 @@ +package de.tudarmstadt.ukp.inception.app.css; + +import org.apache.wicket.request.resource.JavaScriptResourceReference; + +public class HintJavascriptReference extends JavaScriptResourceReference +{ + private static final long serialVersionUID = 1L; + + private static final HintJavascriptReference INSTANCE = new HintJavascriptReference(); + + /** + * Gets the instance of the resource reference + * + * @return the single instance of the resource reference + */ + public static HintJavascriptReference get() + { + return INSTANCE; + } + + /** + * Private constructor + */ + private HintJavascriptReference() + { + super(HintJavascriptReference.class, "hint.js"); + } +} diff --git a/inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/app/css/hint.js b/inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/app/css/hint.js new file mode 100644 index 00000000000..ab231315c94 --- /dev/null +++ b/inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/app/css/hint.js @@ -0,0 +1,167 @@ +/* +#Copyright 2019 +#Ubiquitous Knowledge Processing (UKP) Lab +#Technische Universität Darmstadt +# +#Licensed under the Apache License, Version 2.0 (the "License"); +#you may not use this file except in compliance with the License. +#You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +#Unless required by applicable law or agreed to in writing, software +#distributed under the License is distributed on an "AS IS" BASIS, +#WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +#See the License for the specific language governing permissions and +#limitations under the License. +*/ + +$(document).ready(function(){ + + var enjoyhint_instance = new EnjoyHint({ + onSkip:function(){ +// reset all the cookies + } + + }); + + var enjoyhint_script_steps ; +var currentPage = window.location.pathname; +var cName = createCookieName(currentPage); + +if (currentPage == "/projects.html") { + + var ps = getCookie(cName); + if (ps != "") { + } else { + ps = true; + if (ps != "" && ps != null) { + setCookie(cName, ps, 365); + } + + enjoyhint_script_steps = createNewProjectRoutine(); + } +} else if (currentPage == "/project.html") { + var ps = getCookie(cName); + if (ps != "") { + } else { + ps = true; + if (ps != "" && ps != null) { + setCookie(cName, ps, 365); + } + + enjoyhint_script_steps = createDashboardRoutine(); + } +} else if (currentPage == "/projectsetting.html") { + var ps = getCookie(cName); + if (ps != "") { + //alert("PS Visited? " + ps); + } else { + ps = true; + if (ps != "" && ps != null) { + setCookie(cName, ps, 365); + } + enjoyhint_script_steps = createSettingsRoutine(); + } +} + + +enjoyhint_instance.set(enjoyhint_script_steps); +enjoyhint_instance.runScript(); + +}); + +function setCookie(cname, cvalue, exdays) { + var d = new Date(); + d.setTime(d.getTime() + (exdays * 24 * 60 * 60 * 1000)); + var expires = "expires=" + d.toUTCString(); + document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/"; +} + +function getCookie(cname) { + var name = cname + "="; + var ca = document.cookie.split(';'); + for (var i = 0; i < ca.length; i++) { + var c = ca[i]; + while (c.charAt(0) == ' ') { + c = c.substring(1); + } + if (c.indexOf(name) == 0) { + return c.substring(name.length, c.length); + } + } + return ""; +} + +function createNewProjectRoutine() { + var a = [ + { + 'next .navbar-brand' : 'Welcome to INCEpTION! Let me guide you through its features.', + 'nextButton' : { + className : "myNext", + text : "Sure" + }, + 'skipButton' : { + className : "mySkip", + text : "Nope!" + } + }, + { + 'next .btn btn-primary' : "Here is the button to create a new project", + }, + { + 'next .file-input-new' : "The projects can also be imported", + }, + { + 'next .input-group:last' : "You can filter the projects based on the ownership type", + }, + { + 'click .list-group-item' : "Click on the project to get started.", + + } ]; + return a; +} + +function createDashboardRoutine() { + var a = [ + { + 'next .nav-pills' : 'Lets explore the main features regarding the project', + 'nextButton' : { + className : "myNext", + text : "Okay!" + }, + 'skipButton' : { + className : "mySkip", + text : "Skip" + } + }, + { + 'click li:nth-last-child(1)' : "Before getting started, lets configure the project" + } ]; + return a; +} + +function createSettingsRoutine() { + var a = [ + { + 'click .tab1' : 'Click here to add a the document to the project', + }, + { + 'next [class=flex-h-container]' : "Upload a file and import. Then click Next.", + }, + { + 'click .tab5' : "Now, lets add a recommender. Click here!", + }, + { + 'click [value=Create]' : "Click to create a new recommender", + }, + { + 'next form:last' : "Fill in the details and save", + }]; + + return a; +} + +function createCookieName(currentPage) { + return currentPage.substr(1); +} diff --git a/inception-ui-core/pom.xml b/inception-ui-core/pom.xml index c9ae06c3478..81ba2c4d8f5 100644 --- a/inception-ui-core/pom.xml +++ b/inception-ui-core/pom.xml @@ -164,6 +164,12 @@ de.tudarmstadt.ukp.inception.app inception-support + + javax.servlet + servlet-api + 2.5 + provided + diff --git a/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/menubar/MenuBar.html b/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/menubar/MenuBar.html index bfd293c0cd3..859c5d0f9bf 100644 --- a/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/menubar/MenuBar.html +++ b/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/menubar/MenuBar.html @@ -86,6 +86,12 @@

    + + diff --git a/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/menubar/MenuBar.java b/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/menubar/MenuBar.java index a925bf21bfb..bcb017ce4e6 100644 --- a/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/menubar/MenuBar.java +++ b/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/menubar/MenuBar.java @@ -19,7 +19,12 @@ import static de.tudarmstadt.ukp.clarin.webanno.support.lambda.LambdaBehavior.visibleWhen; +import javax.servlet.http.Cookie; + import org.apache.wicket.Session; +import org.apache.wicket.request.cycle.RequestCycle; +import org.apache.wicket.request.http.WebRequest; +import org.apache.wicket.request.http.WebResponse; import org.apache.wicket.spring.injection.annot.SpringBean; import de.tudarmstadt.ukp.clarin.webanno.api.ProjectService; @@ -57,8 +62,31 @@ public MenuBar(String aId) add(new LambdaStatelessLink("adminLink", () -> setResponsePage(AdminDashboardPage.class)) .add(visibleWhen(this::adminAreaAccessRequired))); + + add(new LambdaStatelessLink("hintLink", () -> + showHint()) + .add(visibleWhen(this::adminAreaAccessRequired))); } + private Object showHint() + { + WebRequest webRequest = (WebRequest)RequestCycle.get().getRequest(); + WebResponse webResponse = (WebResponse)RequestCycle.get().getResponse(); + + // page name is used as cookie name + String cookieName = webRequest.getOriginalUrl().getSegments().get(0); + Cookie cookie = webRequest.getCookie(cookieName); + + if (cookie == null) { + return null; + } + webResponse.clearCookie(cookie); + + //refresh the page for the hint javascript to execute + setResponsePage(getPage()); + return null; + } + private boolean adminAreaAccessRequired() { return userRepository.getCurrentUser() != null && AdminDashboardPage diff --git a/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/menubar/MenuBar.properties b/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/menubar/MenuBar.properties index 8a4163f963e..4cbe79ca786 100644 --- a/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/menubar/MenuBar.properties +++ b/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/menubar/MenuBar.properties @@ -17,3 +17,4 @@ project.null=Choose a project administration=Administration projects=Projects dashboard=Dashboard +hint=Hint From 8e7ac6aa55dbb9d67fb208aff460a3b99420cdf2 Mon Sep 17 00:00:00 2001 From: Sibgha360 Date: Thu, 26 Sep 2019 06:34:28 +0200 Subject: [PATCH 039/453] #1270 - Hints for new users - unused import removed --- .../ukp/inception/app/config/InceptionResourcesBehavior.java | 1 - 1 file changed, 1 deletion(-) diff --git a/inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/app/config/InceptionResourcesBehavior.java b/inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/app/config/InceptionResourcesBehavior.java index 93b3df9b39a..3ad6f566547 100644 --- a/inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/app/config/InceptionResourcesBehavior.java +++ b/inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/app/config/InceptionResourcesBehavior.java @@ -27,7 +27,6 @@ import de.agilecoders.wicket.webjars.request.resource.WebjarsJavaScriptResourceReference; import de.tudarmstadt.ukp.clarin.webanno.ui.core.page.WebAnnoJavascriptReference; import de.tudarmstadt.ukp.inception.app.css.InceptionCssReference; -import de.tudarmstadt.ukp.inception.app.css.HintJavascriptReference; public class InceptionResourcesBehavior extends Behavior From 7789b22e2c6cbb945987f09dcebb9cc472265a0d Mon Sep 17 00:00:00 2001 From: Sibgha360 Date: Thu, 26 Sep 2019 06:52:58 +0200 Subject: [PATCH 040/453] #1270 - Hints for new users - Code fix --- .../inception/app/config/InceptionResourcesBehavior.java | 1 + .../ukp/inception/ui/core/menubar/MenuBar.java | 8 +++----- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/app/config/InceptionResourcesBehavior.java b/inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/app/config/InceptionResourcesBehavior.java index 3ad6f566547..d17a407b552 100644 --- a/inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/app/config/InceptionResourcesBehavior.java +++ b/inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/app/config/InceptionResourcesBehavior.java @@ -26,6 +26,7 @@ import de.agilecoders.wicket.webjars.request.resource.WebjarsCssResourceReference; import de.agilecoders.wicket.webjars.request.resource.WebjarsJavaScriptResourceReference; import de.tudarmstadt.ukp.clarin.webanno.ui.core.page.WebAnnoJavascriptReference; +import de.tudarmstadt.ukp.inception.app.css.HintJavascriptReference; import de.tudarmstadt.ukp.inception.app.css.InceptionCssReference; public class InceptionResourcesBehavior diff --git a/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/menubar/MenuBar.java b/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/menubar/MenuBar.java index a404b078bf3..fe4ab3a2abe 100644 --- a/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/menubar/MenuBar.java +++ b/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/menubar/MenuBar.java @@ -60,11 +60,9 @@ public MenuBar(String aId) add(new BookmarkablePageLink<>("adminLink", AdminDashboardPage.class) .add(visibleWhen(this::adminAreaAccessRequired))); - add(new LambdaStatelessLink("hintLink", () -> - showHint()) - .add(visibleWhen(this::adminAreaAccessRequired))); - -} + add(new LambdaStatelessLink("hintLink", () -> showHint()) + .add(visibleWhen(this::adminAreaAccessRequired))); + } private Object showHint() { From 8f50a296624a4e0cd6d761553c51bf6066bc4ecf Mon Sep 17 00:00:00 2001 From: uwinch Date: Fri, 27 Sep 2019 07:58:05 +0200 Subject: [PATCH 041/453] #1256 Integrate curation into annotation page - sidebar only for curators --- .../curation/sidebar/CurationSidebar.java | 2 +- .../sidebar/CurationSidebarFactory.java | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java index dbd032ffd92..c5719a872ba 100644 --- a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java @@ -94,7 +94,7 @@ public class CurationSidebar private AnnotationPage annoPage; - // TODO: only show to people who are curators + public CurationSidebar(String aId, IModel aModel, AnnotationActionHandler aActionHandler, CasProvider aCasProvider, AnnotationPage aAnnotationPage) diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebarFactory.java b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebarFactory.java index c4b81104ec3..96c6a844de4 100644 --- a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebarFactory.java +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebarFactory.java @@ -20,11 +20,15 @@ import org.apache.wicket.model.IModel; import org.apache.wicket.request.resource.PackageResourceReference; import org.apache.wicket.request.resource.ResourceReference; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import de.tudarmstadt.ukp.clarin.webanno.api.CasProvider; +import de.tudarmstadt.ukp.clarin.webanno.api.ProjectService; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.action.AnnotationActionHandler; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.model.AnnotatorState; +import de.tudarmstadt.ukp.clarin.webanno.model.Project; +import de.tudarmstadt.ukp.clarin.webanno.security.model.User; import de.tudarmstadt.ukp.clarin.webanno.ui.annotation.AnnotationPage; import de.tudarmstadt.ukp.clarin.webanno.ui.annotation.sidebar.AnnotationSidebarFactory_ImplBase; import de.tudarmstadt.ukp.clarin.webanno.ui.annotation.sidebar.AnnotationSidebar_ImplBase; @@ -37,6 +41,8 @@ public class CurationSidebarFactory private static final ResourceReference ICON = new PackageResourceReference( CurationSidebarFactory.class, "data_table.png"); + private @Autowired ProjectService projectService; + @Override public String getDisplayName() { @@ -58,4 +64,15 @@ public AnnotationSidebar_ImplBase create(String aId, IModel aMod aAnnotationPage); } + @Override + public boolean applies(AnnotatorState aState) + { + // FIXME at this point project is null, because it is set in annopage.handleparams + // which is executed after commoninit which creates the sidebar panel + Project project = aState.getProject(); + User user = aState.getUser(); + boolean isCurator = projectService.isCurator(aState.getProject(), aState.getUser()); + return isCurator; + } + } From cb2e62e2d57ccfbebf24c9190fe042876db746f2 Mon Sep 17 00:00:00 2001 From: uwinch Date: Fri, 27 Sep 2019 18:00:13 +0200 Subject: [PATCH 042/453] #1256 Integrate curation into annotation page - updating user selection fix - handling when no merge strategy selected --- .../ukp/inception/curation/sidebar/CurationSidebar.java | 8 ++++++++ .../curation/sidebar/CurationSidebarFactory.java | 6 ------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java index c5719a872ba..1d7de6d5e72 100644 --- a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java @@ -32,6 +32,7 @@ import org.apache.uima.cas.CAS; import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.ajax.markup.html.form.AjaxButton; +import org.apache.wicket.feedback.IFeedback; import org.apache.wicket.markup.html.WebMarkupContainer; import org.apache.wicket.markup.html.basic.Label; import org.apache.wicket.markup.html.form.Check; @@ -234,6 +235,11 @@ private void merge(AjaxRequestTarget aTarget, Form aForm) currentUsername, projectId, users); // update selected merge strategy MergeStrategy mergeStrat = ((MergeStrategy) mergeChoice.getDefaultModelObject()); + if (mergeStrat == null) { + aTarget.addChildren(getPage(), IFeedback.class); + error("Please choose a strategy for merging."); + return; + } curationService.updateMergeStrategy(currentUsername, projectId, mergeStrat); // merge cases try { @@ -251,6 +257,8 @@ private void merge(AjaxRequestTarget aTarget, Form aForm) curator.getUsername(), state.getProject().getId())); e.printStackTrace(); } + + aTarget.add(mainContainer); //open curation doc annoPage.actionLoadDocument(aTarget); } diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebarFactory.java b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebarFactory.java index 96c6a844de4..74f0a774101 100644 --- a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebarFactory.java +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebarFactory.java @@ -27,8 +27,6 @@ import de.tudarmstadt.ukp.clarin.webanno.api.ProjectService; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.action.AnnotationActionHandler; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.model.AnnotatorState; -import de.tudarmstadt.ukp.clarin.webanno.model.Project; -import de.tudarmstadt.ukp.clarin.webanno.security.model.User; import de.tudarmstadt.ukp.clarin.webanno.ui.annotation.AnnotationPage; import de.tudarmstadt.ukp.clarin.webanno.ui.annotation.sidebar.AnnotationSidebarFactory_ImplBase; import de.tudarmstadt.ukp.clarin.webanno.ui.annotation.sidebar.AnnotationSidebar_ImplBase; @@ -67,10 +65,6 @@ public AnnotationSidebar_ImplBase create(String aId, IModel aMod @Override public boolean applies(AnnotatorState aState) { - // FIXME at this point project is null, because it is set in annopage.handleparams - // which is executed after commoninit which creates the sidebar panel - Project project = aState.getProject(); - User user = aState.getUser(); boolean isCurator = projectService.isCurator(aState.getProject(), aState.getUser()); return isCurator; } From 6f900a65ccd289419b51318e9dcd936e9a842830 Mon Sep 17 00:00:00 2001 From: Sibgha360 Date: Sun, 29 Sep 2019 18:47:26 +0200 Subject: [PATCH 043/453] #1270 - Hints for new users - Added more flows in the hint.js --- .../config/InceptionResourcesBehavior.java | 8 +- .../app/css/EnjoyHintJsReference.java | 45 ++ .../ukp/inception/app/css/enjoyhint.js | 415 ++++++++++++++++++ .../tudarmstadt/ukp/inception/app/css/hint.js | 278 +++++++++--- 4 files changed, 672 insertions(+), 74 deletions(-) create mode 100644 inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/app/css/EnjoyHintJsReference.java create mode 100644 inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/app/css/enjoyhint.js diff --git a/inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/app/config/InceptionResourcesBehavior.java b/inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/app/config/InceptionResourcesBehavior.java index d17a407b552..dccd9075c23 100644 --- a/inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/app/config/InceptionResourcesBehavior.java +++ b/inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/app/config/InceptionResourcesBehavior.java @@ -26,6 +26,7 @@ import de.agilecoders.wicket.webjars.request.resource.WebjarsCssResourceReference; import de.agilecoders.wicket.webjars.request.resource.WebjarsJavaScriptResourceReference; import de.tudarmstadt.ukp.clarin.webanno.ui.core.page.WebAnnoJavascriptReference; +import de.tudarmstadt.ukp.inception.app.css.EnjoyHintJsReference; import de.tudarmstadt.ukp.inception.app.css.HintJavascriptReference; import de.tudarmstadt.ukp.inception.app.css.InceptionCssReference; @@ -42,10 +43,11 @@ public void renderHead(Component aComponent, IHeaderResponse aResponse) // Loading WebAnno CSS here so it can override JQuery/Kendo CSS aResponse.render(CssHeaderItem.forReference(InceptionCssReference.get())); aResponse.render(JavaScriptHeaderItem.forReference(WebAnnoJavascriptReference.get())); - + aResponse.render(JavaScriptHeaderItem.forReference(EnjoyHintJsReference.get())); + // Loading resources for the tour guide feature for the new users - aResponse.render(JavaScriptHeaderItem - .forReference(new WebjarsJavaScriptResourceReference("enjoyhint/current/enjoyhint.js"))); +// aResponse.render(JavaScriptHeaderItem +// .forReference(new WebjarsJavaScriptResourceReference("enjoyhint/current/enjoyhint.js"))); aResponse.render(JavaScriptHeaderItem .forReference(new WebjarsJavaScriptResourceReference("enjoyhint/current/jquery.enjoyhint.js"))); aResponse.render(CssHeaderItem.forReference(new WebjarsCssResourceReference("enjoyhint/current/jquery.enjoyhint.css"))); diff --git a/inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/app/css/EnjoyHintJsReference.java b/inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/app/css/EnjoyHintJsReference.java new file mode 100644 index 00000000000..517ba4c0cdf --- /dev/null +++ b/inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/app/css/EnjoyHintJsReference.java @@ -0,0 +1,45 @@ +/* + * Copyright 2015 + * Ubiquitous Knowledge Processing (UKP) Lab + * Technische Universität Darmstadt + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.tudarmstadt.ukp.inception.app.css; + +import org.apache.wicket.request.resource.JavaScriptResourceReference; + +public class EnjoyHintJsReference extends JavaScriptResourceReference +{ + private static final long serialVersionUID = 1L; + + private static final EnjoyHintJsReference INSTANCE = new EnjoyHintJsReference(); + + /** + * Gets the instance of the resource reference + * + * @return the single instance of the resource reference + */ + public static EnjoyHintJsReference get() + { + return INSTANCE; + } + + /** + * Private constructor + */ + private EnjoyHintJsReference() + { + super(EnjoyHintJsReference.class, "enjoyhint.js"); + } +} diff --git a/inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/app/css/enjoyhint.js b/inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/app/css/enjoyhint.js new file mode 100644 index 00000000000..f3c4d915bbd --- /dev/null +++ b/inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/app/css/enjoyhint.js @@ -0,0 +1,415 @@ +var EnjoyHint = function (_options) { + + var $event_element; + var that = this; + + var defaults = { + + onStart: function () { + + }, + + onEnd: function () { + + }, + + onSkip: function () { + + }, + + onNext: function () { + + } + }; + + var options = $.extend(defaults, _options); + var data = []; + var current_step = 0; + + $body = $('body'); + + + /********************* PRIVATE METHODS ***************************************/ + + var init = function () { + + if ($('.enjoyhint')) { + + $('.enjoyhint').remove(); + } + + $body.css({'overflow':'hidden'}); + + $(document).on("touchmove",lockTouch); + + $body.enjoyhint({ + + onNextClick: function () { + + nextStep(); + }, + + onSkipClick: function () { + + options.onSkip(); + skipAll(); + } + }); + }; + + var lockTouch = function(e) { + + e.preventDefault(); + }; + + var destroyEnjoy = function () { + + $('.enjoyhint').remove(); + $body.css({'overflow':'auto'}); + $(document).off("touchmove", lockTouch); + }; + + that.clear = function(){ + + var $nextBtn = $('.enjoyhint_next_btn'); + var $skipBtn = $('.enjoyhint_skip_btn'); + + $nextBtn.removeClass(that.nextUserClass); + $nextBtn.text("Next"); + $skipBtn.removeClass(that.skipUserClass); + $skipBtn.text("Skip"); + }; + + var stepAction = function () { + + if (!(data && data[current_step])) { + + $body.enjoyhint('hide'); + options.onEnd(); + destroyEnjoy(); + return; + } + + options.onNext(); + + var $enjoyhint = $('.enjoyhint'); + + $enjoyhint.removeClass("enjoyhint-step-" + current_step); + $enjoyhint.removeClass("enjoyhint-step-" + (current_step + 1)); + $enjoyhint.addClass("enjoyhint-step-" + (current_step + 1)); + + var step_data = data[current_step]; + + if (step_data.onBeforeStart && typeof step_data.onBeforeStart === 'function') { + + step_data.onBeforeStart(); + } + + var timeout = step_data.timeout || 0; + + setTimeout(function () { + + if (!step_data.selector) { + + for (var prop in step_data) { + + if (step_data.hasOwnProperty(prop) && prop.split(" ")[1]) { + + step_data.selector = prop.split(" ")[1]; + step_data.event = prop.split(" ")[0]; + + if (prop.split(" ")[0] == 'next' || prop.split(" ")[0] == 'auto' || prop.split(" ")[0] == 'custom') { + + step_data.event_type = prop.split(" ")[0]; + } + + step_data.description = step_data[prop]; + } + } + } + + setTimeout(function () { + + that.clear(); + }, 250); + + $(document.body).scrollTop(step_data.selector, step_data.scrollAnimationSpeed || 250, {offset: -100}); + + setTimeout(function () { + + var $element = $(step_data.selector); + var event = makeEventName(step_data.event); + + $body.enjoyhint('show'); + $body.enjoyhint('hide_next'); + $event_element = $element; + + if (step_data.event_selector) { + + $event_element = $(step_data.event_selector); + } + + if (!step_data.event_type && step_data.event == "key") { + + $element.keydown(function (event) { + + if (event.which == step_data.keyCode) { + + current_step++; + stepAction(); + } + }); + } + + if (step_data.showNext == true) { + + $body.enjoyhint('show_next'); + } + + if (step_data.showSkip == false) { + + $body.enjoyhint('hide_skip'); + } else { + + $body.enjoyhint('show_skip'); + } + + if (step_data.showSkip == true) { + + } + + if (step_data.nextButton) { + + var $nextBtn = $('.enjoyhint_next_btn'); + + $nextBtn.addClass(step_data.nextButton.className || ""); + $nextBtn.text(step_data.nextButton.text || "Next"); + that.nextUserClass = step_data.nextButton.className; + } + + if (step_data.skipButton) { + + var $skipBtn = $('.enjoyhint_skip_btn'); + + $skipBtn.addClass(step_data.skipButton.className || ""); + $skipBtn.text(step_data.skipButton.text || "Skip"); + that.skipUserClass = step_data.skipButton.className; + } + + if (step_data.event_type) { + + switch (step_data.event_type) { + + case 'auto': + + $element[step_data.event](); + + switch (step_data.event) { + + case 'click': + break; + } + + current_step++; + stepAction(); + + return; + break; + + case 'custom': + + on(step_data.event, function () { + + current_step++; + off(step_data.event); + stepAction(); + }); + + break; + + case 'next': + + $body.enjoyhint('show_next'); + break; + } + + } else { + + $event_element.on(event, function (e) { + + if (step_data.keyCode && e.keyCode != step_data.keyCode) { + + return; + } + + current_step++; + $(this).off(event); + + stepAction(); // clicked + }); + } + + var max_habarites = Math.max($element.outerWidth(), $element.outerHeight()); + var radius = step_data.radius || Math.round(max_habarites / 2) + 5; + var offset = $element.offset(); + var w = $element.outerWidth(); + var h = $element.outerHeight(); + var shape_margin = (step_data.margin !== undefined) ? step_data.margin : 10; + + var coords = { + x: offset.left + Math.round(w / 2), + y: offset.top + Math.round(h / 2) - $(document).scrollTop() + }; + + var shape_data = { + enjoyHintElementSelector: step_data.selector, + center_x: coords.x, + center_y: coords.y, + text: step_data.description, + top: step_data.top, + bottom: step_data.bottom, + left: step_data.left, + right: step_data.right, + margin: step_data.margin, + scroll: step_data.scroll + }; + + if (step_data.shape && step_data.shape == 'circle') { + + shape_data.shape = 'circle'; + shape_data.radius = radius; + } else { + + shape_data.radius = 0; + shape_data.width = w + shape_margin; + shape_data.height = h + shape_margin; + } + + $body.enjoyhint('render_label_with_shape', shape_data, that.stop); + }, step_data.scrollAnimationSpeed + 20 || 270); + }, timeout); + }; + + var nextStep = function() { + + current_step++; + stepAction(); + }; + + var skipAll = function() { + + var step_data = data[current_step]; + var $element = $(step_data.selector); + + off(step_data.event); + $element.off(makeEventName(step_data.event)); + + destroyEnjoy(); + }; + + var makeEventName = function (name, is_custom) { + + return name + (is_custom ? 'custom' : '') + '.enjoy_hint'; + }; + + var on = function (event_name, callback) { + + $body.on(makeEventName(event_name, true), callback); + }; + + var off = function (event_name) { + + $body.off(makeEventName(event_name, true)); + }; + + + /********************* PUBLIC METHODS ***************************************/ + + window.addEventListener('resize', function() { + + if ($event_element != null) { + + $body.enjoyhint('redo_events_near_rect', $event_element[0].getBoundingClientRect()); + } + }); + + that.stop = function() { + + skipAll(); + }; + + that.reRunScript = function(cs) { + + current_step = cs; + stepAction(); + }; + + that.runScript = function () { + + current_step = 0; + options.onStart(); + stepAction(); + }; + + that.resumeScript = function () { + + stepAction(); + }; + + that.setCurrentStep = function(cs) { + + current_step = cs; + }; + + that.getCurrentStep = function () { + + return current_step; + }; + + that.trigger = function (event_name) { + + switch (event_name) { + + case 'next': + + nextStep(); + break; + + case 'skip': + + skipAll(); + break; + } + }; + + that.setScript = function (_data) { + + if (_data) { + + data = _data; + } + }; + + //support deprecated API methods + that.set = function (_data) { + + that.setScript(_data); + }; + + that.setSteps = function (_data) { + + that.setScript(_data); + }; + + that.run = function () { + + that.runScript(); + }; + + that.resume = function () { + + that.resumeScript(); + }; + + init(); +}; \ No newline at end of file diff --git a/inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/app/css/hint.js b/inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/app/css/hint.js index ab231315c94..c79657ee3a3 100644 --- a/inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/app/css/hint.js +++ b/inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/app/css/hint.js @@ -16,60 +16,140 @@ #limitations under the License. */ -$(document).ready(function(){ +$(document).ready( + function() { + + var enjoyhint_instance = new EnjoyHint({ + onSkip : function() { + // reset all the cookies + } + }); + + var enjoyhint_script_steps; + var currentPage = window.location.pathname; + var projectId = getUrlParameter('p'); + var cName = createCookieName(currentPage); + var ps = getCookie(cName); + + if (currentPage == "/projects.html") { + ps = getCookie(cName); + if (ps == 'true') { + } else if (ps == "") { + enjoyhint_instance = new EnjoyHint({ + onEnd : function() { + setCookie(cName, 'projectCreated'); + }, + onSkip : function() { + setCookie(cName, true); + } + }); + + enjoyhint_script_steps = createFirstPageRoutine(); + enjoyhint_instance.set(enjoyhint_script_steps); + enjoyhint_instance.runScript(); + } else if (ps == "projectCreated") { + enjoyhint_instance = new EnjoyHint({ + onEnd : function() { + setCookie(cName, true); + }, + onSkip : function() { + setCookie(cName, true); + } + }); + enjoyhint_script_steps = createFirstPageRoutinePart2(); + enjoyhint_instance.set(enjoyhint_script_steps); + enjoyhint_instance.runScript(); + + } + } else if (currentPage == "/projectsetting.html" + && projectId == 'NEW' && ps != "redirect") { + + enjoyhint_instance = new EnjoyHint({ + onEnd : function() { + // reset all the cookies + setCookie(cName, 'redirect'); + }, + onSkip : function() { + setCookie(cName, true); + } + + }); + + ps = getCookie(cName); + if (ps == 'true') { + } else { + ps = 'redirect'; + if (ps != "" && ps != null) { + setCookie(cName, ps, 365); + } + + enjoyhint_script_steps = createNewProjectRoutine(); + enjoyhint_instance.set(enjoyhint_script_steps); + enjoyhint_instance.runScript(); + } + } else if (currentPage == "/projectsetting.html" + && projectId == 'NEW' && ps == "redirect") { + enjoyhint_instance = new EnjoyHint({}); + + ps = true; + if (ps != "" && ps != null) { + setCookie(cName, ps, 365); + } + enjoyhint_script_steps = createRedirectRoutine(); + enjoyhint_instance.set(enjoyhint_script_steps); + enjoyhint_instance.runScript(); + + } else if (currentPage == "/project.html") { + var ps = getCookie(cName); + if (ps != "") { + } else { + ps = true; + if (ps != "" && ps != null) { + setCookie(cName, ps, 365); + } + + enjoyhint_script_steps = createDashboardRoutine(); + enjoyhint_instance.set(enjoyhint_script_steps); + enjoyhint_instance.runScript(); + } + } else if (currentPage == "/projectsetting.html") { + var ps = getCookie(cName); + + debugger; + + if (ps == 'true') { + // alert("PS Visited? " + ps); + } + else if (ps == "recommenderSaved") { + ps = true; + if (ps != "" && ps != null) { + setCookie(cName, ps, 365); + } + enjoyhint_script_steps = createAnnotationRoutine(); + enjoyhint_instance.set(enjoyhint_script_steps); + enjoyhint_instance.runScript(); + } + else { + enjoyhint_instance = new EnjoyHint({ + onEnd : function() { + ps = "recommenderSaved"; + if (ps != "" && ps != null) { + setCookie(cName, ps, 365); + } + }, + onSkip : function() { + setCookie(cName, true); + } + + }); + + enjoyhint_script_steps = createSettingsRoutine(); + enjoyhint_instance.set(enjoyhint_script_steps); + enjoyhint_instance.runScript(); + } + } - var enjoyhint_instance = new EnjoyHint({ - onSkip:function(){ -// reset all the cookies - } - }); - - var enjoyhint_script_steps ; -var currentPage = window.location.pathname; -var cName = createCookieName(currentPage); - -if (currentPage == "/projects.html") { - - var ps = getCookie(cName); - if (ps != "") { - } else { - ps = true; - if (ps != "" && ps != null) { - setCookie(cName, ps, 365); - } - - enjoyhint_script_steps = createNewProjectRoutine(); - } -} else if (currentPage == "/project.html") { - var ps = getCookie(cName); - if (ps != "") { - } else { - ps = true; - if (ps != "" && ps != null) { - setCookie(cName, ps, 365); - } - - enjoyhint_script_steps = createDashboardRoutine(); - } -} else if (currentPage == "/projectsetting.html") { - var ps = getCookie(cName); - if (ps != "") { - //alert("PS Visited? " + ps); - } else { - ps = true; - if (ps != "" && ps != null) { - setCookie(cName, ps, 365); - } - enjoyhint_script_steps = createSettingsRoutine(); - } -} - - -enjoyhint_instance.set(enjoyhint_script_steps); -enjoyhint_instance.runScript(); - -}); function setCookie(cname, cvalue, exdays) { var d = new Date(); @@ -94,6 +174,25 @@ function getCookie(cname) { } function createNewProjectRoutine() { + var a = [ { + 'change [name=\'p::name\']' : 'Enter the name of the project and click next', + 'nextButton' : { + className : "myNext", + text : "Next" + }, + 'skipButton' : { + className : "mySkip", + text : "Nope!" + } + }, { + 'click [type=submit]' : 'Click save' + } + + ]; + return a; +} + +function createFirstPageRoutine() { var a = [ { 'next .navbar-brand' : 'Welcome to INCEpTION! Let me guide you through its features.', @@ -107,8 +206,14 @@ function createNewProjectRoutine() { } }, { - 'next .btn btn-primary' : "Here is the button to create a new project", - }, + 'click .btn btn-primary' : "Click here to create a new project", + } ]; + return a; +} + +function createFirstPageRoutinePart2() { + var a = [ + { 'next .file-input-new' : "The projects can also be imported", }, @@ -143,25 +248,56 @@ function createDashboardRoutine() { function createSettingsRoutine() { var a = [ - { - 'click .tab1' : 'Click here to add a the document to the project', - }, - { - 'next [class=flex-h-container]' : "Upload a file and import. Then click Next.", - }, - { - 'click .tab5' : "Now, lets add a recommender. Click here!", - }, - { - 'click [value=Create]' : "Click to create a new recommender", - }, - { - 'next form:last' : "Fill in the details and save", - }]; - + { + 'click .tab2' : 'Click here to add a the document to the project', + }, + { + 'next [class=flex-h-container]' : "Upload a file and import. Then click Next.", + }, { + 'click .tab5' : "Now, lets add a recommender. Click here!", + }, { + 'click [value=Create]' : "Click to create a new recommender", + }, { + 'next form:last' : "Fill in the details and click next", + }, { + 'click [name=save]' : "Click to save", + } ]; + return a; } -function createCookieName(currentPage) { +function createRedirectRoutine() { + debugger; + var a = [ { + 'click .navbar-link' : 'Click here to go back to the projects page' + } ]; + + return a; +} + +function createAnnotationRoutine() { + var a = [ + { + 'click [href=\'./project.html\']:last' : 'Now, lets go to go to the Dashboard', + } ]; + + return a; +} + +function createCookieName(currentPage) { return currentPage.substr(1); } + +function getUrlParameter(sParam) { + var sPageURL = window.location.search.substring(1), sURLVariables = sPageURL + .split('&'), sParameterName, i; + + for (i = 0; i < sURLVariables.length; i++) { + sParameterName = sURLVariables[i].split('='); + + if (sParameterName[0] === sParam) { + return sParameterName[1] === undefined ? true + : decodeURIComponent(sParameterName[1]); + } + } +}; From 359a617cbacaf09a977a909197b0c82fd094676d Mon Sep 17 00:00:00 2001 From: uwinch Date: Mon, 30 Sep 2019 13:23:25 +0200 Subject: [PATCH 044/453] #1256 Integrate curation into annotation page - version mismatch - fixed npe in trigger recommenders for curation user --- .../recommendation/service/RecommendationServiceImpl.java | 7 +------ pom.xml | 2 +- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommendationServiceImpl.java b/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommendationServiceImpl.java index f583c96eab7..bf746dc2c45 100644 --- a/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommendationServiceImpl.java +++ b/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommendationServiceImpl.java @@ -501,13 +501,8 @@ public void onDocumentCreated(BeforeDocumentRemovedEvent aEvent) public void triggerTrainingAndClassification(String aUser, Project aProject, String aEventName) { User user = userRepository.get(aUser); - // do not trigger training during when viewing others' work - if (!user.equals(userRepository.getCurrentUser())) { - return; - } - - if (user == null) { + if (user == null || !user.equals(userRepository.getCurrentUser())) { return; } diff --git a/pom.xml b/pom.xml index e95bc299052..fa8d3ddfde2 100644 --- a/pom.xml +++ b/pom.xml @@ -54,7 +54,6 @@ - 4.0.0-beta-1 3.1.0 2.0.0 5.1.10.RELEASE @@ -73,6 +72,7 @@ 8 8 ${session.executionRootDirectory}/cache/dkpro-core-datasets + 4.0.0-SNAPSHOT From 625624690b452d6c8f4cb8afd73d1c646414a8cc Mon Sep 17 00:00:00 2001 From: uwinch Date: Tue, 1 Oct 2019 15:12:30 +0200 Subject: [PATCH 045/453] #1256 Integrate curation into annotation page - added empty space message --- .../curation/sidebar/CurationSidebar.html | 1 + .../curation/sidebar/CurationSidebar.java | 27 ++++++++++++++++--- .../sidebar/CurationSidebar.properties | 3 ++- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.html b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.html index 665a08c2126..936e7b55751 100644 --- a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.html +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.html @@ -24,6 +24,7 @@

    +

    diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java index 1d7de6d5e72..452916b5735 100644 --- a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.java @@ -18,7 +18,6 @@ package de.tudarmstadt.ukp.inception.curation.sidebar; import static de.tudarmstadt.ukp.clarin.webanno.api.WebAnnoConst.CURATION_USER; -import static de.tudarmstadt.ukp.clarin.webanno.support.lambda.LambdaBehavior.visibleWhen; import java.io.IOException; import java.util.ArrayList; @@ -46,6 +45,7 @@ import org.apache.wicket.model.IModel; import org.apache.wicket.model.LoadableDetachableModel; import org.apache.wicket.model.Model; +import org.apache.wicket.model.ResourceModel; import org.apache.wicket.model.util.ListModel; import org.apache.wicket.spring.injection.annot.SpringBean; import org.apache.wicket.util.value.AttributeMap; @@ -92,6 +92,8 @@ public class CurationSidebar private RadioChoice curationTargetChoice; private WebMarkupContainer mainContainer; private DropDownChoice mergeChoice; + private ListView users; + private Label noDocsLabel; private AnnotationPage annoPage; @@ -116,8 +118,12 @@ public CurationSidebar(String aId, IModel aModel, settingsForm.setOutputMarkupId(true); settingsForm.setVisible(false); mainContainer.add(settingsForm); + + // Add empty space message + noDocsLabel = new Label("noDocumentsLabel", new ResourceModel("noDocuments")); + mainContainer.add(noDocsLabel); } - + private Form createSettingsForm(String aId) { Form settingsForm = new Form(aId); @@ -206,6 +212,19 @@ protected void onConfigure() setEnabled((user.equals(userRepository.getCurrentUser()) || user.getUsername().equals(CURATION_USER)) && !documentService.isAnnotationFinished(state.getDocument(), user)); + configureVisibility(); + } + + protected void configureVisibility() + { + if (users.getModelObject().isEmpty()) { + usersForm.setVisible(false); + noDocsLabel.setVisible(true); + } + else { + usersForm.setVisible(true); + noDocsLabel.setVisible(false); + } } private void merge(AjaxRequestTarget aTarget, Form aForm) @@ -272,7 +291,7 @@ private Form> createUserSelection() usersForm.add(clearButton); usersForm.add(showButton); selectedUsers = new CheckGroup("selectedUsers", usersForm.getModelObject()); - ListView users = new ListView("users", + users = new ListView("users", LoadableDetachableModel.of(this::listUsers)) { private static final long serialVersionUID = 1L; @@ -287,7 +306,7 @@ protected void populateItem(ListItem aItem) }; selectedUsers.add(users); usersForm.add(selectedUsers); - usersForm.add(visibleWhen(() -> !users.getModelObject().isEmpty())); +// usersForm.add(visibleWhen(() -> !users.getModelObject().isEmpty())); return usersForm; } diff --git a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.properties b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.properties index 0344de09ab8..2b532469d9f 100644 --- a/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.properties +++ b/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/sidebar/CurationSidebar.properties @@ -21,4 +21,5 @@ apply=Apply clear=Clear curationHeader=Curation curationTarget=Save as -mergeChoice=Merge \ No newline at end of file +mergeChoice=Merge +noDocuments=You need documents marked as finished to start curating! \ No newline at end of file From 04126aee692133f8c66d74c53a8aaffc69d7ad42 Mon Sep 17 00:00:00 2001 From: Sibgha360 Date: Mon, 7 Oct 2019 11:42:53 +0200 Subject: [PATCH 046/453] #1270 - Hints for new users - Bug fixed --- .../tudarmstadt/ukp/inception/app/css/hint.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/app/css/hint.js b/inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/app/css/hint.js index c79657ee3a3..9bd033e090e 100644 --- a/inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/app/css/hint.js +++ b/inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/app/css/hint.js @@ -28,10 +28,13 @@ $(document).ready( var enjoyhint_script_steps; var currentPage = window.location.pathname; var projectId = getUrlParameter('p'); - var cName = createCookieName(currentPage); - var ps = getCookie(cName); + + var cName = document.location.pathname.match(/[^\/]+$/)[0]; + //createCookieName(currentPage); + var ps = getCookie(cName); + debugger; - if (currentPage == "/projects.html") { + if (currentPage.includes("projects.html")) { ps = getCookie(cName); if (ps == 'true') { } else if (ps == "") { @@ -61,7 +64,7 @@ $(document).ready( enjoyhint_instance.runScript(); } - } else if (currentPage == "/projectsetting.html" + } else if (currentPage.includes("projectsetting.html") && projectId == 'NEW' && ps != "redirect") { enjoyhint_instance = new EnjoyHint({ @@ -87,7 +90,7 @@ $(document).ready( enjoyhint_instance.set(enjoyhint_script_steps); enjoyhint_instance.runScript(); } - } else if (currentPage == "/projectsetting.html" + } else if (currentPage.includes("projectsetting.html") && projectId == 'NEW' && ps == "redirect") { enjoyhint_instance = new EnjoyHint({}); @@ -99,7 +102,7 @@ $(document).ready( enjoyhint_instance.set(enjoyhint_script_steps); enjoyhint_instance.runScript(); - } else if (currentPage == "/project.html") { + } else if (currentPage.includes("project.html")) { var ps = getCookie(cName); if (ps != "") { } else { @@ -112,7 +115,7 @@ $(document).ready( enjoyhint_instance.set(enjoyhint_script_steps); enjoyhint_instance.runScript(); } - } else if (currentPage == "/projectsetting.html") { + } else if (currentPage.includes("projectsetting.html")) { var ps = getCookie(cName); debugger; From eea62a57e2c0ba676cd665e634a6a5ad804cc7c2 Mon Sep 17 00:00:00 2001 From: Sibgha360 Date: Mon, 7 Oct 2019 11:45:34 +0200 Subject: [PATCH 047/453] #1270 - Hints for new users - Moved the Javascript for hints/tutorial to the footer item --- .../config/InceptionResourcesBehavior.java | 19 ------ .../app/css/HintJavascriptReference.java | 28 -------- .../ui/core/footer/TutorialFooterItem.java | 18 +++++ .../ui/core/footer/TutorialFooterPanel.html | 28 ++++++++ .../ui/core/footer/TutorialFooterPanel.java | 65 +++++++++++++++++++ .../resources}/EnjoyHintJsReference.java | 2 +- .../TutorialJavascriptReference.java | 28 ++++++++ .../ui/core/footer/resources}/enjoyhint.js | 0 .../ui/core/footer/resources/tutorial.js | 0 .../ui/core/menubar/MenuBar.properties | 2 +- 10 files changed, 141 insertions(+), 49 deletions(-) delete mode 100644 inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/app/css/HintJavascriptReference.java create mode 100644 inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/footer/TutorialFooterItem.java create mode 100644 inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/footer/TutorialFooterPanel.html create mode 100644 inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/footer/TutorialFooterPanel.java rename {inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/app/css => inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/footer/resources}/EnjoyHintJsReference.java (95%) create mode 100644 inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/footer/resources/TutorialJavascriptReference.java rename {inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/app/css => inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/footer/resources}/enjoyhint.js (100%) rename inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/app/css/hint.js => inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/footer/resources/tutorial.js (100%) diff --git a/inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/app/config/InceptionResourcesBehavior.java b/inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/app/config/InceptionResourcesBehavior.java index dccd9075c23..9bfd51df20b 100644 --- a/inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/app/config/InceptionResourcesBehavior.java +++ b/inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/app/config/InceptionResourcesBehavior.java @@ -21,13 +21,7 @@ import org.apache.wicket.behavior.Behavior; import org.apache.wicket.markup.head.CssHeaderItem; import org.apache.wicket.markup.head.IHeaderResponse; -import org.apache.wicket.markup.head.JavaScriptHeaderItem; -import de.agilecoders.wicket.webjars.request.resource.WebjarsCssResourceReference; -import de.agilecoders.wicket.webjars.request.resource.WebjarsJavaScriptResourceReference; -import de.tudarmstadt.ukp.clarin.webanno.ui.core.page.WebAnnoJavascriptReference; -import de.tudarmstadt.ukp.inception.app.css.EnjoyHintJsReference; -import de.tudarmstadt.ukp.inception.app.css.HintJavascriptReference; import de.tudarmstadt.ukp.inception.app.css.InceptionCssReference; public class InceptionResourcesBehavior @@ -42,19 +36,6 @@ public void renderHead(Component aComponent, IHeaderResponse aResponse) { // Loading WebAnno CSS here so it can override JQuery/Kendo CSS aResponse.render(CssHeaderItem.forReference(InceptionCssReference.get())); - aResponse.render(JavaScriptHeaderItem.forReference(WebAnnoJavascriptReference.get())); - aResponse.render(JavaScriptHeaderItem.forReference(EnjoyHintJsReference.get())); - - // Loading resources for the tour guide feature for the new users -// aResponse.render(JavaScriptHeaderItem -// .forReference(new WebjarsJavaScriptResourceReference("enjoyhint/current/enjoyhint.js"))); - aResponse.render(JavaScriptHeaderItem - .forReference(new WebjarsJavaScriptResourceReference("enjoyhint/current/jquery.enjoyhint.js"))); - aResponse.render(CssHeaderItem.forReference(new WebjarsCssResourceReference("enjoyhint/current/jquery.enjoyhint.css"))); - aResponse.render(JavaScriptHeaderItem.forReference(new WebjarsJavaScriptResourceReference("jquery.scrollTo/current/jquery.scrollTo.js"))); - aResponse.render(JavaScriptHeaderItem.forReference(new WebjarsJavaScriptResourceReference("kinetic/current/kinetic.min.js"))); - - aResponse.render(JavaScriptHeaderItem.forReference(HintJavascriptReference.get())); } public static InceptionResourcesBehavior get() diff --git a/inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/app/css/HintJavascriptReference.java b/inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/app/css/HintJavascriptReference.java deleted file mode 100644 index 0fcaadf0d6a..00000000000 --- a/inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/app/css/HintJavascriptReference.java +++ /dev/null @@ -1,28 +0,0 @@ -package de.tudarmstadt.ukp.inception.app.css; - -import org.apache.wicket.request.resource.JavaScriptResourceReference; - -public class HintJavascriptReference extends JavaScriptResourceReference -{ - private static final long serialVersionUID = 1L; - - private static final HintJavascriptReference INSTANCE = new HintJavascriptReference(); - - /** - * Gets the instance of the resource reference - * - * @return the single instance of the resource reference - */ - public static HintJavascriptReference get() - { - return INSTANCE; - } - - /** - * Private constructor - */ - private HintJavascriptReference() - { - super(HintJavascriptReference.class, "hint.js"); - } -} diff --git a/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/footer/TutorialFooterItem.java b/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/footer/TutorialFooterItem.java new file mode 100644 index 00000000000..e7a3b952abb --- /dev/null +++ b/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/footer/TutorialFooterItem.java @@ -0,0 +1,18 @@ +package de.tudarmstadt.ukp.inception.ui.core.footer; + +import org.apache.wicket.Component; +import org.springframework.core.annotation.Order; + +import de.tudarmstadt.ukp.clarin.webanno.ui.core.footer.FooterItem; + +@Order(100) +@org.springframework.stereotype.Component +public class TutorialFooterItem + implements FooterItem +{ + @Override + public Component create(String aId) + { + return new TutorialFooterPanel(aId); + } +} diff --git a/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/footer/TutorialFooterPanel.html b/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/footer/TutorialFooterPanel.html new file mode 100644 index 00000000000..aaeeedba870 --- /dev/null +++ b/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/footer/TutorialFooterPanel.html @@ -0,0 +1,28 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/footer/TutorialFooterPanel.java b/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/footer/TutorialFooterPanel.java new file mode 100644 index 00000000000..62ea8d521de --- /dev/null +++ b/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/footer/TutorialFooterPanel.java @@ -0,0 +1,65 @@ +/* + * Copyright 2019 + * Ubiquitous Knowledge Processing (UKP) Lab and FG Language Technology + * Technische Universität Darmstadt + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.tudarmstadt.ukp.inception.ui.core.footer; + +import org.apache.wicket.markup.head.CssHeaderItem; +import org.apache.wicket.markup.head.IHeaderResponse; +import org.apache.wicket.markup.head.JavaScriptHeaderItem; +import org.apache.wicket.markup.html.panel.Panel; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import de.agilecoders.wicket.webjars.request.resource.WebjarsCssResourceReference; +import de.agilecoders.wicket.webjars.request.resource.WebjarsJavaScriptResourceReference; +import de.tudarmstadt.ukp.clarin.webanno.ui.core.page.WebAnnoJavascriptReference; +import de.tudarmstadt.ukp.inception.ui.core.footer.resources.EnjoyHintJsReference; +import de.tudarmstadt.ukp.inception.ui.core.footer.resources.TutorialJavascriptReference; + +public class TutorialFooterPanel + extends Panel +{ + private static final long serialVersionUID = -1520440226035000228L; + private final static Logger LOG = LoggerFactory.getLogger(TutorialFooterPanel.class); + + public TutorialFooterPanel(String aId) + { + super(aId); + } + + @Override + public void renderHead(IHeaderResponse aResponse) { + aResponse.render(JavaScriptHeaderItem.forReference(WebAnnoJavascriptReference.get())); + + //TODO move it back to web jars after latest release + aResponse.render(JavaScriptHeaderItem.forReference(EnjoyHintJsReference.get())); +// aResponse.render(JavaScriptHeaderItem +// .forReference(new WebjarsJavaScriptResourceReference("enjoyhint/current/enjoyhint.js"))); + + + + // Loading resources for the tour guide feature for the new users + aResponse.render(JavaScriptHeaderItem + .forReference(new WebjarsJavaScriptResourceReference("enjoyhint/current/jquery.enjoyhint.js"))); + aResponse.render(CssHeaderItem.forReference(new WebjarsCssResourceReference("enjoyhint/current/jquery.enjoyhint.css"))); + aResponse.render(JavaScriptHeaderItem.forReference(new WebjarsJavaScriptResourceReference("jquery.scrollTo/current/jquery.scrollTo.js"))); + aResponse.render(JavaScriptHeaderItem.forReference(new WebjarsJavaScriptResourceReference("kinetic/current/kinetic.min.js"))); + + aResponse.render(JavaScriptHeaderItem.forReference(TutorialJavascriptReference.get())); + } + +} diff --git a/inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/app/css/EnjoyHintJsReference.java b/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/footer/resources/EnjoyHintJsReference.java similarity index 95% rename from inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/app/css/EnjoyHintJsReference.java rename to inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/footer/resources/EnjoyHintJsReference.java index 517ba4c0cdf..50ae4dfbcb3 100644 --- a/inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/app/css/EnjoyHintJsReference.java +++ b/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/footer/resources/EnjoyHintJsReference.java @@ -15,7 +15,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package de.tudarmstadt.ukp.inception.app.css; +package de.tudarmstadt.ukp.inception.ui.core.footer.resources; import org.apache.wicket.request.resource.JavaScriptResourceReference; diff --git a/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/footer/resources/TutorialJavascriptReference.java b/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/footer/resources/TutorialJavascriptReference.java new file mode 100644 index 00000000000..11c04a2d137 --- /dev/null +++ b/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/footer/resources/TutorialJavascriptReference.java @@ -0,0 +1,28 @@ +package de.tudarmstadt.ukp.inception.ui.core.footer.resources; + +import org.apache.wicket.request.resource.JavaScriptResourceReference; + +public class TutorialJavascriptReference extends JavaScriptResourceReference +{ + private static final long serialVersionUID = 1L; + + private static final TutorialJavascriptReference INSTANCE = new TutorialJavascriptReference(); + + /** + * Gets the instance of the resource reference + * + * @return the single instance of the resource reference + */ + public static TutorialJavascriptReference get() + { + return INSTANCE; + } + + /** + * Private constructor + */ + private TutorialJavascriptReference() + { + super(TutorialJavascriptReference.class, "tutorial.js"); + } +} diff --git a/inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/app/css/enjoyhint.js b/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/footer/resources/enjoyhint.js similarity index 100% rename from inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/app/css/enjoyhint.js rename to inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/footer/resources/enjoyhint.js diff --git a/inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/app/css/hint.js b/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/footer/resources/tutorial.js similarity index 100% rename from inception-app-webapp/src/main/java/de/tudarmstadt/ukp/inception/app/css/hint.js rename to inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/footer/resources/tutorial.js diff --git a/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/menubar/MenuBar.properties b/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/menubar/MenuBar.properties index 4cbe79ca786..68a1e4026a7 100644 --- a/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/menubar/MenuBar.properties +++ b/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/menubar/MenuBar.properties @@ -17,4 +17,4 @@ project.null=Choose a project administration=Administration projects=Projects dashboard=Dashboard -hint=Hint +hint=Get Started From 8c29c5e6eb7bc268d0ad7110318912fdb16ce50c Mon Sep 17 00:00:00 2001 From: Sibgha360 Date: Thu, 10 Oct 2019 01:20:23 +0200 Subject: [PATCH 048/453] #1270 - Hints for new users - issues fixed in tutorial.js --- .../ui/core/footer/resources/tutorial.js | 175 ++++++++++++------ .../inception/ui/core/menubar/MenuBar.html | 2 +- 2 files changed, 122 insertions(+), 55 deletions(-) diff --git a/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/footer/resources/tutorial.js b/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/footer/resources/tutorial.js index 9bd033e090e..de741cf6668 100644 --- a/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/footer/resources/tutorial.js +++ b/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/footer/resources/tutorial.js @@ -15,10 +15,7 @@ #See the License for the specific language governing permissions and #limitations under the License. */ - -$(document).ready( - function() { - +$(document).ready(function() { var enjoyhint_instance = new EnjoyHint({ onSkip : function() { // reset all the cookies @@ -32,7 +29,6 @@ $(document).ready( var cName = document.location.pathname.match(/[^\/]+$/)[0]; //createCookieName(currentPage); var ps = getCookie(cName); - debugger; if (currentPage.includes("projects.html")) { ps = getCookie(cName); @@ -65,12 +61,11 @@ $(document).ready( } } else if (currentPage.includes("projectsetting.html") - && projectId == 'NEW' && ps != "redirect") { - + && projectId == 'NEW' && ps != "projectSaved") { enjoyhint_instance = new EnjoyHint({ onEnd : function() { // reset all the cookies - setCookie(cName, 'redirect'); + setCookie(cName, 'projectSaved'); }, onSkip : function() { setCookie(cName, true); @@ -81,7 +76,7 @@ $(document).ready( ps = getCookie(cName); if (ps == 'true') { } else { - ps = 'redirect'; + ps = 'projectSaved'; if (ps != "" && ps != null) { setCookie(cName, ps, 365); } @@ -91,14 +86,21 @@ $(document).ready( enjoyhint_instance.runScript(); } } else if (currentPage.includes("projectsetting.html") - && projectId == 'NEW' && ps == "redirect") { + && projectId == 'NEW' && ps == "projectSaved") { + + //save the project name in the cookie + var projectName = $('[name="p::name"]').val() + setCookie("projectName", projectName, 365); + + //re-initialize the instance enjoyhint_instance = new EnjoyHint({}); - ps = true; + //reset the projectsetting cookie so the tutorial shows up on the settings page later + ps = ""; if (ps != "" && ps != null) { setCookie(cName, ps, 365); } - enjoyhint_script_steps = createRedirectRoutine(); + enjoyhint_script_steps = createProjectSavedRoutine(); enjoyhint_instance.set(enjoyhint_script_steps); enjoyhint_instance.runScript(); @@ -118,11 +120,10 @@ $(document).ready( } else if (currentPage.includes("projectsetting.html")) { var ps = getCookie(cName); - debugger; - if (ps == 'true') { // alert("PS Visited? " + ps); } + //TODO do we need it else if (ps == "recommenderSaved") { ps = true; if (ps != "" && ps != null) { @@ -133,9 +134,10 @@ $(document).ready( enjoyhint_instance.runScript(); } else { - enjoyhint_instance = new EnjoyHint({ + enjoyhint_instance = new EnjoyHint({ + onEnd : function() { - ps = "recommenderSaved"; + ps = "true"; if (ps != "" && ps != null) { setCookie(cName, ps, 365); } @@ -146,14 +148,15 @@ $(document).ready( }); - enjoyhint_script_steps = createSettingsRoutine(); + enjoyhint_script_steps = createSettingsRoutine(enjoyhint_instance); enjoyhint_instance.set(enjoyhint_script_steps); enjoyhint_instance.runScript(); } } - }); + + function setCookie(cname, cvalue, exdays) { var d = new Date(); d.setTime(d.getTime() + (exdays * 24 * 60 * 60 * 1000)); @@ -178,18 +181,11 @@ function getCookie(cname) { function createNewProjectRoutine() { var a = [ { - 'change [name=\'p::name\']' : 'Enter the name of the project and click next', - 'nextButton' : { - className : "myNext", - text : "Next" - }, - 'skipButton' : { - className : "mySkip", - text : "Nope!" - } - }, { - 'click [type=submit]' : 'Click save' + 'key [name=\'p::name\']' : 'Write the name of the project and press Enter', } +// , { +// 'click [type=submit]' : 'Click save' +// } ]; return a; @@ -198,14 +194,14 @@ function createNewProjectRoutine() { function createFirstPageRoutine() { var a = [ { - 'next .navbar-brand' : 'Welcome to INCEpTION! Let me guide you through its features.', + 'next .navbar-brand' : 'Welcome to INCEpTION! Let me guide you through its features. At any time during the tutorial you can skip and exit the tour', 'nextButton' : { className : "myNext", text : "Sure" }, 'skipButton' : { className : "mySkip", - text : "Nope!" + text : "Skip" } }, { @@ -215,6 +211,13 @@ function createFirstPageRoutine() { } function createFirstPageRoutinePart2() { + var projectName = getCookie("projectName"); + var selector = "a:contains('"+projectName+"'):first"; + + var t = $('a').filter(function() { + return $(this).text() == projectName; + }); + var a = [ { @@ -224,35 +227,43 @@ function createFirstPageRoutinePart2() { 'next .input-group:last' : "You can filter the projects based on the ownership type", }, { - 'click .list-group-item' : "Click on the project to get started.", + "event": "click", + "selector": t[0], + "description": "Click on the project to get started.", + - } ]; + } + ]; return a; } function createDashboardRoutine() { var a = [ + { + 'next li:first-of-type' : 'Here, you can annotate your documents', + }, + { + 'next li:nth-of-type(2)' : 'Completely annotated documents can be curated to form the final result documents.', + }, + { + 'next li:nth-of-type(3)' : 'This will show you the agreement between annotators across documents.', + }, + { + 'next li:nth-of-type(4)' : 'Here, you can see the annotators\' progress and assign documents.', + }, + { + 'next li:nth-of-type(5)' : 'This allows you to evaluate your recommenders.', + }, { - 'next .nav-pills' : 'Lets explore the main features regarding the project', - 'nextButton' : { - className : "myNext", - text : "Okay!" - }, - 'skipButton' : { - className : "mySkip", - text : "Skip" - } - }, - { - 'click li:nth-last-child(1)' : "Before getting started, lets configure the project" + 'click li:nth-last-child(1)' : "Before getting started, lets configure the project. Please click Settings" } ]; return a; } -function createSettingsRoutine() { +function createSettingsRoutine(enjoyHint) { var a = [ { - 'click .tab2' : 'Click here to add a the document to the project', + 'click .tab2' : 'Click here to add a document to the project', }, { 'next [class=flex-h-container]' : "Upload a file and import. Then click Next.", @@ -260,17 +271,71 @@ function createSettingsRoutine() { 'click .tab5' : "Now, lets add a recommender. Click here!", }, { 'click [value=Create]' : "Click to create a new recommender", - }, { - 'next form:last' : "Fill in the details and click next", - }, { - 'click [name=save]' : "Click to save", - } ]; + }, + { + 'next .form-group:nth(2)' : "Select a Layer", + onBeforeStart:function(){ + $('[name=layer]').on('change', function() { + enjoyHint.trigger('next'); + }); + } + }, + { + 'next .form-group:nth(3)' : "Select a Feature", + onBeforeStart:function(){ + $('[name=feature]').on('change', function() { + enjoyHint.trigger('next'); + }); + } + }, + { + 'next .form-group:nth(4)' : "Select a Tool", + onBeforeStart:function(){ + $('[name=tool]').on('change', function() { + enjoyHint.trigger('next'); + }); + }, + }, + + { + "event_type": "custom", + "event": "event-save-recommender", + "selector": "[method=post]", + "description": "Fill in the details and click save", + onBeforeStart:function(){ + $("[name=\'save\']").on('click', + function(e) { + enjoyHint.trigger('next'); + }); + + }, + + + }, + { + 'click [href=\'./project.html\']:last' : 'Now, lets go to the Dashboard', + } + ]; return a; } -function createRedirectRoutine() { - debugger; +function createSettingsRoutine2() { + var a = [ + { + "event_type": "custom", + "event": "event-save-recommender", + "selector": "[method=post]", + "description": "Fill in the details and click save", + }, + { + 'click [href=\'./project.html\']:last' : 'Now, lets go to the Dashboard', + } + ]; + + return a; +} +function createProjectSavedRoutine() { var a = [ { 'click .navbar-link' : 'Click here to go back to the projects page' } ]; @@ -304,3 +369,5 @@ function getUrlParameter(sParam) { } } }; + + diff --git a/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/menubar/MenuBar.html b/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/menubar/MenuBar.html index d2fda1fe68b..ab51b626471 100644 --- a/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/menubar/MenuBar.html +++ b/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/menubar/MenuBar.html @@ -87,7 +87,7 @@

    From 10f3a6605b856ebf10a6afd4fafe02fd2be3d98a Mon Sep 17 00:00:00 2001 From: Marcel de Boer Date: Tue, 15 Oct 2019 13:02:46 +0200 Subject: [PATCH 049/453] 1054 - Dealing with many results in search sidebar - Implement Wrapper for SearchResultsProvider - adjust sidebar and SearchResultsProvider --- .../sidebar/SearchAnnotationSidebar.java | 20 +-- .../search/sidebar/SearchResultsProvider.java | 19 +++ .../sidebar/SearchResultsProviderWrapper.java | 118 ++++++++++++++++++ 3 files changed, 149 insertions(+), 8 deletions(-) create mode 100644 inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchResultsProviderWrapper.java diff --git a/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebar.java b/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebar.java index 65f8609c6db..291904ed4cd 100644 --- a/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebar.java +++ b/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebar.java @@ -30,8 +30,6 @@ import java.util.Map; import java.util.Map.Entry; import java.util.Objects; -import java.util.Optional; -import java.util.Set; import java.util.stream.Collectors; import org.apache.uima.cas.CAS; @@ -121,7 +119,7 @@ public class SearchAnnotationSidebar private final WebMarkupContainer mainContainer; private final WebMarkupContainer resultsGroupContainer; - private final SearchResultsProvider resultsProvider; + private final SearchResultsProviderWrapper resultsProvider; private IModel targetQuery = Model.of(""); private IModel searchOptions = CompoundPropertyModel.of(new SearchOptions()); @@ -144,9 +142,9 @@ public SearchAnnotationSidebar(String aId, IModel aModel, super(aId, aModel, aActionHandler, aCasProvider, aAnnotationPage); currentUser = userRepository.getCurrentUser(); - - resultsProvider = new SearchResultsProvider(searchService, - groupedSearchResults); + resultsProvider = new SearchResultsProviderWrapper(new SearchResultsProvider(searchService, + groupedSearchResults)); + mainContainer = new WebMarkupContainer("mainContainer"); mainContainer.setOutputMarkupId(true); @@ -205,7 +203,10 @@ protected void populateItem(Item item) searchOptions.getObject().setItemsPerPage(searchProperties.getPageSizes()[0]); searchResultGroups.setItemsPerPage(searchOptions.getObject().getItemsPerPage()); resultsGroupContainer.add(searchResultGroups); - mainContainer.add(new PagingNavigator("pagingNavigator", searchResultGroups)); + mainContainer.add(new PagingNavigator("pagingNavigator", searchResultGroups).add( + visibleWhen( + () -> groupedSearchResults.getObject() != null && !groupedSearchResults.getObject() + .isEmpty()))); Form annotationForm = new Form<>("annotateForm"); @@ -347,10 +348,11 @@ private void actionClearResults(AjaxRequestTarget aTarget, Form aForm) aTarget.add(mainContainer); } - private Map getSearchResultsGrouped() + private void getSearchResultsGrouped() { if (isBlank(targetQuery.getObject())) { resultsProvider.emptyQuery(); + groupedSearchResults.setObject(null); return; } @@ -360,6 +362,7 @@ private Map getSearchResultsGrouped() error( "A feature has to be selected in order to group by feature values. If you want to group by document title, select none for both layer and feature."); resultsProvider.emptyQuery(); + groupedSearchResults.setObject(null); return; } @@ -380,6 +383,7 @@ private Map getSearchResultsGrouped() catch (Exception e) { error("Error in the query: " + e.getMessage()); resultsProvider.emptyQuery(); + groupedSearchResults.setObject(null); return; } } diff --git a/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchResultsProvider.java b/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchResultsProvider.java index 5f7e9160912..d05243354d0 100644 --- a/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchResultsProvider.java +++ b/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchResultsProvider.java @@ -154,5 +154,24 @@ public void emptyQuery() query = null; totalResults = 0; } + + public AnnotationLayer getAnnotationLayer() + { + return annotationLayer; + } + + public AnnotationFeature getAnnotationFeature() + { + return annotationFeature; + } + + public SearchService getSearchService() { + return searchService; + } + + public IModel> getCurrentPageCache() + { + return currentPageCache; + } } diff --git a/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchResultsProviderWrapper.java b/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchResultsProviderWrapper.java new file mode 100644 index 00000000000..b899e436d20 --- /dev/null +++ b/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchResultsProviderWrapper.java @@ -0,0 +1,118 @@ +package de.tudarmstadt.ukp.inception.app.ui.search.sidebar; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import org.apache.wicket.markup.repeater.data.IDataProvider; +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.clarin.webanno.model.Project; +import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; +import de.tudarmstadt.ukp.clarin.webanno.security.model.User; +import de.tudarmstadt.ukp.inception.search.ResultsGroup; +import de.tudarmstadt.ukp.inception.search.SearchResult; + +public class SearchResultsProviderWrapper implements IDataProvider +{ + private SearchResultsProvider searchResultsProvider; + private List resultGroups; + private boolean groupingActivated; + + + public SearchResultsProviderWrapper(SearchResultsProvider aSearchResultsProvider) + { + searchResultsProvider = aSearchResultsProvider; + } + + @Override + public Iterator iterator(long first, long count) + { + /* + If the grouping is activated we first fetch all results sorted them by group and then apply + the paging. + We do this because if we apply paging at query level and grouping together, members of a + group might me scattered over multiple pages. (Grouping by document is + an exception because the query iterates over documents. So results from the same document + appear in a sequence) + + If the grouping is not activated we can apply paging directly when fetching the results + since it defaults to grouping by document. So we fetch them from the searchResultsProvider. + */ + if (groupingActivated) { + List resultsGroupsSubList = new ArrayList<>(); + int counter = 0; + for (ResultsGroup resultsGroup : resultGroups) { + if (counter - first + 1 > count) { + break; + } + List sublist = new ArrayList<>(); + for (SearchResult result : resultsGroup.getResults()) { + if (counter < first) { + counter ++; + continue; + } + if (counter - first + 1 > count) { + break; + } + sublist.add(result); + counter++; + } + if (! (counter <= first)) { + ResultsGroup group = new ResultsGroup(resultsGroup.getGroupKey(), sublist); + resultsGroupsSubList.add(group); + } + } + searchResultsProvider.getCurrentPageCache().setObject(resultsGroupsSubList); + return resultsGroupsSubList.iterator(); + } + + return searchResultsProvider.iterator(first, count); + } + + @Override + public long size() + { + return searchResultsProvider.size(); + } + + @Override + public IModel model(ResultsGroup object) + { + return searchResultsProvider.model(object); + } + + + public void initializeQuery(User aUser, Project aProject, String aQuery, + SourceDocument aDocument, AnnotationLayer aAnnotationLayer, + AnnotationFeature aAnnotationFeature) + { + searchResultsProvider.initializeQuery(aUser, aProject, aQuery, aDocument, aAnnotationLayer, + aAnnotationFeature); + + groupingActivated = !(searchResultsProvider.getAnnotationFeature() == null + && searchResultsProvider.getAnnotationLayer() == null); + + if (groupingActivated) { + resultGroups = getAllResults(); + } + + } + + private List getAllResults() + { + Iterator resultsIterator = searchResultsProvider.iterator(0, Long.MAX_VALUE); + ArrayList resultsList = new ArrayList<>(); + resultsIterator.forEachRemaining(r -> resultsList.add(r)); + return resultsList; + + } + + public void emptyQuery() + { + searchResultsProvider.emptyQuery(); + groupingActivated = false; + } +} From 6dd197bbcfeda93901f8d8e07417aa16bbfe6533 Mon Sep 17 00:00:00 2001 From: xuanxuanhoney <1041308160@qq.com> Date: Fri, 18 Oct 2019 21:16:22 +0200 Subject: [PATCH 050/453] Remove the useless curly braces --- .../kb/KnowledgeBaseServiceImpl.java | 54 +++++++++---------- ...owledgeBaseServiceImplIntegrationTest.java | 12 ++--- ...seServiceImplQualifierIntegrationTest.java | 6 +-- .../log/exporter/LoggedEventExporter.java | 6 +-- .../recommendation/api/model/Predictions.java | 6 +-- .../SimulationLearningCurvePanel.java | 6 +-- .../ukp/inception/scheduling/Task.java | 3 ++ .../DocumentRepositoryEditorPanel.java | 6 +-- .../inception/ui/kb/AbstractInfoPanel.java | 12 ++--- .../ui/kb/feature/QualifierFeatureEditor.java | 6 +-- .../project/AccessSpecificSettingsPanel.java | 6 +-- .../ui/kb/project/KnowledgeBaseIriPanel.java | 4 +- .../ui/kb/project/RootConceptsPanel.java | 6 +-- 13 files changed, 68 insertions(+), 65 deletions(-) diff --git a/inception-kb/src/main/java/de/tudarmstadt/ukp/inception/kb/KnowledgeBaseServiceImpl.java b/inception-kb/src/main/java/de/tudarmstadt/ukp/inception/kb/KnowledgeBaseServiceImpl.java index 0631be64ed5..e7d63e68f44 100644 --- a/inception-kb/src/main/java/de/tudarmstadt/ukp/inception/kb/KnowledgeBaseServiceImpl.java +++ b/inception-kb/src/main/java/de/tudarmstadt/ukp/inception/kb/KnowledgeBaseServiceImpl.java @@ -495,9 +495,9 @@ public void updateConcept(KnowledgeBase kb, KBConcept aConcept) @Override public void deleteConcept(KnowledgeBase aKB, KBConcept aConcept) { - update(aKB, conn -> { - getReificationStrategy(aKB).deleteConcept(conn, aKB, aConcept); - }); + update(aKB, conn -> + getReificationStrategy(aKB).deleteConcept(conn, aKB, aConcept) + ); } @Override @@ -559,9 +559,9 @@ public void updateProperty(KnowledgeBase kb, KBProperty aProperty) @Override public void deleteProperty(KnowledgeBase aKB, KBProperty aType) { - update(aKB, conn -> { - getReificationStrategy(aKB).deleteProperty(conn, aKB, aType); - }); + update(aKB, conn -> + getReificationStrategy(aKB).deleteProperty(conn, aKB, aType) + ); } @Override @@ -646,9 +646,9 @@ public void updateInstance(KnowledgeBase kb, KBInstance aInstance) @Override public void deleteInstance(KnowledgeBase aKB, KBInstance aInstance) { - update(aKB, conn -> { - getReificationStrategy(aKB).deleteInstance(conn, aKB, aInstance); - }); + update(aKB, conn -> + getReificationStrategy(aKB).deleteInstance(conn, aKB, aInstance) + ); } @Override @@ -670,18 +670,18 @@ public List listInstances(KnowledgeBase aKB, String aConceptIri, boole public void upsertStatement(KnowledgeBase aKB, KBStatement aStatement) throws RepositoryException { - update(aKB, conn -> { - getReificationStrategy(aKB).upsertStatement(conn, aKB, aStatement); - }); + update(aKB, conn -> + getReificationStrategy(aKB).upsertStatement(conn, aKB, aStatement) + ); } @Override public void deleteStatement(KnowledgeBase aKB, KBStatement aStatement) throws RepositoryException { - update(aKB, conn -> { - getReificationStrategy(aKB).deleteStatement(conn, aKB, aStatement); - }); + update(aKB, conn -> + getReificationStrategy(aKB).deleteStatement(conn, aKB, aStatement) + ); } @Override @@ -873,33 +873,33 @@ private ReificationStrategy getReificationStrategy(KnowledgeBase kb) */ public void createBaseProperty(KnowledgeBase akb, KBProperty aProperty) { - update(akb, (conn) -> { - aProperty.write(conn, akb); - }); + update(akb, (conn) -> + aProperty.write(conn, akb) + ); } @Override public void addQualifier(KnowledgeBase aKB, KBQualifier newQualifier) { - update(aKB, conn -> { - getReificationStrategy(aKB).upsertQualifier(conn, aKB, newQualifier); - }); + update(aKB, conn -> + getReificationStrategy(aKB).upsertQualifier(conn, aKB, newQualifier) + ); } @Override public void deleteQualifier(KnowledgeBase aKB, KBQualifier oldQualifier) { - update(aKB, conn -> { - getReificationStrategy(aKB).deleteQualifier(conn, aKB, oldQualifier); - }); + update(aKB, conn -> + getReificationStrategy(aKB).deleteQualifier(conn, aKB, oldQualifier) + ); } @Override public void upsertQualifier(KnowledgeBase aKB, KBQualifier aQualifier) { - update(aKB, conn -> { - getReificationStrategy(aKB).upsertQualifier(conn, aKB, aQualifier); - }); + update(aKB, conn -> + getReificationStrategy(aKB).upsertQualifier(conn, aKB, aQualifier) + ); } @Override diff --git a/inception-kb/src/test/java/de/tudarmstadt/ukp/inception/kb/KnowledgeBaseServiceImplIntegrationTest.java b/inception-kb/src/test/java/de/tudarmstadt/ukp/inception/kb/KnowledgeBaseServiceImplIntegrationTest.java index b2999745ef4..2eb9b040309 100644 --- a/inception-kb/src/test/java/de/tudarmstadt/ukp/inception/kb/KnowledgeBaseServiceImplIntegrationTest.java +++ b/inception-kb/src/test/java/de/tudarmstadt/ukp/inception/kb/KnowledgeBaseServiceImplIntegrationTest.java @@ -1176,9 +1176,9 @@ public void deleteStatement_WithNonExistentStatement_ShouldDoNothing() { sut.createProperty(kb, property); KBStatement statement = buildStatement(kb, concept.toKBHandle(), property, "Test statement"); - assertThatCode(() -> { - sut.deleteStatement(kb, statement); - }).doesNotThrowAnyException(); + assertThatCode(() -> + sut.deleteStatement(kb, statement) + ).doesNotThrowAnyException(); } @Test @@ -1527,9 +1527,9 @@ public void readKnowledgeBaseProfiles_ShouldReturnValidHashMapWithProfiles() thr Map profiles = KnowledgeBaseProfile.readKnowledgeBaseProfiles(); assertThat(profiles) - .allSatisfy((key, profile) -> { - assertThat(key).isNotNull(); - }); + .allSatisfy((key, profile) -> + assertThat(key).isNotNull() + ); } diff --git a/inception-kb/src/test/java/de/tudarmstadt/ukp/inception/kb/KnowledgeBaseServiceImplQualifierIntegrationTest.java b/inception-kb/src/test/java/de/tudarmstadt/ukp/inception/kb/KnowledgeBaseServiceImplQualifierIntegrationTest.java index 8a9abb625dc..b9ddb469596 100644 --- a/inception-kb/src/test/java/de/tudarmstadt/ukp/inception/kb/KnowledgeBaseServiceImplQualifierIntegrationTest.java +++ b/inception-kb/src/test/java/de/tudarmstadt/ukp/inception/kb/KnowledgeBaseServiceImplQualifierIntegrationTest.java @@ -258,10 +258,10 @@ public void deleteQualifier_WithExistingQualifier_ShouldDeleteQualifier() @Test public void deleteQualifier_WithNonExistentQualifier_ShouldDoNothing() { - assertThatCode(() -> { + assertThatCode(() -> sut.deleteQualifier(kb, - testFixtures.buildQualifier(statement, property, "Test qualifier")); - }).doesNotThrowAnyException(); + testFixtures.buildQualifier(statement, property, "Test qualifier")) + ).doesNotThrowAnyException(); } @Test diff --git a/inception-log/src/main/java/de/tudarmstadt/ukp/inception/log/exporter/LoggedEventExporter.java b/inception-log/src/main/java/de/tudarmstadt/ukp/inception/log/exporter/LoggedEventExporter.java index 4f8791c4571..bd07338601a 100644 --- a/inception-log/src/main/java/de/tudarmstadt/ukp/inception/log/exporter/LoggedEventExporter.java +++ b/inception-log/src/main/java/de/tudarmstadt/ukp/inception/log/exporter/LoggedEventExporter.java @@ -93,9 +93,9 @@ public void exportData(ProjectExportRequest aRequest, ExportedProject aExProject // Set up a map of document IDs to document names because we export by name and not // by ID. Map documentNameIndex = new HashMap<>(); - documentService.listSourceDocuments(project).forEach(doc -> { - documentNameIndex.put(doc.getId(), doc.getName()); - }); + documentService.listSourceDocuments(project).forEach(doc -> + documentNameIndex.put(doc.getId(), doc.getName()) + ); File eventLog = new File(aFile, EVENT_LOG); eventLog.createNewFile(); diff --git a/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/Predictions.java b/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/Predictions.java index d7fe0c014d6..585e6a5f1bc 100644 --- a/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/Predictions.java +++ b/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/Predictions.java @@ -162,12 +162,12 @@ public Optional getPrediction(SourceDocument aDocument, in */ public void putPredictions(long aLayerId, List aPredictions) { - aPredictions.forEach(prediction -> { + aPredictions.forEach(prediction -> predictions.put(new ExtendedId(user.getUsername(), project.getId(), prediction.getDocumentName(), aLayerId, prediction.getOffset(), - prediction.getRecommenderId(), prediction.getId(), -1), prediction); + prediction.getRecommenderId(), prediction.getId(), -1), prediction) - }); + ); } public Project getProject() { diff --git a/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/evaluation/SimulationLearningCurvePanel.java b/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/evaluation/SimulationLearningCurvePanel.java index 5f8491bfcc4..578aafc14eb 100644 --- a/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/evaluation/SimulationLearningCurvePanel.java +++ b/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/evaluation/SimulationLearningCurvePanel.java @@ -137,9 +137,9 @@ protected void onUpdate(AjaxRequestTarget _target) // for the selected recommender is plotted in the hCart Panel @SuppressWarnings({ "unchecked", "rawtypes" }) LambdaAjaxButton startButton = new LambdaAjaxButton(MID_SIMULATION_START_BUTTON, - (_target, _form) -> { - startEvaluation(_target, _form ); - }); + (_target, _form) -> + startEvaluation(_target, _form ) + ); form.add(startButton); } diff --git a/inception-scheduling/src/main/java/de/tudarmstadt/ukp/inception/scheduling/Task.java b/inception-scheduling/src/main/java/de/tudarmstadt/ukp/inception/scheduling/Task.java index 9e78df79855..127e4756afa 100644 --- a/inception-scheduling/src/main/java/de/tudarmstadt/ukp/inception/scheduling/Task.java +++ b/inception-scheduling/src/main/java/de/tudarmstadt/ukp/inception/scheduling/Task.java @@ -19,10 +19,12 @@ import static org.apache.commons.lang3.Validate.notNull; +import java.util.List; import java.util.Objects; import java.util.concurrent.atomic.AtomicInteger; import de.tudarmstadt.ukp.clarin.webanno.model.Project; +import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.clarin.webanno.security.model.User; public abstract class Task @@ -99,4 +101,5 @@ public boolean equals(Object o) { public int hashCode() { return Objects.hash(user, project); } + } diff --git a/inception-ui-external-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/externalsearch/project/DocumentRepositoryEditorPanel.java b/inception-ui-external-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/externalsearch/project/DocumentRepositoryEditorPanel.java index d88b42bfe08..a706975bda8 100644 --- a/inception-ui-external-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/externalsearch/project/DocumentRepositoryEditorPanel.java +++ b/inception-ui-external-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/externalsearch/project/DocumentRepositoryEditorPanel.java @@ -86,9 +86,9 @@ public DocumentRepositoryEditorPanel(String aId, IModel aProject, add(form); form.add(new TextField("name") - .add(new LambdaAjaxFormSubmittingBehavior("change", t -> { - t.add(form); - }))); + .add(new LambdaAjaxFormSubmittingBehavior("change", t -> + t.add(form) + ))); IModel> typeModel = LambdaModelAdapter.of(() -> { return listTypes().stream() diff --git a/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/AbstractInfoPanel.java b/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/AbstractInfoPanel.java index cc120bd5e24..c333d0477d3 100644 --- a/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/AbstractInfoPanel.java +++ b/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/AbstractInfoPanel.java @@ -179,21 +179,21 @@ public ViewMode(String id, CompoundPropertyModel compoundMod // button for deleting the KBObject LambdaAjaxLink deleteButton = new LambdaAjaxLink("delete", - AbstractInfoPanel.this::confirmActionDelete).onConfigure((_this) -> { + AbstractInfoPanel.this::confirmActionDelete).onConfigure((_this) -> _this.setVisible(kbObjectModel.getObject() != null - && isNotEmpty(kbObjectModel.getObject().getIdentifier())); - }); + && isNotEmpty(kbObjectModel.getObject().getIdentifier())) + ); deleteButton.add(new Label("label", new ResourceModel(getDeleteButtonResourceKey()))); deleteButton.add(new WriteProtectionBehavior(kbModel)); add(deleteButton); // button for creating a new subclass that is only visible for concepts LambdaAjaxLink createSubclassButton = new LambdaAjaxLink("createSubclass", - AbstractInfoPanel.this::actionCreateSubclass).onConfigure((_this) -> { + AbstractInfoPanel.this::actionCreateSubclass).onConfigure((_this) -> _this.setVisible(kbObjectModel.getObject() != null && isNotEmpty(kbObjectModel.getObject().getIdentifier()) - && kbObjectModel.getObject() instanceof KBConcept); - }); + && kbObjectModel.getObject() instanceof KBConcept) + ); createSubclassButton.add(new Label("subclassLabel", new ResourceModel(getCreateSubclassButtonResourceKey()))); createSubclassButton.add(new WriteProtectionBehavior(kbModel)); diff --git a/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/feature/QualifierFeatureEditor.java b/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/feature/QualifierFeatureEditor.java index 41d63d23c97..4ca8d121147 100644 --- a/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/feature/QualifierFeatureEditor.java +++ b/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/feature/QualifierFeatureEditor.java @@ -278,9 +278,9 @@ private AutoCompleteTextField createMentionKBLinkTextField( { AnnotationFeature linkedAnnotationFeature = getLinkedAnnotationFeature(); - qualifierModel = new LambdaModelAdapter<>(() -> this.getSelectedKBItem(aItem), (v) -> { - this.setSelectedKBItem((KBHandle) v, aItem, linkedAnnotationFeature); - }); + qualifierModel = new LambdaModelAdapter<>(() -> this.getSelectedKBItem(aItem), (v) -> + this.setSelectedKBItem((KBHandle) v, aItem, linkedAnnotationFeature) + ); AutoCompleteTextField field = new AutoCompleteTextField("value", qualifierModel, new TextRenderer("uiLabel"), KBHandle.class) diff --git a/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/project/AccessSpecificSettingsPanel.java b/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/project/AccessSpecificSettingsPanel.java index 52eaf343630..1ca5ecff35d 100644 --- a/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/project/AccessSpecificSettingsPanel.java +++ b/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/project/AccessSpecificSettingsPanel.java @@ -184,9 +184,9 @@ private TextField defaultDataset(String aId, IModel model) { IModel adapter = new LambdaModelAdapter(() -> { return model.getObject() != null ? model.getObject().stringValue() : null; - }, str -> { - model.setObject(str != null ? SimpleValueFactory.getInstance().createIRI(str) : null); - }); + }, str -> + model.setObject(str != null ? SimpleValueFactory.getInstance().createIRI(str) : null) + ); TextField defaultDataset = new TextField<>(aId, adapter); defaultDataset.add(Validators.IRI_VALIDATOR); return defaultDataset; diff --git a/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/project/KnowledgeBaseIriPanel.java b/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/project/KnowledgeBaseIriPanel.java index f45a5697f0f..640fa48fb45 100644 --- a/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/project/KnowledgeBaseIriPanel.java +++ b/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/project/KnowledgeBaseIriPanel.java @@ -146,9 +146,9 @@ private ComboBox buildComboBox(String id, IModel model, List i IModel adapter = new LambdaModelAdapter( () -> { return model.getObject() != null ? model.getObject().stringValue() : null; }, - str -> { + str -> model.setObject(str != null ? SimpleValueFactory.getInstance().createIRI(str) : - null); }); + null) ); ComboBox comboBox = new ComboBox<>(id, adapter, choices); comboBox.add(LambdaBehavior.enabledWhen(() -> diff --git a/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/project/RootConceptsPanel.java b/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/project/RootConceptsPanel.java index f8156bd0724..4d3db0312fe 100644 --- a/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/project/RootConceptsPanel.java +++ b/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/project/RootConceptsPanel.java @@ -71,9 +71,9 @@ protected void populateItem(ListItem item) { Form conceptForm = new Form("conceptForm"); conceptForm.add(buildTextField("textField", item.getModel())); - conceptForm.add(new LambdaAjaxLink("removeConcept", t -> { - RootConceptsPanel.this.actionRemoveConcept(t, item.getModelObject()); - })); + conceptForm.add(new LambdaAjaxLink("removeConcept", t -> + RootConceptsPanel.this.actionRemoveConcept(t, item.getModelObject()) + )); item.add(conceptForm); } From 4fb0e65ee3e660cea3fe085f5104439e71b24ecf Mon Sep 17 00:00:00 2001 From: Marcel de Boer Date: Fri, 1 Nov 2019 10:22:09 +0100 Subject: [PATCH 051/453] 1054 - Dealing with many results in search sidebar - checkstyle - use LinkedHashMap for storing search results, so that results from the same document will stay together --- .../search/index/mtas/MtasDocumentIndex.java | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/inception-search-mtas/src/main/java/de/tudarmstadt/ukp/inception/search/index/mtas/MtasDocumentIndex.java b/inception-search-mtas/src/main/java/de/tudarmstadt/ukp/inception/search/index/mtas/MtasDocumentIndex.java index a54a3705d9c..d275178a923 100644 --- a/inception-search-mtas/src/main/java/de/tudarmstadt/ukp/inception/search/index/mtas/MtasDocumentIndex.java +++ b/inception-search-mtas/src/main/java/de/tudarmstadt/ukp/inception/search/index/mtas/MtasDocumentIndex.java @@ -30,16 +30,7 @@ import java.io.Reader; import java.io.StringReader; import java.text.BreakIterator; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.ListIterator; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.TreeMap; +import java.util.*; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.StringUtils; @@ -412,7 +403,7 @@ private Map> doQuery(IndexReader aIndexReader, SearchQueryRequest aRequest, String field, MtasSpanQuery q) throws IOException { - Map> results = new TreeMap<>(FEATUREVALUE_COMPARATOR); + Map> results = new LinkedHashMap<>(); ListIterator leafReaderContextIterator = aIndexReader.leaves() .listIterator(); From cf24290802bf31a2dc8bfa7fe10101d5e26a30e7 Mon Sep 17 00:00:00 2001 From: Marcel de Boer Date: Fri, 1 Nov 2019 11:04:02 +0100 Subject: [PATCH 052/453] 1054 - Dealing with many results in search sidebar - checkstyle --- .../search/index/mtas/MtasDocumentIndex.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/inception-search-mtas/src/main/java/de/tudarmstadt/ukp/inception/search/index/mtas/MtasDocumentIndex.java b/inception-search-mtas/src/main/java/de/tudarmstadt/ukp/inception/search/index/mtas/MtasDocumentIndex.java index d275178a923..2731cadd3c5 100644 --- a/inception-search-mtas/src/main/java/de/tudarmstadt/ukp/inception/search/index/mtas/MtasDocumentIndex.java +++ b/inception-search-mtas/src/main/java/de/tudarmstadt/ukp/inception/search/index/mtas/MtasDocumentIndex.java @@ -30,7 +30,16 @@ import java.io.Reader; import java.io.StringReader; import java.text.BreakIterator; -import java.util.*; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.StringUtils; From 37fe314cfeb1ae3335b24ada2292866387fdc23d Mon Sep 17 00:00:00 2001 From: Marcel de Boer Date: Sat, 2 Nov 2019 08:19:18 +0100 Subject: [PATCH 053/453] 1054 - Dealing with many results in search sidebar - fix dependency --- inception-search-core/pom.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/inception-search-core/pom.xml b/inception-search-core/pom.xml index 53902a9f045..c2584ffb07b 100644 --- a/inception-search-core/pom.xml +++ b/inception-search-core/pom.xml @@ -77,6 +77,10 @@ org.springframework spring-context + + org.springframework.boot + spring-boot + From 2fd1ada2b70c65d7cb7b1a3f7c519fde7a611075 Mon Sep 17 00:00:00 2001 From: Marcel de Boer Date: Sat, 2 Nov 2019 10:27:34 +0100 Subject: [PATCH 054/453] 1054 - Dealing with many results in search sidebar - add missing licence --- .../sidebar/SearchResultsProviderWrapper.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchResultsProviderWrapper.java b/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchResultsProviderWrapper.java index b899e436d20..892e08dcde6 100644 --- a/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchResultsProviderWrapper.java +++ b/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchResultsProviderWrapper.java @@ -1,3 +1,20 @@ +/* + * Copyright 2019 + * Ubiquitous Knowledge Processing (UKP) Lab + * Technische Universität Darmstadt + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package de.tudarmstadt.ukp.inception.app.ui.search.sidebar; import java.util.ArrayList; From 828a2597601b17a51ee3f1ef07be7fa703c6fc5d Mon Sep 17 00:00:00 2001 From: Sibgha360 Date: Mon, 4 Nov 2019 09:48:25 +0100 Subject: [PATCH 055/453] #1270 - Hints for new users - used/created one cookie (not using the page name) - There is only one flow that is started from the first page - used the js from git added notice.txt - context param passed from wicket to to create the cookie - In case of error, event generated. Event handled in the TutorialFooterPanel.java --- .../recommendation/event/ErrorForJSEvent.java | 40 ++ .../project/RecommenderEditorPanel.java | 8 + inception-ui-core/NOTICE.txt | 7 + inception-ui-core/pom.xml | 4 + .../projectlist/ProjectsOverviewPage.html | 2 + .../projectlist/ProjectsOverviewPage.java | 25 +- .../ProjectsOverviewPage.properties | 1 + .../ui/core/footer/TutorialFooterPanel.java | 15 + .../ui/core/footer/resources/tutorial.js | 378 ++++++++++++++---- .../inception/ui/core/menubar/MenuBar.html | 6 - .../inception/ui/core/menubar/MenuBar.java | 30 +- 11 files changed, 393 insertions(+), 123 deletions(-) create mode 100644 inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/event/ErrorForJSEvent.java create mode 100644 inception-ui-core/NOTICE.txt diff --git a/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/event/ErrorForJSEvent.java b/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/event/ErrorForJSEvent.java new file mode 100644 index 00000000000..a7e6b32130d --- /dev/null +++ b/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/event/ErrorForJSEvent.java @@ -0,0 +1,40 @@ +package de.tudarmstadt.ukp.inception.recommendation.event; +import org.apache.wicket.ajax.AjaxRequestTarget; +import org.springframework.context.ApplicationEvent; + +import de.tudarmstadt.ukp.inception.recommendation.api.model.Recommender; + +public class ErrorForJSEvent +{ + private static final long serialVersionUID = 4618078923202025558L; + + private final String errorMessage; + public AjaxRequestTarget target; + + public ErrorForJSEvent(Object aSource, String aerrorMessage) + { + errorMessage = aerrorMessage; + } + + public String getErrorMessage() + { + return errorMessage; + } + + @Override public String toString() + { + return errorMessage; + } + + public AjaxRequestTarget getTarget() { + return target; + } + + public void setTarget(AjaxRequestTarget target) { + this.target = target; + } + + public static long getSerialversionuid() { + return serialVersionUID; + } +} diff --git a/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/project/RecommenderEditorPanel.java b/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/project/RecommenderEditorPanel.java index 2cb74680fe7..2c11f16211a 100644 --- a/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/project/RecommenderEditorPanel.java +++ b/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/project/RecommenderEditorPanel.java @@ -35,6 +35,7 @@ import org.apache.wicket.Component; import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.ajax.markup.html.form.AjaxButton; +import org.apache.wicket.event.Broadcast; import org.apache.wicket.feedback.IFeedback; import org.apache.wicket.markup.html.WebMarkupContainer; import org.apache.wicket.markup.html.form.CheckBox; @@ -74,6 +75,7 @@ import de.tudarmstadt.ukp.inception.recommendation.api.RecommenderFactoryRegistry; import de.tudarmstadt.ukp.inception.recommendation.api.model.Recommender; import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommendationEngineFactory; +import de.tudarmstadt.ukp.inception.recommendation.event.ErrorForJSEvent; public class RecommenderEditorPanel extends Panel @@ -264,6 +266,12 @@ protected void onModelChanged() @Override protected void onError(AjaxRequestTarget aTarget) { + ErrorForJSEvent event = new ErrorForJSEvent(this, "add recommender error"); + event.setTarget(aTarget); +// appEventPublisherHolder.get().publishEvent(event); + + send(getPage(), Broadcast.BREADTH, event); + aTarget.addChildren(getPage(), IFeedback.class); } diff --git a/inception-ui-core/NOTICE.txt b/inception-ui-core/NOTICE.txt new file mode 100644 index 00000000000..aa784c08837 --- /dev/null +++ b/inception-ui-core/NOTICE.txt @@ -0,0 +1,7 @@ +== Enjoyhint + +File: src/main/java/de/tudarmstadt/ukp/inception/ui/core/footer/resources/enjoyhint.js + +The file is from xbsoftware and are licensed under the MIT. + +Original source: https://github.com/xbsoftware/enjoyhint \ No newline at end of file diff --git a/inception-ui-core/pom.xml b/inception-ui-core/pom.xml index 60cbd57d696..8f4e5eb7836 100644 --- a/inception-ui-core/pom.xml +++ b/inception-ui-core/pom.xml @@ -170,6 +170,10 @@ 2.5 provided + + de.tudarmstadt.ukp.inception.app + inception-recommendation + diff --git a/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/dashboard/projectlist/ProjectsOverviewPage.html b/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/dashboard/projectlist/ProjectsOverviewPage.html index c286f3f8b40..87298a8c510 100644 --- a/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/dashboard/projectlist/ProjectsOverviewPage.html +++ b/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/dashboard/projectlist/ProjectsOverviewPage.html @@ -23,6 +23,8 @@
    + +
    diff --git a/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/dashboard/projectlist/ProjectsOverviewPage.java b/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/dashboard/projectlist/ProjectsOverviewPage.java index 377848ee3d8..1211d27c930 100644 --- a/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/dashboard/projectlist/ProjectsOverviewPage.java +++ b/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/dashboard/projectlist/ProjectsOverviewPage.java @@ -38,6 +38,8 @@ import java.util.stream.Collectors; import java.util.zip.ZipFile; +import javax.servlet.ServletContext; + import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.exception.ExceptionUtils; import org.apache.wicket.Component; @@ -104,6 +106,7 @@ public class ProjectsOverviewPage private static final String MID_LEAVE_PROJECT = "leaveProject"; private static final String MID_CONFIRM_LEAVE = "confirmLeave"; private static final String MID_EMPTY_LIST_LABEL = "emptyListLabel"; + private static final String MID_START_TUTORIAL= "startTutorial"; private static final long serialVersionUID = -2159246322262294746L; @@ -112,7 +115,8 @@ public class ProjectsOverviewPage private @SpringBean ProjectService projectService; private @SpringBean UserDao userRepository; private @SpringBean ProjectExportService exportService; - + private @SpringBean ServletContext context; + private BootstrapFileInputField fileUpload; private WebMarkupContainer projectListContainer; private WebMarkupContainer roleFilters; @@ -125,6 +129,7 @@ public ProjectsOverviewPage() { add(projectListContainer = createProjectList()); add(createNewProjectLink()); + add(createStartTutorialLink()); add(createImportProjectForm()); add(roleFilters = createRoleFilters()); add(confirmLeaveDialog = new ConfirmationDialog(MID_CONFIRM_LEAVE, @@ -150,6 +155,24 @@ private LambdaAjaxLink createNewProjectLink() return newProjectLink; } + private LambdaAjaxLink createStartTutorialLink() + { + LambdaAjaxLink startTutorialLink = new LambdaAjaxLink(MID_START_TUTORIAL, + this::startTutorial); + + add(startTutorialLink); + + return startTutorialLink; + } + + private void startTutorial(AjaxRequestTarget aTarget) + { + String contextPath = "inception-app-webapp"; + contextPath = context.getContextPath(); + + aTarget.appendJavaScript(" startTutorial('"+contextPath+"'); "); + } + private Form createImportProjectForm() { Form importProjectForm = new Form<>(MID_IMPORT_PROJECT_FORM); diff --git a/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/dashboard/projectlist/ProjectsOverviewPage.properties b/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/dashboard/projectlist/ProjectsOverviewPage.properties index 84ee9a37827..8d7c6699a84 100644 --- a/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/dashboard/projectlist/ProjectsOverviewPage.properties +++ b/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/dashboard/projectlist/ProjectsOverviewPage.properties @@ -19,6 +19,7 @@ page.help=Help page.help.link=doc/user-guide.html#sect_menu newProject=Create new project ... +startTutorial=Start Tutorial leaveDialog.title=Leave Project leaveDialog.text=Do you really want to leave the project? You cannot join it again without a manager's approval. diff --git a/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/footer/TutorialFooterPanel.java b/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/footer/TutorialFooterPanel.java index 62ea8d521de..0fe425d9c07 100644 --- a/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/footer/TutorialFooterPanel.java +++ b/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/footer/TutorialFooterPanel.java @@ -17,16 +17,21 @@ */ package de.tudarmstadt.ukp.inception.ui.core.footer; +import org.apache.wicket.ajax.AjaxRequestTarget; +import org.apache.wicket.event.IEvent; import org.apache.wicket.markup.head.CssHeaderItem; import org.apache.wicket.markup.head.IHeaderResponse; import org.apache.wicket.markup.head.JavaScriptHeaderItem; import org.apache.wicket.markup.html.panel.Panel; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.wicketstuff.event.annotation.OnEvent; import de.agilecoders.wicket.webjars.request.resource.WebjarsCssResourceReference; import de.agilecoders.wicket.webjars.request.resource.WebjarsJavaScriptResourceReference; import de.tudarmstadt.ukp.clarin.webanno.ui.core.page.WebAnnoJavascriptReference; +import de.tudarmstadt.ukp.inception.recommendation.event.ErrorForJSEvent; +import de.tudarmstadt.ukp.inception.recommendation.sidebar.DropDownEvent; import de.tudarmstadt.ukp.inception.ui.core.footer.resources.EnjoyHintJsReference; import de.tudarmstadt.ukp.inception.ui.core.footer.resources.TutorialJavascriptReference; @@ -62,4 +67,14 @@ public void renderHead(IHeaderResponse aResponse) { aResponse.render(JavaScriptHeaderItem.forReference(TutorialJavascriptReference.get())); } + @Override + public void onEvent(IEvent aEvent) { + if (aEvent.getPayload() instanceof ErrorForJSEvent){ + ErrorForJSEvent dEvent = (ErrorForJSEvent) aEvent.getPayload(); + + AjaxRequestTarget target = dEvent.getTarget(); + //setResponsePage(getPage()); + target.appendJavaScript("skipit()"); + } + } } diff --git a/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/footer/resources/tutorial.js b/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/footer/resources/tutorial.js index de741cf6668..3365357cb71 100644 --- a/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/footer/resources/tutorial.js +++ b/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/footer/resources/tutorial.js @@ -15,62 +15,53 @@ #See the License for the specific language governing permissions and #limitations under the License. */ -$(document).ready(function() { - var enjoyhint_instance = new EnjoyHint({ - onSkip : function() { - // reset all the cookies - } - }); + var cName = "tutorialSession"; + var contextPath = ""; + var isRecommenderError=false; + + var enjoyhint_instance = new EnjoyHint({ + onSkip : function() { + // reset all the cookies + } + }); + + +$(document).ready(function() { var enjoyhint_script_steps; var currentPage = window.location.pathname; var projectId = getUrlParameter('p'); - var cName = document.location.pathname.match(/[^\/]+$/)[0]; - //createCookieName(currentPage); + // var cName = document.location.pathname.match(/[^\/]+$/)[0]; + // createCookieName(currentPage); var ps = getCookie(cName); - if (currentPage.includes("projects.html")) { - ps = getCookie(cName); - if (ps == 'true') { - } else if (ps == "") { + if (currentPage.includes("projects.html") && ps == "tutorialStarted") + { enjoyhint_instance = new EnjoyHint({ onEnd : function() { - setCookie(cName, 'projectCreated'); + setCookie(cName, 'tutorialStarted',contextPath); }, onSkip : function() { - setCookie(cName, true); + setCookie(cName, true, contextPath); } }); enjoyhint_script_steps = createFirstPageRoutine(); enjoyhint_instance.set(enjoyhint_script_steps); enjoyhint_instance.runScript(); - } else if (ps == "projectCreated") { - enjoyhint_instance = new EnjoyHint({ - onEnd : function() { - setCookie(cName, true); - }, - onSkip : function() { - setCookie(cName, true); - } - }); - enjoyhint_script_steps = createFirstPageRoutinePart2(); - enjoyhint_instance.set(enjoyhint_script_steps); - enjoyhint_instance.runScript(); - - } - } else if (currentPage.includes("projectsetting.html") - && projectId == 'NEW' && ps != "projectSaved") { + } + + else if (currentPage.includes("projectsetting.html") && projectId == 'NEW' && ps == "tutorialStarted") + { enjoyhint_instance = new EnjoyHint({ onEnd : function() { // reset all the cookies - setCookie(cName, 'projectSaved'); + setCookie(cName, 'projectSaved', contextPath); }, onSkip : function() { - setCookie(cName, true); + setCookie(cName, true, contextPath); } - }); ps = getCookie(cName); @@ -78,90 +69,172 @@ $(document).ready(function() { } else { ps = 'projectSaved'; if (ps != "" && ps != null) { - setCookie(cName, ps, 365); + setCookie(cName, ps, contextPath); } enjoyhint_script_steps = createNewProjectRoutine(); enjoyhint_instance.set(enjoyhint_script_steps); enjoyhint_instance.runScript(); - } - } else if (currentPage.includes("projectsetting.html") - && projectId == 'NEW' && ps == "projectSaved") { - + } + } + + else if (currentPage.includes("projectsetting.html") && projectId == 'NEW' && ps == "projectSaved") + { //save the project name in the cookie var projectName = $('[name="p::name"]').val() - setCookie("projectName", projectName, 365); + setCookie("projectName", projectName, contextPath); //re-initialize the instance - enjoyhint_instance = new EnjoyHint({}); + enjoyhint_instance = new EnjoyHint({ + onEnd : function() { + setCookie(cName, 'projectCreated',contextPath); + } + }); //reset the projectsetting cookie so the tutorial shows up on the settings page later ps = ""; if (ps != "" && ps != null) { - setCookie(cName, ps, 365); + setCookie(cName, ps, contextPath); } enjoyhint_script_steps = createProjectSavedRoutine(); enjoyhint_instance.set(enjoyhint_script_steps); enjoyhint_instance.runScript(); - - } else if (currentPage.includes("project.html")) { - var ps = getCookie(cName); - if (ps != "") { - } else { - ps = true; - if (ps != "" && ps != null) { - setCookie(cName, ps, 365); + } + + else if (currentPage.includes("projects.html") && ps == "projectCreated") + { + enjoyhint_instance = new EnjoyHint({ + onEnd : function() { + setCookie(cName, "projectView", contextPath); + }, + onSkip : function() { + setCookie(cName, true, contextPath); + } + }); + enjoyhint_script_steps = createFirstPageRoutinePart2(); + enjoyhint_instance.set(enjoyhint_script_steps); + enjoyhint_instance.runScript(); + } + + + else if (currentPage.includes("project.html") && ps == "projectView") + { + enjoyhint_instance = new EnjoyHint({ + onEnd : function() { + setCookie(cName, "projectsettingView", contextPath); + }, + onSkip : function() { } - + }); + enjoyhint_script_steps = createDashboardRoutine(); enjoyhint_instance.set(enjoyhint_script_steps); enjoyhint_instance.runScript(); - } - } else if (currentPage.includes("projectsetting.html")) { - var ps = getCookie(cName); + } + + else if (currentPage.includes("projectsetting.html") && ps == "projectsettingView") { - if (ps == 'true') { - // alert("PS Visited? " + ps); - } - //TODO do we need it - else if (ps == "recommenderSaved") { - ps = true; - if (ps != "" && ps != null) { - setCookie(cName, ps, 365); - } - enjoyhint_script_steps = createAnnotationRoutine(); + enjoyhint_instance = new EnjoyHint({ + + onEnd : function() { + setCookie(cName, "projectsettingConfigured", contextPath); + isRecommenderError=false; + }, + onSkip : function() { + setCookie(cName, true, contextPath); + } + + }); + + enjoyhint_script_steps = createSettingsRoutine(enjoyhint_instance); enjoyhint_instance.set(enjoyhint_script_steps); enjoyhint_instance.runScript(); - } - else { + } + + //recommender saving error + else if (currentPage.includes("projectsetting.html") && ps == "recommenderError") { + debugger; enjoyhint_instance = new EnjoyHint({ onEnd : function() { - ps = "true"; - if (ps != "" && ps != null) { - setCookie(cName, ps, 365); - } + setCookie(cName, "projectsettingConfigured", contextPath); }, onSkip : function() { - setCookie(cName, true); + setCookie(cName, true, contextPath); } }); - enjoyhint_script_steps = createSettingsRoutine(enjoyhint_instance); + enjoyhint_script_steps = createAddRecommenderRoutine(enjoyhint_instance); enjoyhint_instance.set(enjoyhint_script_steps); enjoyhint_instance.runScript(); - } + } + + else if(currentPage.includes("projectsetting.html") && ps == "recommenderAdded") + { + enjoyhint_instance = new EnjoyHint({ + onEnd : function() { + setCookie(cName, "projectsettingConfigured", contextPath); + }, + onSkip : function() { + setCookie(cName, true, contextPath); + } + }); + + enjoyhint_script_steps = createAddRecommenderRoutine(enjoyhint_instance); + enjoyhint_instance.set( + [ + { + 'click [href=\'./project.html\']:last' : 'Now, lets go to the Dashboard', + } + ] + ); + enjoyhint_instance.runScript(); + } + + else if (currentPage.includes("project.html") && ps == "projectsettingConfigured") + { + enjoyhint_instance = new EnjoyHint({ + onEnd : function() { + setCookie(cName, "ended", contextPath); + }, + onSkip : function() { + setCookie(cName, "ended", contextPath); + } + }); + enjoyhint_script_steps = createLastRoutine(); + enjoyhint_instance.set(enjoyhint_script_steps); + enjoyhint_instance.runScript(); } }); -function setCookie(cname, cvalue, exdays) { +function startTutorial(aContextPath) { + contextPath = aContextPath; + var enjoyhint_instance = new EnjoyHint({ + onEnd : function() { + //setCookie(cName, 'projectNotSaved',contextPath); + }, + onSkip : function() { + setCookie(cName, true, contextPath); + } + }); + + enjoyhint_script_steps = createFirstPageRoutine(); + enjoyhint_instance.set(enjoyhint_script_steps); + + setCookie(cName, 'tutorialStarted',contextPath); + + enjoyhint_instance.runScript(); +} + + +function setCookie(cname, cvalue, contextPath) { var d = new Date(); - d.setTime(d.getTime() + (exdays * 24 * 60 * 60 * 1000)); + d.setTime(d.getTime() + (1 * 24 * 60 * 60 * 1000)); var expires = "expires=" + d.toUTCString(); - document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/"; + document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/"+contextPath; } function getCookie(cname) { @@ -181,7 +254,7 @@ function getCookie(cname) { function createNewProjectRoutine() { var a = [ { - 'key [name=\'p::name\']' : 'Write the name of the project and press Enter', + 'next [name=\'p::name\']' : 'Write the name of the project and press Enter', } // , { // 'click [type=submit]' : 'Click save' @@ -228,10 +301,8 @@ function createFirstPageRoutinePart2() { }, { "event": "click", - "selector": t[0], + "selector": ".list-group-item", "description": "Click on the project to get started.", - - } ]; return a; @@ -260,10 +331,49 @@ function createDashboardRoutine() { return a; } +function createAddRecommenderRoutine(enjoyHint) { + var a = [ + { + 'next .col-sm-9' : "Already exists. Try again!", + onBeforeStart:function(){ + + $("[name=\'save\']").on('click', + function(e) { + debugger; + enjoyHint.trigger('next'); + enjoyHint.trigger('next'); + + if(setItToError) + { + setCookie(cName, "recommenderError", contextPath); + } + +// goBackToDashboardRoutine(); + +// if(!isRecommenderError) +// {enjoyHint.trigger('next'); } + }); + + }, + + }, + { + 'click [value=Save]:last' : 'Now, lets go to the Dashboard', + + } , + { + 'click [href=\'./project.html\']:last' : 'Now, lets go to the Dashboard', + } + ]; + + return a; +} + +var setItToError = false; function createSettingsRoutine(enjoyHint) { var a = [ { - 'click .tab2' : 'Click here to add a document to the project', + 'click .tab1' : 'Click here to add a document to the project', }, { 'next [class=flex-h-container]' : "Upload a file and import. Then click Next.", @@ -297,21 +407,43 @@ function createSettingsRoutine(enjoyHint) { }, }, - { - "event_type": "custom", - "event": "event-save-recommender", - "selector": "[method=post]", - "description": "Fill in the details and click save", + { + "event .col-sm-9" : "Fill in the details and click save", onBeforeStart:function(){ + //setCookie(cName, "recommenderError", contextPath); $("[name=\'save\']").on('click', function(e) { + debugger; enjoyHint.trigger('next'); + enjoyHint.trigger('next'); + + if(setItToError) + { + setCookie(cName, "recommenderError", contextPath); + } + +// goBackToDashboardRoutine(); + +// if(!isRecommenderError) +// {enjoyHint.trigger('next'); } }); }, }, +// { +// 'click [value=Save]:last' : 'Now, lets go to the Dashboard', +//// onBeforeStart : function() { +//// setCookie(cName, "recommenderError", contextPath); +//// $("[name=\'save\']").on('click', function(e) { +//// debugger; +//// //if(doNext) +//// //enjoyHint.trigger('next'); +//// }); +//// } +// }, + { 'click [href=\'./project.html\']:last' : 'Now, lets go to the Dashboard', } @@ -320,6 +452,28 @@ function createSettingsRoutine(enjoyHint) { return a; } +function goBackToDashboardRoutine() +{ + doNext = false; + debugger; + var enjoyhint_instance = new EnjoyHint({ + onEnd : function() { + setCookie(cName, "projectsettingConfigured", contextPath); + }, + onSkip : function() { + setCookie(cName, true, contextPath); + } + }); + + enjoyhint_instance.set( + [ + { + 'click [href=\'./project.html\']:last' : 'Now, lets go to the Dashboard', + } + ] + ); + enjoyhint_instance.runScript();} + function createSettingsRoutine2() { var a = [ { @@ -343,10 +497,15 @@ function createProjectSavedRoutine() { return a; } -function createAnnotationRoutine() { +function createLastRoutine() { var a = [ { - 'click [href=\'./project.html\']:last' : 'Now, lets go to go to the Dashboard', + 'click .flex-sidebar' : 'You can start exploring the website further', + + 'skipButton' : { + className : "mySkip", + text : "Thanks!" + } } ]; return a; @@ -370,4 +529,49 @@ function getUrlParameter(sParam) { } }; +function skipit(){ + debugger; + setCookie(cName, "recommenderError", contextPath); + location.reload(); +// debugger; + setItToError = true; + +// enjoyhint_instance.trigger('skip'); + +// doNext = true; +// +// var enjoyhint_instance = new EnjoyHint({ +// +// onEnd : function() { +// setCookie(cName, "recommenderAdded", contextPath); +// }, +// onSkip : function() { +// //setCookie(cName, true, contextPath); +// } +// +// }); +// +// enjoyhint_script_steps = createAddRecommenderRoutine(enjoyhint_instance); +// enjoyhint_instance.set(enjoyhint_script_steps); +// enjoyhint_instance.runScript(); +} +function skipIt() { + debugger; + + //enjoyhint_instance.trigger('skip'); + + enjoyhint_instance = new EnjoyHint({ + + onEnd : function() { + setCookie(cName, "projectsettingConfigured", contextPath); + }, + onSkip : function() { + //setCookie(cName, true, contextPath); + } + }); + + enjoyhint_script_steps = createAddRecommenderRoutine(enjoyhint_instance); + enjoyhint_instance.set(enjoyhint_script_steps); + enjoyhint_instance.runScript(); +} diff --git a/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/menubar/MenuBar.html b/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/menubar/MenuBar.html index ab51b626471..2f09d16b0b9 100644 --- a/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/menubar/MenuBar.html +++ b/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/menubar/MenuBar.html @@ -86,12 +86,6 @@

    - - diff --git a/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/menubar/MenuBar.java b/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/menubar/MenuBar.java index fe4ab3a2abe..bbb6a3af2c7 100644 --- a/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/menubar/MenuBar.java +++ b/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/menubar/MenuBar.java @@ -19,18 +19,12 @@ import static de.tudarmstadt.ukp.clarin.webanno.support.lambda.LambdaBehavior.visibleWhen; -import javax.servlet.http.Cookie; - import org.apache.wicket.Session; import org.apache.wicket.markup.html.link.BookmarkablePageLink; -import org.apache.wicket.request.cycle.RequestCycle; -import org.apache.wicket.request.http.WebRequest; -import org.apache.wicket.request.http.WebResponse; import org.apache.wicket.spring.injection.annot.SpringBean; import de.tudarmstadt.ukp.clarin.webanno.api.ProjectService; import de.tudarmstadt.ukp.clarin.webanno.security.UserDao; -import de.tudarmstadt.ukp.clarin.webanno.support.lambda.LambdaStatelessLink; import de.tudarmstadt.ukp.inception.ui.core.dashboard.admin.AdminDashboardPage; import de.tudarmstadt.ukp.inception.ui.core.dashboard.project.ProjectDashboardPage; import de.tudarmstadt.ukp.inception.ui.core.dashboard.projectlist.ProjectsOverviewPage; @@ -58,31 +52,9 @@ public MenuBar(String aId) .add(visibleWhen(() -> userRepository.getCurrentUser() != null))); add(new BookmarkablePageLink<>("adminLink", AdminDashboardPage.class) - .add(visibleWhen(this::adminAreaAccessRequired))); - - add(new LambdaStatelessLink("hintLink", () -> showHint()) - .add(visibleWhen(this::adminAreaAccessRequired))); + .add(visibleWhen(this::adminAreaAccessRequired))); } - private Object showHint() - { - WebRequest webRequest = (WebRequest)RequestCycle.get().getRequest(); - WebResponse webResponse = (WebResponse)RequestCycle.get().getResponse(); - - // page name is used as cookie name - String cookieName = webRequest.getOriginalUrl().getSegments().get(0); - Cookie cookie = webRequest.getCookie(cookieName); - - if (cookie == null) { - return null; - } - webResponse.clearCookie(cookie); - - //refresh the page for the hint javascript to execute - setResponsePage(getPage()); - return null; - } - private boolean adminAreaAccessRequired() { return userRepository.getCurrentUser() != null && AdminDashboardPage From 4e1ebe0fbf0d28d0fb381222cedd25bf358ad0b9 Mon Sep 17 00:00:00 2001 From: Marcel de Boer Date: Mon, 4 Nov 2019 11:22:22 +0100 Subject: [PATCH 056/453] 1054 - Dealing with many results in search sidebar - cache already fetched pages, keep track of selected results across multiple pages - show number of results on the current page and total number of results for each group --- .../sidebar/SearchAnnotationSidebar.java | 108 ++++++++++-------- .../sidebar/SearchResultsPagesCache.java | 98 ++++++++++++++++ .../search/sidebar/SearchResultsProvider.java | 30 ++--- .../sidebar/SearchResultsProviderWrapper.java | 18 ++- 4 files changed, 192 insertions(+), 62 deletions(-) create mode 100644 inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchResultsPagesCache.java diff --git a/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebar.java b/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebar.java index 291904ed4cd..20ae9f9ee63 100644 --- a/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebar.java +++ b/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebar.java @@ -24,6 +24,7 @@ import static org.apache.uima.fit.util.CasUtil.selectAt; import java.io.IOException; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -58,7 +59,6 @@ import org.apache.wicket.model.IModel; import org.apache.wicket.model.LoadableDetachableModel; import org.apache.wicket.model.Model; -import org.apache.wicket.model.util.ListModel; import org.apache.wicket.spring.injection.annot.SpringBean; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -123,7 +123,8 @@ public class SearchAnnotationSidebar private IModel targetQuery = Model.of(""); private IModel searchOptions = CompoundPropertyModel.of(new SearchOptions()); - private IModel> groupedSearchResults = new ListModel<>(); + private IModel groupedSearchResults = Model + .of(new SearchResultsPagesCache()); private IModel createOptions = CompoundPropertyModel .of(new CreateAnnotationsOptions()); private IModel deleteOptions = CompoundPropertyModel @@ -145,7 +146,7 @@ public SearchAnnotationSidebar(String aId, IModel aModel, resultsProvider = new SearchResultsProviderWrapper(new SearchResultsProvider(searchService, groupedSearchResults)); - + mainContainer = new WebMarkupContainer("mainContainer"); mainContainer.setOutputMarkupId(true); add(mainContainer); @@ -193,7 +194,7 @@ protected void populateItem(Item item) { ResultsGroup result = item.getModelObject(); item.add(new Label("groupTitle", LoadableDetachableModel - .of(() -> result.getGroupKey() + " (" + result.getResults().size() + ")"))); + .of(() -> createGroupSizeLabel(result)))); item.add(createGroupLevelSelectionCheckBox("selectAllInGroup", result.getGroupKey())); item.add(new SearchResultGroup("group", "resultGroup", SearchAnnotationSidebar.this, @@ -251,14 +252,25 @@ protected void populateItem(Item item) annotationForm.add(visibleWhen(() -> groupedSearchResults.getObject() != null && !groupedSearchResults.getObject().isEmpty())); - + LambdaAjaxButton clearButton = new LambdaAjaxButton<>("clearButton", this::actionClearResults); annotationForm.add(clearButton); - + mainContainer.add(annotationForm); } + private String createGroupSizeLabel(ResultsGroup aResultsGroup) { + StringBuilder sb = new StringBuilder(); + sb.append(aResultsGroup.getGroupKey() + " (" + aResultsGroup.getResults().size()); + // If grouping is activated, paging is deactivated, so we know the total group sizes + if (resultsProvider.isGroupingActivated()) { + sb.append("/" + resultsProvider.groupSize(aResultsGroup.getGroupKey())); + } + sb.append(")"); + return sb.toString(); + } + private DropDownChoice createResultsPerPageSelection(String aId) { List choices = Arrays.stream(searchProperties.getPageSizes()).boxed().collect( @@ -299,10 +311,12 @@ private AjaxCheckBox createGroupLevelSelectionCheckBox(String aId, @Override protected void onUpdate(AjaxRequestTarget target) { - for (ResultsGroup resultsGroup : groupedSearchResults.getObject()) { - if (resultsGroup.getGroupKey().equals(aGroupKey)) { - resultsGroup.getResults().stream() - .forEach(r -> r.setSelectedForAnnotation(getModelObject())); + for (List page : groupedSearchResults.getObject().allPages()) { + for (ResultsGroup resultsGroup : page) { + if (resultsGroup.getGroupKey().equals(aGroupKey)) { + resultsGroup.getResults().stream() + .forEach(r -> r.setSelectedForAnnotation(getModelObject())); + } } } target.add(resultsGroupContainer); @@ -312,19 +326,20 @@ protected void onUpdate(AjaxRequestTarget target) protected void onConfigure() { super.onConfigure(); - for (ResultsGroup resultsGroup : groupedSearchResults.getObject()) { - if (resultsGroup.getGroupKey().equals(aGroupKey)) { - List unselectedResults = resultsGroup.getResults().stream() - .filter(sr -> !sr.isSelectedForAnnotation()) + for (List page : groupedSearchResults.getObject().allPages()) { + for (ResultsGroup resultsGroup : page) { + if (resultsGroup.getGroupKey().equals(aGroupKey)) { + List unselectedResults = resultsGroup.getResults() + .stream().filter(sr -> !sr.isSelectedForAnnotation()) .collect(Collectors.toList()); - if (unselectedResults.isEmpty()) { - setModelObject(true); - } - else { - setModelObject(false); + if (!unselectedResults.isEmpty()) { + setModelObject(false); + return; + } } } } + setModelObject(true); } }; return selectAllCheckBox; @@ -339,7 +354,7 @@ private void actionSearch(AjaxRequestTarget aTarget, Form aForm) aTarget.add(mainContainer); aTarget.addChildren(getPage(), IFeedback.class); } - + private void actionClearResults(AjaxRequestTarget aTarget, Form aForm) { targetQuery.setObject(""); @@ -347,12 +362,11 @@ private void actionClearResults(AjaxRequestTarget aTarget, Form aForm) groupedSearchResults.detach(); aTarget.add(mainContainer); } - + private void getSearchResultsGrouped() { if (isBlank(targetQuery.getObject())) { resultsProvider.emptyQuery(); - groupedSearchResults.setObject(null); return; } @@ -362,10 +376,9 @@ private void getSearchResultsGrouped() error( "A feature has to be selected in order to group by feature values. If you want to group by document title, select none for both layer and feature."); resultsProvider.emptyQuery(); - groupedSearchResults.setObject(null); return; } - + try { AnnotatorState state = getModelObject(); Project project = state.getProject(); @@ -377,13 +390,11 @@ private void getSearchResultsGrouped() SearchOptions opt = searchOptions.getObject(); resultsProvider.initializeQuery(currentUser, project, targetQuery.getObject(), limitToDocument, opt.getGroupingLayer(), opt.getGroupingFeature()); - groupedSearchResults.setObject(null); return; } catch (Exception e) { error("Error in the query: " + e.getMessage()); resultsProvider.emptyQuery(); - groupedSearchResults.setObject(null); return; } } @@ -414,17 +425,22 @@ public void actionApplyToSelectedResults(AjaxRequestTarget aTarget, try { SpanAdapter adapter = (SpanAdapter) annotationService.getAdapter(layer); adapter.silenceEvents(); - + + // Flatten ResultGroups that are currently in the cache into one list + List resultsGroups = new ArrayList<>(); + groupedSearchResults.getObject().allPages().stream() + .forEach(p -> resultsGroups.addAll(p)); + // Group the results by document such that we can process one CAS at a time - Map> resultsByDocument = groupedSearchResults.getObject() + Map> resultsByDocument = resultsGroups .stream() // the grouping can be based on some other strategy than the document, so // we re-group here .flatMap(group -> group.getResults().stream()) .collect(groupingBy(SearchResult::getDocumentId)); - + BulkOperationResult bulkResult = new BulkOperationResult(); - + AnnotatorState state = getModelObject(); for (Entry> resultsGroup : resultsByDocument.entrySet()) { long documentId = resultsGroup.getKey(); @@ -433,7 +449,7 @@ public void actionApplyToSelectedResults(AjaxRequestTarget aTarget, AnnotationDocument annoDoc = documentService .createOrGetAnnotationDocument(sourceDoc, currentUser); - + switch (annoDoc.getState()) { case FINISHED: // fall-through case IGNORE: @@ -442,7 +458,7 @@ public void actionApplyToSelectedResults(AjaxRequestTarget aTarget, default: // Do nothing } - + // Load annotated document CAS cas = documentService.readAnnotationCas(sourceDoc, currentUser.getUsername()); @@ -452,14 +468,14 @@ public void actionApplyToSelectedResults(AjaxRequestTarget aTarget, if (result.isReadOnly() || !result.isSelectedForAnnotation()) { continue; } - + aConsumer.apply(sourceDoc, cas, adapter, result, bulkResult); } // Persist annotated document writeJCasAndUpdateTimeStamp(sourceDoc, cas); } - + if (bulkResult.created > 0) { success("Created annotations: " + bulkResult.created); } @@ -472,11 +488,11 @@ public void actionApplyToSelectedResults(AjaxRequestTarget aTarget, if (bulkResult.conflict > 0) { warn("Annotations skipped due to conflicts: " + bulkResult.conflict); } - + if (bulkResult.created == 0 && bulkResult.updated == 0 && bulkResult.deleted == 0) { info("No changes"); } - + applicationEventPublisher.get().publishEvent(new BulkAnnotationEvent(this, getModelObject().getProject(), currentUser.getUsername(), layer)); } @@ -489,7 +505,7 @@ public void actionApplyToSelectedResults(AjaxRequestTarget aTarget, LOG.error("Unable to apply action to search results: ", e); } } - + getAnnotationPage().actionRefreshDocument(aTarget); } @@ -500,7 +516,7 @@ private void createAnnotationAtSearchResult(SourceDocument aDocument, CAS aCas, AnnotatorState state = getModelObject(); AnnotationLayer layer = aAdapter.getLayer(); List featureStates = state.getFeatureStates(); - + Type type = CasUtil.getAnnotationType(aCas, aAdapter.getAnnotationTypeName()); AnnotationFS annoFS = selectAt(aCas, type, aSearchResult.getOffsetStart(), aSearchResult.getOffsetEnd()).stream().findFirst().orElse(null); @@ -553,7 +569,7 @@ private void deleteAnnotationAtSearchResult(SourceDocument aDocument, CAS aCas, && deleteOptions.getObject().isDeleteOnlyMatchingFeatureValues()) { return; } - + aAdapter.delete(aDocument, currentUser.getUsername(), aCas, new VID(annoFS)); aBulkResult.deleted++; } @@ -587,7 +603,7 @@ private boolean featureValuesMatchCurrentState(AnnotationFS aAnnotationFS) } return true; } - + private class SearchResultGroup extends Fragment { @@ -597,18 +613,18 @@ public SearchResultGroup(String aId, String aMarkupId, MarkupContainer aMarkupPr String groupKey, IModel aModel) { super(aId, aMarkupId, aMarkupProvider, aModel); - + ListView statementList = new ListView("results") { private static final long serialVersionUID = 5811425707843441458L; - + @Override protected void populateItem(ListItem aItem) { Project currentProject = SearchAnnotationSidebar.this.getModel().getObject() .getProject(); SearchResult result = aItem.getModelObject(); - + LambdaAjaxLink lambdaAjaxLink = new LambdaAjaxLink("showSelectedDocument", t -> { selectedResult = aItem.getModelObject(); @@ -652,10 +668,10 @@ protected void onUpdate(AjaxRequestTarget target) }; statementList .setModel(LoadableDetachableModel.of(() -> aModel.getObject().getResults())); - add(statementList); + add(statementList); } } - + @FunctionalInterface private interface Operation { @@ -663,7 +679,7 @@ void apply(SourceDocument aSourceDoc, CAS aCas, SpanAdapter aAdapter, SearchResu BulkOperationResult aBulkResult) throws AnnotationException; } - + private class BulkOperationResult { public int created = 0; diff --git a/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchResultsPagesCache.java b/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchResultsPagesCache.java new file mode 100644 index 00000000000..d0062719356 --- /dev/null +++ b/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchResultsPagesCache.java @@ -0,0 +1,98 @@ +/* + * Copyright 2019 + * Ubiquitous Knowledge Processing (UKP) Lab + * Technische Universität Darmstadt + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.tudarmstadt.ukp.inception.app.ui.search.sidebar; + +import java.io.Serializable; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import de.tudarmstadt.ukp.inception.search.ResultsGroup; + + +public class SearchResultsPagesCache implements Serializable +{ + + private Map> pages; + + public SearchResultsPagesCache() { + pages = new HashMap<>(); + } + + public List getPage(PageKey aPageKey) { + return pages.get(aPageKey); + } + + public List getPage(int pageFrom, int pageTo) { + return pages.get(new PageKey(pageFrom, pageTo)); + } + + public void setPage(PageKey aPageKey, List aPage) { + pages.put(aPageKey, aPage); + } + + public void setPage(int pageFrom, int pageTo, List aPage) { + pages.put(new PageKey(pageFrom, pageTo), aPage); + } + + public Collection> allPages() { + return pages.values(); + } + + public void clear() { + pages = new HashMap<>(); + } + + public boolean isEmpty() { + return pages.isEmpty(); + } + + public boolean containsPage(long from, long to) { + return pages.containsKey(new PageKey(from, to)); + } + + public static class PageKey implements Serializable { + + private long from; + private long to; + + public PageKey (long aFrom, long aTo) { + from = aFrom; + to = aTo; + } + + @Override + public boolean equals(Object o) + { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + PageKey pageKey = (PageKey) o; + return from == pageKey.from && to == pageKey.to; + } + + @Override + public int hashCode() + { + return Objects.hash(from, to); + } + } +} diff --git a/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchResultsProvider.java b/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchResultsProvider.java index d05243354d0..ac9080e172a 100644 --- a/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchResultsProvider.java +++ b/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchResultsProvider.java @@ -59,12 +59,10 @@ public class SearchResultsProvider // Cache private long totalResults = 0; - private IModel> currentPageCache; - private long currentOffset = -1; - private long currentCount = -1; + private IModel currentPageCache; public SearchResultsProvider(SearchService aSearchService, - IModel> aCurrentPageCache) + IModel aCurrentPageCache) { searchService = aSearchService; currentPageCache = aCurrentPageCache; @@ -74,23 +72,24 @@ public SearchResultsProvider(SearchService aSearchService, public Iterator iterator(long first, long count) { if (query == null) { - currentPageCache.setObject(Collections.emptyList()); - return currentPageCache.getObject().iterator(); + currentPageCache.getObject().clear(); + List emptyList = Collections.emptyList(); + return emptyList.iterator(); } // Query if we just initialized a new query currentPageCache.getObject() == null or we want - // to retrieve a new page of the current query (currentOffset != first, + // to retrieve a new page of the current query (currentOffset != first, // currentCount != count) - if (currentPageCache.getObject() == null || (currentOffset != first - || currentCount != count)) { + if (currentPageCache.getObject().isEmpty() || !currentPageCache.getObject() + .containsPage(first, first + count)) { try { List queryResults = searchService .query(user, project, query, document, annotationLayer, annotationFeature, first, count).entrySet().stream() .map(e -> new ResultsGroup(e.getKey(), e.getValue())) .collect(Collectors.toList()); - currentPageCache.setObject(queryResults); - currentCount = count; - currentOffset = first; + currentPageCache.getObject() + .setPage(new SearchResultsPagesCache.PageKey(first, first + count), + queryResults); return queryResults.iterator(); } catch (IOException | ExecutionException e) { @@ -99,7 +98,8 @@ public Iterator iterator(long first, long count) } } else { - return currentPageCache.getObject().iterator(); + return currentPageCache.getObject() + .getPage(new SearchResultsPagesCache.PageKey(first, first + count)).iterator(); } } @@ -146,7 +146,7 @@ public void initializeQuery(User aUser, Project aProject, String aQuery, annotationFeature = aAnnotationFeature; totalResults = -1; // reset size cache - currentPageCache.setObject(null); // reset page cache + currentPageCache.getObject().clear(); // reset page cache } public void emptyQuery() @@ -169,7 +169,7 @@ public SearchService getSearchService() { return searchService; } - public IModel> getCurrentPageCache() + public IModel getCurrentPageCache() { return currentPageCache; } diff --git a/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchResultsProviderWrapper.java b/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchResultsProviderWrapper.java index 892e08dcde6..12944e0e1ce 100644 --- a/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchResultsProviderWrapper.java +++ b/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchResultsProviderWrapper.java @@ -82,7 +82,9 @@ public Iterator iterator(long first, long count) resultsGroupsSubList.add(group); } } - searchResultsProvider.getCurrentPageCache().setObject(resultsGroupsSubList); + searchResultsProvider.getCurrentPageCache().getObject() + .setPage(new SearchResultsPagesCache.PageKey(first, first + count), + resultsGroupsSubList); return resultsGroupsSubList.iterator(); } @@ -101,6 +103,20 @@ public IModel model(ResultsGroup object) return searchResultsProvider.model(object); } + public long groupSize(String aGroupKey) + { + for (ResultsGroup group : resultGroups) { + if (group.getGroupKey().equals(aGroupKey)) { + return group.getResults().size(); + } + } + return -1; + } + + public boolean isGroupingActivated() + { + return groupingActivated; + } public void initializeQuery(User aUser, Project aProject, String aQuery, SourceDocument aDocument, AnnotationLayer aAnnotationLayer, From c3ce1482096051d64d8b85e7edc5a3f68703c76a Mon Sep 17 00:00:00 2001 From: Marcel de Boer Date: Tue, 5 Nov 2019 10:50:50 +0100 Subject: [PATCH 057/453] 1054 - Dealing with many results in search sidebar - clean up some code --- .../inception/search/SearchServiceImpl.java | 4 +- .../inception/search/index/PhysicalIndex.java | 2 +- .../search/index/mtas/MtasDocumentIndex.java | 54 +++++-------- .../sidebar/SearchAnnotationSidebar.java | 40 +++++----- .../sidebar/SearchResultsPagesCache.java | 43 +++++------ .../search/sidebar/SearchResultsProvider.java | 32 +++----- .../sidebar/SearchResultsProviderWrapper.java | 76 ++++++++++++------- 7 files changed, 117 insertions(+), 134 deletions(-) diff --git a/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/SearchServiceImpl.java b/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/SearchServiceImpl.java index 4bf1406ab28..d53e07f68e5 100644 --- a/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/SearchServiceImpl.java +++ b/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/SearchServiceImpl.java @@ -513,7 +513,7 @@ public boolean isIndexInProgress(Project aProject) log.debug("Starting query for user [{}] in project [{}]({})", aUser.getUsername(), aProject.getName(), aProject.getId()); - long numResults = -1; + long numResults; Index index = getIndexFromMemory(aProject); @@ -553,7 +553,7 @@ public boolean isIndexInProgress(Project aProject) log.debug("Running query: [{}]", aQuery); numResults = index.getPhysicalIndex() - .numberofQueryResults(new SearchQueryRequest(aProject, aUser, aQuery, + .numberOfQueryResults(new SearchQueryRequest(aProject, aUser, aQuery, aDocument, aAnnotationLayer, aAnnotationFeature, 0L, 0L)); } diff --git a/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/index/PhysicalIndex.java b/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/index/PhysicalIndex.java index 36bbd4f9c4d..138b9efb95d 100644 --- a/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/index/PhysicalIndex.java +++ b/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/index/PhysicalIndex.java @@ -69,5 +69,5 @@ Map> executeQuery(SearchQueryRequest aRequest) */ public Optional getTimestamp(AnnotationDocument aDocument) throws IOException; - long numberofQueryResults(SearchQueryRequest aSearchQueryRequest) throws ExecutionException; + long numberOfQueryResults(SearchQueryRequest aSearchQueryRequest) throws ExecutionException; } diff --git a/inception-search-mtas/src/main/java/de/tudarmstadt/ukp/inception/search/index/mtas/MtasDocumentIndex.java b/inception-search-mtas/src/main/java/de/tudarmstadt/ukp/inception/search/index/mtas/MtasDocumentIndex.java index 2731cadd3c5..5e791c5f426 100644 --- a/inception-search-mtas/src/main/java/de/tudarmstadt/ukp/inception/search/index/mtas/MtasDocumentIndex.java +++ b/inception-search-mtas/src/main/java/de/tudarmstadt/ukp/inception/search/index/mtas/MtasDocumentIndex.java @@ -107,6 +107,7 @@ import mtas.codec.util.CodecInfo; import mtas.codec.util.CodecUtil; import mtas.parser.cql.MtasCQLParser; +import mtas.parser.cql.ParseException; import mtas.search.spans.util.MtasSpanQuery; /** @@ -158,23 +159,6 @@ public class MtasDocumentIndex private static final String EMPTY_FEATURE_VALUE_KEY = ""; - // Comparator for feature values. Sort lexicographically and make sure - // EMPTY_FEATUREVALUE_KEY is the "biggest" value - private static final Comparator FEATUREVALUE_COMPARATOR = (o1, o2) -> { - if (EMPTY_FEATURE_VALUE_KEY.equals(o1) && EMPTY_FEATURE_VALUE_KEY.equals(o2)) { - return 0; - } - else if (EMPTY_FEATURE_VALUE_KEY.equals(o1)) { - return 1; - } - else if (EMPTY_FEATURE_VALUE_KEY.equals(o2)) { - return -1; - } - else { - return o1.compareTo(o2); - } - }; - private final Logger log = LoggerFactory.getLogger(getClass()); private @Autowired FeatureIndexingSupportRegistry featureIndexingSupportRegistry; @@ -224,12 +208,7 @@ public Map> executeQuery(SearchQueryRequest aRequest) Directory directory = FSDirectory.open(getIndexDir().toPath()); IndexReader indexReader = DirectoryReader.open(directory); - String modifiedQuery = parseQuery(aRequest.getQuery()); - MtasSpanQuery mtasSpanQuery; - try (Reader reader = new StringReader(modifiedQuery)) { - MtasCQLParser parser = new MtasCQLParser(reader); - mtasSpanQuery = parser.parse(FIELD_CONTENT, DEFAULT_PREFIX, null, null, null); - } + MtasSpanQuery mtasSpanQuery = prepareMtasSpanQuery(aRequest); return doQuery(indexReader, aRequest, FIELD_CONTENT, mtasSpanQuery); } @@ -244,7 +223,7 @@ public Map> executeQuery(SearchQueryRequest aRequest) } @Override - public long numberofQueryResults(SearchQueryRequest aRequest) throws ExecutionException + public long numberOfQueryResults(SearchQueryRequest aRequest) throws ExecutionException { try { log.trace("Determining number of results for query {} on index {}", aRequest, @@ -253,12 +232,7 @@ public long numberofQueryResults(SearchQueryRequest aRequest) throws ExecutionEx Directory directory = FSDirectory.open(getIndexDir().toPath()); IndexReader indexReader = DirectoryReader.open(directory); - String modifiedQuery = parseQuery(aRequest.getQuery()); - MtasSpanQuery mtasSpanQuery; - try (Reader reader = new StringReader(modifiedQuery)) { - MtasCQLParser parser = new MtasCQLParser(reader); - mtasSpanQuery = parser.parse(FIELD_CONTENT, DEFAULT_PREFIX, null, null, null); - } + MtasSpanQuery mtasSpanQuery = prepareMtasSpanQuery(aRequest); return countResults(indexReader, aRequest, mtasSpanQuery); } @@ -272,6 +246,18 @@ public long numberofQueryResults(SearchQueryRequest aRequest) throws ExecutionEx } } + private MtasSpanQuery prepareMtasSpanQuery (SearchQueryRequest aRequest) + throws IOException, ParseException + { + String modifiedQuery = parseQuery(aRequest.getQuery()); + MtasSpanQuery mtasSpanQuery; + try (Reader reader = new StringReader(modifiedQuery)) { + MtasCQLParser parser = new MtasCQLParser(reader); + mtasSpanQuery = parser.parse(FIELD_CONTENT, DEFAULT_PREFIX, null, null, null); + } + return mtasSpanQuery; + } + private String parseQuery(String aQuery) { String result; @@ -307,7 +293,6 @@ private String parseQuery(String aQuery) return result; } - private long countResults(IndexReader aIndexReader, SearchQueryRequest aRequest, MtasSpanQuery q) throws IOException { @@ -322,7 +307,7 @@ private long countResults(IndexReader aIndexReader, final float boost = 0; SpanWeight spanweight = q.rewrite(aIndexReader).createWeight(searcher, false, boost); - long current = 0; + long numResults = 0; while (leafReaderContextIterator.hasNext()) { LeafReaderContext leafReaderContext = leafReaderContextIterator.next(); @@ -385,7 +370,7 @@ else if (annotationDocumentId != -1 && !aRequest.getUser().getUsername() } while (spans.nextStartPosition() != Spans.NO_MORE_POSITIONS) { - current++; + numResults++; } } } @@ -393,9 +378,10 @@ else if (annotationDocumentId != -1 && !aRequest.getUser().getUsername() } catch (Exception e) { log.error("Unable to process query results", e); + numResults = -1; } } - return current; + return numResults; } private Map listAnnotatableDocuments(Project aProject, User aUser) diff --git a/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebar.java b/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebar.java index 20ae9f9ee63..ea25d9a96a7 100644 --- a/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebar.java +++ b/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebar.java @@ -24,7 +24,6 @@ import static org.apache.uima.fit.util.CasUtil.selectAt; import java.io.IOException; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -311,12 +310,11 @@ private AjaxCheckBox createGroupLevelSelectionCheckBox(String aId, @Override protected void onUpdate(AjaxRequestTarget target) { - for (List page : groupedSearchResults.getObject().allPages()) { - for (ResultsGroup resultsGroup : page) { - if (resultsGroup.getGroupKey().equals(aGroupKey)) { - resultsGroup.getResults().stream() - .forEach(r -> r.setSelectedForAnnotation(getModelObject())); - } + for (ResultsGroup resultsGroup : groupedSearchResults.getObject() + .allResultsGroups()) { + if (resultsGroup.getGroupKey().equals(aGroupKey)) { + resultsGroup.getResults().stream() + .forEach(r -> r.setSelectedForAnnotation(getModelObject())); } } target.add(resultsGroupContainer); @@ -326,16 +324,16 @@ protected void onUpdate(AjaxRequestTarget target) protected void onConfigure() { super.onConfigure(); - for (List page : groupedSearchResults.getObject().allPages()) { - for (ResultsGroup resultsGroup : page) { - if (resultsGroup.getGroupKey().equals(aGroupKey)) { - List unselectedResults = resultsGroup.getResults() - .stream().filter(sr -> !sr.isSelectedForAnnotation()) - .collect(Collectors.toList()); - if (!unselectedResults.isEmpty()) { - setModelObject(false); - return; - } + + for (ResultsGroup resultsGroup : groupedSearchResults.getObject() + .allResultsGroups()) { + if (resultsGroup.getGroupKey().equals(aGroupKey)) { + List unselectedResults = resultsGroup.getResults().stream() + .filter(sr -> !sr.isSelectedForAnnotation()) + .collect(Collectors.toList()); + if (!unselectedResults.isEmpty()) { + setModelObject(false); + return; } } } @@ -426,13 +424,9 @@ public void actionApplyToSelectedResults(AjaxRequestTarget aTarget, SpanAdapter adapter = (SpanAdapter) annotationService.getAdapter(layer); adapter.silenceEvents(); - // Flatten ResultGroups that are currently in the cache into one list - List resultsGroups = new ArrayList<>(); - groupedSearchResults.getObject().allPages().stream() - .forEach(p -> resultsGroups.addAll(p)); - // Group the results by document such that we can process one CAS at a time - Map> resultsByDocument = resultsGroups + Map> resultsByDocument = groupedSearchResults.getObject() + .allResultsGroups() .stream() // the grouping can be based on some other strategy than the document, so // we re-group here diff --git a/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchResultsPagesCache.java b/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchResultsPagesCache.java index d0062719356..c7f31563360 100644 --- a/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchResultsPagesCache.java +++ b/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchResultsPagesCache.java @@ -9,7 +9,7 @@ * * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software + * Unless required by applicable law or agreed count 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 @@ -23,6 +23,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.stream.Collectors; import de.tudarmstadt.ukp.inception.search.ResultsGroup; @@ -36,24 +37,16 @@ public SearchResultsPagesCache() { pages = new HashMap<>(); } - public List getPage(PageKey aPageKey) { - return pages.get(aPageKey); + public List getPage(long pageFirst, long pageCount) { + return pages.get(new PageKey(pageFirst, pageCount)); } - public List getPage(int pageFrom, int pageTo) { - return pages.get(new PageKey(pageFrom, pageTo)); + public void putPage(long pageFirst, long pageCount, List aPage) { + pages.put(new PageKey(pageFirst, pageCount), aPage); } - public void setPage(PageKey aPageKey, List aPage) { - pages.put(aPageKey, aPage); - } - - public void setPage(int pageFrom, int pageTo, List aPage) { - pages.put(new PageKey(pageFrom, pageTo), aPage); - } - - public Collection> allPages() { - return pages.values(); + public List allResultsGroups() { + return pages.values().stream().flatMap(Collection::stream).collect(Collectors.toList()); } public void clear() { @@ -64,18 +57,18 @@ public boolean isEmpty() { return pages.isEmpty(); } - public boolean containsPage(long from, long to) { - return pages.containsKey(new PageKey(from, to)); + public boolean containsPage(long first, long count) { + return pages.containsKey(new PageKey(first, count)); } - public static class PageKey implements Serializable { + private class PageKey implements Serializable { - private long from; - private long to; + private long first; + private long count; - public PageKey (long aFrom, long aTo) { - from = aFrom; - to = aTo; + public PageKey (long aFirst, long aCount) { + first = aFirst; + count = aCount; } @Override @@ -86,13 +79,13 @@ public boolean equals(Object o) if (o == null || getClass() != o.getClass()) return false; PageKey pageKey = (PageKey) o; - return from == pageKey.from && to == pageKey.to; + return first == pageKey.first && count == pageKey.count; } @Override public int hashCode() { - return Objects.hash(from, to); + return Objects.hash(first, count); } } } diff --git a/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchResultsProvider.java b/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchResultsProvider.java index ac9080e172a..11f8d874aa1 100644 --- a/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchResultsProvider.java +++ b/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchResultsProvider.java @@ -59,37 +59,34 @@ public class SearchResultsProvider // Cache private long totalResults = 0; - private IModel currentPageCache; + private IModel pagesCache; public SearchResultsProvider(SearchService aSearchService, IModel aCurrentPageCache) { searchService = aSearchService; - currentPageCache = aCurrentPageCache; + pagesCache = aCurrentPageCache; } @Override public Iterator iterator(long first, long count) { if (query == null) { - currentPageCache.getObject().clear(); + pagesCache.getObject().clear(); List emptyList = Collections.emptyList(); return emptyList.iterator(); } - // Query if we just initialized a new query currentPageCache.getObject() == null or we want - // to retrieve a new page of the current query (currentOffset != first, - // currentCount != count) - if (currentPageCache.getObject().isEmpty() || !currentPageCache.getObject() - .containsPage(first, first + count)) { + // Query if the results in the given range are not in the cache i.e. if we need to fetch + // a new page + if (!pagesCache.getObject().containsPage(first, count)) { try { List queryResults = searchService .query(user, project, query, document, annotationLayer, annotationFeature, first, count).entrySet().stream() .map(e -> new ResultsGroup(e.getKey(), e.getValue())) .collect(Collectors.toList()); - currentPageCache.getObject() - .setPage(new SearchResultsPagesCache.PageKey(first, first + count), - queryResults); + + pagesCache.getObject().putPage(first, count, queryResults); return queryResults.iterator(); } catch (IOException | ExecutionException e) { @@ -98,8 +95,7 @@ public Iterator iterator(long first, long count) } } else { - return currentPageCache.getObject() - .getPage(new SearchResultsPagesCache.PageKey(first, first + count)).iterator(); + return pagesCache.getObject().getPage(first, count).iterator(); } } @@ -146,7 +142,7 @@ public void initializeQuery(User aUser, Project aProject, String aQuery, annotationFeature = aAnnotationFeature; totalResults = -1; // reset size cache - currentPageCache.getObject().clear(); // reset page cache + pagesCache.getObject().clear(); // reset page cache } public void emptyQuery() @@ -165,13 +161,9 @@ public AnnotationFeature getAnnotationFeature() return annotationFeature; } - public SearchService getSearchService() { - return searchService; - } - - public IModel getCurrentPageCache() + public IModel getPagesCache() { - return currentPageCache; + return pagesCache; } } diff --git a/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchResultsProviderWrapper.java b/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchResultsProviderWrapper.java index 12944e0e1ce..135bad790fb 100644 --- a/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchResultsProviderWrapper.java +++ b/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchResultsProviderWrapper.java @@ -48,47 +48,65 @@ public SearchResultsProviderWrapper(SearchResultsProvider aSearchResultsProvider public Iterator iterator(long first, long count) { /* - If the grouping is activated we first fetch all results sorted them by group and then apply + If the grouping is activated we first fetch all results sorted by group and then apply the paging. - We do this because if we apply paging at query level and grouping together, members of a + This is done because if we apply paging at query level and grouping together, members of a group might me scattered over multiple pages. (Grouping by document is an exception because the query iterates over documents. So results from the same document appear in a sequence) - If the grouping is not activated we can apply paging directly when fetching the results - since it defaults to grouping by document. So we fetch them from the searchResultsProvider. + If the grouping is not activated (defaults to grouping by document) we can apply paging at + query level. So we fetch them directly from the searchResultsProvider. */ if (groupingActivated) { - List resultsGroupsSubList = new ArrayList<>(); - int counter = 0; - for (ResultsGroup resultsGroup : resultGroups) { + List subList = resultsGroupsSublist(first, count); + + searchResultsProvider.getPagesCache().getObject().putPage(first, count, subList); + + return subList.iterator(); + } + + return searchResultsProvider.iterator(first, count); + } + + /** + * Iterate over a List of ResultGroups and return a sublist that contains the SearchResults + * first to (first + count). + * + * e.g.: sublist of elements 2 - 5 + * + * ResultsGroup1----------ResultsGroup2 ResultsGroup1----------ResultsGroup2 + * | | | | + * SearchResult-1 SearchResult-4 SearchResult-3 SearchResult-4 + * SearchResult-2 SearchResult-5 ---> SearchResult-5 + * SearchResult-3 SearchResult-6 + * + */ + private List resultsGroupsSublist(long first, long count) { + List resultsGroupsSubList = new ArrayList<>(); + int counter = 0; + for (ResultsGroup resultsGroup : resultGroups) { + if (counter - first + 1 > count) { + break; + } + List sublist = new ArrayList<>(); + for (SearchResult result : resultsGroup.getResults()) { + if (counter < first) { + counter ++; + continue; + } if (counter - first + 1 > count) { break; } - List sublist = new ArrayList<>(); - for (SearchResult result : resultsGroup.getResults()) { - if (counter < first) { - counter ++; - continue; - } - if (counter - first + 1 > count) { - break; - } - sublist.add(result); - counter++; - } - if (! (counter <= first)) { - ResultsGroup group = new ResultsGroup(resultsGroup.getGroupKey(), sublist); - resultsGroupsSubList.add(group); - } + sublist.add(result); + counter++; + } + if (! (counter <= first)) { + ResultsGroup group = new ResultsGroup(resultsGroup.getGroupKey(), sublist); + resultsGroupsSubList.add(group); } - searchResultsProvider.getCurrentPageCache().getObject() - .setPage(new SearchResultsPagesCache.PageKey(first, first + count), - resultsGroupsSubList); - return resultsGroupsSubList.iterator(); } - - return searchResultsProvider.iterator(first, count); + return resultsGroupsSubList; } @Override From ede588176dc944f64f271b2d711473ac0cc137be Mon Sep 17 00:00:00 2001 From: Marcel de Boer Date: Tue, 5 Nov 2019 11:26:02 +0100 Subject: [PATCH 058/453] 1054 - Dealing with many results in search sidebar - renaming --- .../search/sidebar/SearchResultsProvider.java | 20 +++++++++---------- .../sidebar/SearchResultsProviderWrapper.java | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchResultsProvider.java b/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchResultsProvider.java index 11f8d874aa1..0d107dbf794 100644 --- a/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchResultsProvider.java +++ b/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchResultsProvider.java @@ -59,26 +59,26 @@ public class SearchResultsProvider // Cache private long totalResults = 0; - private IModel pagesCache; + private IModel pagesCacheModel; public SearchResultsProvider(SearchService aSearchService, - IModel aCurrentPageCache) + IModel aPageCacheModel) { searchService = aSearchService; - pagesCache = aCurrentPageCache; + pagesCacheModel = aPageCacheModel; } @Override public Iterator iterator(long first, long count) { if (query == null) { - pagesCache.getObject().clear(); + pagesCacheModel.getObject().clear(); List emptyList = Collections.emptyList(); return emptyList.iterator(); } // Query if the results in the given range are not in the cache i.e. if we need to fetch // a new page - if (!pagesCache.getObject().containsPage(first, count)) { + if (!pagesCacheModel.getObject().containsPage(first, count)) { try { List queryResults = searchService .query(user, project, query, document, annotationLayer, annotationFeature, @@ -86,7 +86,7 @@ public Iterator iterator(long first, long count) .map(e -> new ResultsGroup(e.getKey(), e.getValue())) .collect(Collectors.toList()); - pagesCache.getObject().putPage(first, count, queryResults); + pagesCacheModel.getObject().putPage(first, count, queryResults); return queryResults.iterator(); } catch (IOException | ExecutionException e) { @@ -95,7 +95,7 @@ public Iterator iterator(long first, long count) } } else { - return pagesCache.getObject().getPage(first, count).iterator(); + return pagesCacheModel.getObject().getPage(first, count).iterator(); } } @@ -142,7 +142,7 @@ public void initializeQuery(User aUser, Project aProject, String aQuery, annotationFeature = aAnnotationFeature; totalResults = -1; // reset size cache - pagesCache.getObject().clear(); // reset page cache + pagesCacheModel.getObject().clear(); // reset page cache } public void emptyQuery() @@ -161,9 +161,9 @@ public AnnotationFeature getAnnotationFeature() return annotationFeature; } - public IModel getPagesCache() + public IModel getPagesCacheModel() { - return pagesCache; + return pagesCacheModel; } } diff --git a/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchResultsProviderWrapper.java b/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchResultsProviderWrapper.java index 135bad790fb..d06dfd29b3f 100644 --- a/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchResultsProviderWrapper.java +++ b/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchResultsProviderWrapper.java @@ -61,7 +61,7 @@ If the grouping is not activated (defaults to grouping by document) we can apply if (groupingActivated) { List subList = resultsGroupsSublist(first, count); - searchResultsProvider.getPagesCache().getObject().putPage(first, count, subList); + searchResultsProvider.getPagesCacheModel().getObject().putPage(first, count, subList); return subList.iterator(); } From ce92f21de6ff66cf0db2414e6841099b40d3e058 Mon Sep 17 00:00:00 2001 From: Marcel de Boer Date: Tue, 5 Nov 2019 11:37:10 +0100 Subject: [PATCH 059/453] 1054 - Dealing with many results in search sidebar - clear cache on empty query --- .../inception/app/ui/search/sidebar/SearchResultsProvider.java | 1 + 1 file changed, 1 insertion(+) diff --git a/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchResultsProvider.java b/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchResultsProvider.java index 0d107dbf794..e911e5bc3f9 100644 --- a/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchResultsProvider.java +++ b/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchResultsProvider.java @@ -149,6 +149,7 @@ public void emptyQuery() { query = null; totalResults = 0; + pagesCacheModel.getObject().clear(); } public AnnotationLayer getAnnotationLayer() From 3fc84dbd82c5bd253180d314ca8395616f87b635 Mon Sep 17 00:00:00 2001 From: Marcel de Boer Date: Tue, 5 Nov 2019 12:36:17 +0100 Subject: [PATCH 060/453] 1054 - Dealing with many results in search sidebar - fix bug regarding clear button --- .../app/ui/search/sidebar/SearchAnnotationSidebar.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebar.java b/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebar.java index ea25d9a96a7..18a36978bbe 100644 --- a/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebar.java +++ b/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebar.java @@ -346,7 +346,6 @@ protected void onConfigure() private void actionSearch(AjaxRequestTarget aTarget, Form aForm) { selectedResult = null; - groupedSearchResults.detach(); searchResultGroups.setItemsPerPage(searchOptions.getObject().getItemsPerPage()); getSearchResultsGrouped(); aTarget.add(mainContainer); @@ -356,8 +355,8 @@ private void actionSearch(AjaxRequestTarget aTarget, Form aForm) private void actionClearResults(AjaxRequestTarget aTarget, Form aForm) { targetQuery.setObject(""); + resultsProvider.emptyQuery(); selectedResult = null; - groupedSearchResults.detach(); aTarget.add(mainContainer); } From ecb1bd38608765009bc698425f2aa7ec9809b05f Mon Sep 17 00:00:00 2001 From: Marcel de Boer Date: Wed, 6 Nov 2019 08:12:58 +0100 Subject: [PATCH 061/453] 1054 - Dealing with many results in search sidebar - disable paging at low level per default and add an option enable it if necessary --- .../sidebar/SearchAnnotationSidebar.html | 8 ++++ .../sidebar/SearchAnnotationSidebar.java | 43 +++++++++++++------ .../SearchAnnotationSidebar.properties | 3 ++ .../sidebar/SearchResultsProviderWrapper.java | 19 ++++---- .../search/sidebar/options/SearchOptions.java | 12 ++++++ 5 files changed, 64 insertions(+), 21 deletions(-) diff --git a/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebar.html b/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebar.html index 9bce305444d..316c9ddafc4 100644 --- a/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebar.html +++ b/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebar.html @@ -58,6 +58,14 @@

    +
    +
    + +
    +

    -
    - +
    +
    -
    - +
    +
    @@ -94,20 +92,20 @@

    -