From 09e64b026d37f960b4c4958e31ec1825e9011654 Mon Sep 17 00:00:00 2001 From: Jose Date: Wed, 27 Apr 2016 16:07:29 -0400 Subject: [PATCH 01/14] [misc] Update some dependencies. --- biolark/pom.xml | 6 +----- pom.xml | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/biolark/pom.xml b/biolark/pom.xml index 3127d94..c27d02e 100644 --- a/biolark/pom.xml +++ b/biolark/pom.xml @@ -86,6 +86,7 @@ net.sf.json-lib json-lib jdk15 + 2.3 org.slf4j @@ -200,11 +201,6 @@ gson 2.2.2 - - com.ibm.icu - icu4j - 49.1 - com.ibm.icu icu4j diff --git a/pom.xml b/pom.xml index 6abd966..19b637e 100644 --- a/pom.xml +++ b/pom.xml @@ -23,7 +23,7 @@ org.phenotips phenotips-components - 1.2-SNAPSHOT + 1.3-SNAPSHOT clinical-text-analysis-extension From d2f1f79ea0e0d35c59742b5630ebe07fe3fcd574 Mon Sep 17 00:00:00 2001 From: Jose Date: Thu, 19 May 2016 17:26:19 -0400 Subject: [PATCH 02/14] [misc] show entire sentences in the annotation context. --- .../textanalysis/TermAnnotation.java | 52 +++++++- .../TermAnnotationSentenceDetector.java | 70 +++++++++++ .../script/TermAnnotationScriptService.java | 6 +- .../TermAnnotationSentenceDetectorTest.java | 111 ++++++++++++++++++ .../resources/PhenoTips/AnnotationService.xml | 5 +- .../PhenoTips/Clinical Notes Annotation.xml | 15 +-- 6 files changed, 249 insertions(+), 10 deletions(-) create mode 100644 api/src/main/java/org/phenotips/textanalysis/internal/TermAnnotationSentenceDetector.java create mode 100644 api/src/test/java/org/phenotips/textanalysis/internal/TermAnnotationSentenceDetectorTest.java diff --git a/api/src/main/java/org/phenotips/textanalysis/TermAnnotation.java b/api/src/main/java/org/phenotips/textanalysis/TermAnnotation.java index fdba726..11d0b4a 100644 --- a/api/src/main/java/org/phenotips/textanalysis/TermAnnotation.java +++ b/api/src/main/java/org/phenotips/textanalysis/TermAnnotation.java @@ -25,7 +25,7 @@ * @version $Id$ * @since 1.0M1 */ -public class TermAnnotation +public class TermAnnotation implements Comparable { private final long mStartPos; @@ -33,6 +33,12 @@ public class TermAnnotation private final VocabularyTerm mTerm; + private String sentence; + + private long startInSentence; + + private long endInSentence; + /** * Constructs an annotation for a an ontology term using it's start and end positions within the text. * @@ -71,6 +77,50 @@ public VocabularyTerm getTerm() return this.mTerm; } + /** + * @return the sentence in which the term occurs. + */ + public String getSentence() + { + return sentence; + } + + /** + * @return the position within the sentence where the term starts + */ + public long getStartInSentence() + { + return startInSentence; + } + + /** + * @return the position within the sentence where the term ends + */ + public long getEndInSentence() + { + return endInSentence; + } + + /** + * Set the sentence that this term appears in. + * @param sentence the sentence. + * @param startInSentence the position within the sentence where the term starts + * @param endInSentence the position within the sentence where the term ends + */ + public void setSentence(String sentence, long startInSentence, long endInSentence) + { + this.sentence = sentence.trim(); + this.startInSentence = startInSentence; + this.endInSentence = endInSentence; + } + + @Override + public int compareTo(TermAnnotation other) + { + /* TODO: Casting. Hopefully they're not that far off that we overflow... */ + return (int) (this.getStartPos() - other.getStartPos()); + } + @Override public int hashCode() { diff --git a/api/src/main/java/org/phenotips/textanalysis/internal/TermAnnotationSentenceDetector.java b/api/src/main/java/org/phenotips/textanalysis/internal/TermAnnotationSentenceDetector.java new file mode 100644 index 0000000..b0610b1 --- /dev/null +++ b/api/src/main/java/org/phenotips/textanalysis/internal/TermAnnotationSentenceDetector.java @@ -0,0 +1,70 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see http://www.gnu.org/licenses/ + */ +package org.phenotips.textanalysis.internal; + +import org.phenotips.textanalysis.TermAnnotation; + +import java.text.BreakIterator; +import java.util.Collections; +import java.util.List; +import java.util.Locale; + +/** + * Takes in lists of term annotations, as well as the text they appear in and + * assigns sentences to them from the text. + */ +public class TermAnnotationSentenceDetector +{ + /** + * Attaches sentences to the term annotations given. + * @param annotations the annotations + * @param text the text where the annotations appear + */ + public void detectSentences(List annotations, String text) + { + BreakIterator sentences = BreakIterator.getSentenceInstance(Locale.US); + sentences.setText(text); + Collections.sort(annotations); + int currentAnnotation = 0; + int currentSentence = 0; + while (currentSentence != BreakIterator.DONE && currentAnnotation < annotations.size()) { + TermAnnotation annotation = annotations.get(currentAnnotation); + int nextSentence = sentences.next(); + sentences.previous(); + if (annotation.getStartPos() >= currentSentence && annotation.getStartPos() < nextSentence) { + long start = annotation.getStartPos() - currentSentence; + long end = annotation.getEndPos() - currentSentence; + String sentence; + if (annotation.getEndPos() <= nextSentence) { + /* Yay, straightforward! */ + sentence = text.substring(currentSentence, nextSentence); + } else { + /* Uh-oh, cross sentence annotation */ + int crossSentenceEnd = sentences.following((int) annotation.getEndPos()); + sentence = text.substring(currentSentence, crossSentenceEnd); + /* Rewind the iterator */ + sentences.preceding(currentSentence + 1); + } + annotation.setSentence(sentence, start, end); + currentAnnotation++; + } else { + currentSentence = sentences.next(); + } + } + } +} diff --git a/api/src/main/java/org/phenotips/textanalysis/script/TermAnnotationScriptService.java b/api/src/main/java/org/phenotips/textanalysis/script/TermAnnotationScriptService.java index e9802c9..7142713 100644 --- a/api/src/main/java/org/phenotips/textanalysis/script/TermAnnotationScriptService.java +++ b/api/src/main/java/org/phenotips/textanalysis/script/TermAnnotationScriptService.java @@ -20,6 +20,7 @@ import org.phenotips.textanalysis.TermAnnotation; import org.phenotips.textanalysis.TermAnnotationService; import org.phenotips.textanalysis.TermAnnotationService.AnnotationException; +import org.phenotips.textanalysis.internal.TermAnnotationSentenceDetector; import org.xwiki.component.annotation.Component; import org.xwiki.script.service.ScriptService; @@ -55,7 +56,10 @@ public class TermAnnotationScriptService implements ScriptService public List get(String text) { try { - return this.service.annotate(text); + List retval = this.service.annotate(text); + TermAnnotationSentenceDetector detector = new TermAnnotationSentenceDetector(); + detector.detectSentences(retval, text); + return retval; } catch (AnnotationException e) { return null; } diff --git a/api/src/test/java/org/phenotips/textanalysis/internal/TermAnnotationSentenceDetectorTest.java b/api/src/test/java/org/phenotips/textanalysis/internal/TermAnnotationSentenceDetectorTest.java new file mode 100644 index 0000000..6a9e63a --- /dev/null +++ b/api/src/test/java/org/phenotips/textanalysis/internal/TermAnnotationSentenceDetectorTest.java @@ -0,0 +1,111 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see http://www.gnu.org/licenses/ + */ +package org.phenotips.textanalysis.internal; + +import org.phenotips.textanalysis.TermAnnotation; + + +import org.junit.Test; +import org.phenotips.vocabulary.VocabularyTerm; + +import java.util.List; +import java.util.ArrayList; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.junit.Assert.assertEquals; + + +public class TermAnnotationSentenceDetectorTest +{ + + private TermAnnotationSentenceDetector client; + + /* This is where you miss heredocs. */ + private static final String TEXT = "Now is the winter of our discontent " + + "Made glorious summer by this son of York; " + + "And all the clouds that lowered upon our house " + + "In the deep bosom of the ocean buried. " + + "Now are our brows bound with victorious wreaths, " + + "Our bruised arms hung up for monuments, " + + "Our stern alarums changed to merry meetings, " + + "Our dreadful marches to delightful measures. " + + "Grim-visaged war hath smoothed his wrinkled front, " + + "And now, instead of mounting barbed steeds " + + "To fright the souls of fearful adversaries, " + + "He capers nimbly in a lady's chamber " + + "To the lascivious pleasing of a lute."; + + private static final String SENTENCE1; + + private static final String SENTENCE2; + + private static final String SENTENCE3; + + static { + SENTENCE1 = TEXT.substring(0, 163); + /* 163 is just whitespace, skip it. */ + SENTENCE2 = TEXT.substring(164, 342); + SENTENCE3 = TEXT.substring(343); + } + + @Test + public void testBasic() + { + client = new TermAnnotationSentenceDetector(); + + List annotations = new ArrayList<>(3); + VocabularyTerm winterTerm = mock(VocabularyTerm.class); + when(winterTerm.getId()).thenReturn("winter"); + TermAnnotation winter = new TermAnnotation(11, 17, winterTerm); + annotations.add(winter); + + VocabularyTerm monumentsTerm = mock(VocabularyTerm.class); + when(monumentsTerm.getId()).thenReturn("monuments"); + TermAnnotation monuments = new TermAnnotation(242, 251, monumentsTerm); + annotations.add(monuments); + + VocabularyTerm nimblyTerm = mock(VocabularyTerm.class); + when(nimblyTerm.getId()).thenReturn("nimbly"); + TermAnnotation nimbly = new TermAnnotation(491, 497, nimblyTerm); + annotations.add(nimbly); + + client.detectSentences(annotations, TEXT); + + assertEquals("winter", + winter.getSentence(). + substring((int) winter.getStartInSentence(), (int) winter.getEndInSentence())); + assertEquals(SENTENCE1, winter.getSentence()); + assertEquals(11, winter.getStartInSentence()); + assertEquals(17, winter.getEndInSentence()); + + assertEquals("monuments", + monuments.getSentence(). + substring((int) monuments.getStartInSentence(), (int) monuments.getEndInSentence())); + assertEquals(SENTENCE2, monuments.getSentence()); + assertEquals(78, monuments.getStartInSentence()); + assertEquals(87, monuments.getEndInSentence()); + + assertEquals("nimbly", + nimbly.getSentence(). + substring((int) nimbly.getStartInSentence(), (int) nimbly.getEndInSentence())); + assertEquals(SENTENCE3, nimbly.getSentence()); + assertEquals(148, nimbly.getStartInSentence()); + assertEquals(154, nimbly.getEndInSentence()); + } +} diff --git a/ui/src/main/resources/PhenoTips/AnnotationService.xml b/ui/src/main/resources/PhenoTips/AnnotationService.xml index f0aa90e..7b83333 100644 --- a/ui/src/main/resources/PhenoTips/AnnotationService.xml +++ b/ui/src/main/resources/PhenoTips/AnnotationService.xml @@ -58,9 +58,12 @@ #set ($result = { 'start' : $annotation.getStartPos(), 'end' : $annotation.getEndPos(), + 'startInSentence': $annotation.getStartInSentence(), + 'endInSentence': $annotation.getEndInSentence(), 'id' : $termId, 'label' : $term.name, - 'term_category' : $term.term_category + 'term_category' : $term.term_category, + 'sentence': $annotation.getSentence() }) #set ($discard = $results.put($termId, $result)) #end diff --git a/ui/src/main/resources/PhenoTips/Clinical Notes Annotation.xml b/ui/src/main/resources/PhenoTips/Clinical Notes Annotation.xml index 5e5a86f..8050748 100644 --- a/ui/src/main/resources/PhenoTips/Clinical Notes Annotation.xml +++ b/ui/src/main/resources/PhenoTips/Clinical Notes Annotation.xml @@ -285,12 +285,13 @@ * @param {Object} suggestion Annotation object with the fields: 'start', 'end', 'id' and 'label'. */ function generateSuggestionUIElement(suggestion) { - var start = suggestion.start; - var end = suggestion.end; - var quoteStart = Math.max(0, start - 22); - var quoteEnd = Math.min(end + 22, text.length) + var start = suggestion.startInSentence; + var end = suggestion.endInSentence; + var quoteStart = 0; + var quoteEnd = text.length; var suggestionBox = new Element('div', {'class' : 'suggestion'}); var termCategories = new Element("span", {"class" : "hidden term-category"}); + var sentence = suggestion.sentence; suggestion['term_category'].forEach(function(category) { termCategories.insert( new Element('input', {'type' : 'hidden', 'value' : category}) @@ -312,11 +313,11 @@ ).insert('<span class="xHelpButton fa fa-info-circle phenotype-info" title="' + suggestion.id + '"></span>') ).insert( new Element('div', {'class' : 'suggestion-quote'}).insert( - "..." + text.substring(quoteStart, start) + "..." + sentence.substring(quoteStart, start) ).insert( - new Element('span', {'class' : 'quoted-term'}).update(text.substring(start, end)) + new Element('span', {'class' : 'quoted-term'}).update(sentence.substring(start, end)) ).insert( - text.substring(end, quoteEnd) + "..." + sentence.substring(end, quoteEnd) + "..." ) ).insert( dismissButton From a113dd4c98d593eaff29b3c0a508ff2fcd2e9667 Mon Sep 17 00:00:00 2001 From: Joe C Date: Sun, 12 Jun 2016 10:26:15 -0400 Subject: [PATCH 03/14] [misc] Whitespace changes --- .../PhenoTips/Clinical Notes Annotation.xml | 700 +++++++++--------- 1 file changed, 351 insertions(+), 349 deletions(-) diff --git a/ui/src/main/resources/PhenoTips/Clinical Notes Annotation.xml b/ui/src/main/resources/PhenoTips/Clinical Notes Annotation.xml index 8050748..076884a 100644 --- a/ui/src/main/resources/PhenoTips/Clinical Notes Annotation.xml +++ b/ui/src/main/resources/PhenoTips/Clinical Notes Annotation.xml @@ -118,377 +118,379 @@ forbid - document.observe('xwiki:dom:loaded', function() { + +document.observe('xwiki:dom:loaded', function() { // global variables - /** - * Suggestions that have been discarded or accepted. - * Maps Term IDs to annotation objects - */ - var dismissedTerms = {}; - - /** - * Unfiltered results of annotation by the annotationService. - * Maps Term IDs to annotation objects - */ - var annotationServiceResults = {}; - - /** - * List of suggestions that still haven't been displayed - */ - var suggestionBank = []; - - /** - * Suggestions that are currently displayed. - * Maps Term IDs to annotation objects - */ - var currentSuggestions = {}; - - /** - * True if the Dismissed XClass instance exists for this patient - */ - var existsDismissedSuggestionDataStore = false; - - /** - * Number of suggestions to display in the UI - */ - var numSuggestionsToDisplay = 4; - - /** - * Text to be annotated - */ - var text = ""; - - /** - * Container of the clinical notes annotation widget. - */ - var widgetContainer = new Element('div', {"id" : 'annotation-widget-container'}); - - //generate initial UI elements - var panel = $$(".current-phenotype-selection")[0]; - if(panel) { - var clinicalNotesSubPanel = - new Element('div', {"class" : "sub-panel"}).insert( - new Element('h3', {"class" : "wikigeneratedheader"}).update( - "<span><strong>Suggestions from Clinical Notes</strong></span>" - ) - ).insert(widgetContainer); - panel.insert({top : clinicalNotesSubPanel}); - - //listen for changes in indication for referral text box - var textArea = $("PhenoTips.PatientClass_0_indication_for_referral"); - text = textArea.value; - fetchDismissedSuggestions(updateAnnotations); - textArea.observe('blur', function() { - if(textArea.value != text) { - text = textArea.value; - updateAnnotations(); +/** + * Suggestions that have been discarded or accepted. + * Maps Term IDs to annotation objects + */ +var dismissedTerms = {}; + +/** + * Unfiltered results of annotation by the annotationService. + * Maps Term IDs to annotation objects + */ +var annotationServiceResults = {}; + +/** + * List of suggestions that still haven't been displayed + */ +var suggestionBank = []; + +/** + * Suggestions that are currently displayed. + * Maps Term IDs to annotation objects + */ +var currentSuggestions = {}; + +/** + * True if the Dismissed XClass instance exists for this patient + */ +var existsDismissedSuggestionDataStore = false; + +/** + * Number of suggestions to display in the UI + */ +var numSuggestionsToDisplay = 4; + +/** + * Text to be annotated + */ +var text = ""; + +/** + * Container of the clinical notes annotation widget. + */ +var widgetContainer = new Element('div', {"id" : 'annotation-widget-container'}); + +//generate initial UI elements +var panel = $$(".current-phenotype-selection")[0]; +if(panel) { + var clinicalNotesSubPanel = + new Element('div', {"class" : "sub-panel"}).insert( + new Element('h3', {"class" : "wikigeneratedheader"}).update( + "<span><strong>Suggestions from Clinical Notes</strong></span>" + ) + ).insert(widgetContainer); + panel.insert({top : clinicalNotesSubPanel}); + + //listen for changes in indication for referral text box + var textArea = $("PhenoTips.PatientClass_0_indication_for_referral"); + text = textArea.value; + fetchDismissedSuggestions(updateAnnotations); + textArea.observe('blur', function() { + if(textArea.value != text) { + text = textArea.value; + updateAnnotations(); + } + }); +} + +/** + * Queries server for annotations to the text, updates list of suggestions and + * displays suggestions in the UI. + */ +function updateAnnotations() { + if(text) { + currentSuggestions = {}; + var queryString = '?outputSyntax=plain'; + new Ajax.Request(new XWiki.Document('AnnotationService', 'PhenoTips').getURL('get') + queryString, + { + parameters : { + text : text + }, + onCreate : function() { + var loadingContainer = new Element('div', {'class' : 'loading-container'}); + var spinner = new Element('div', { + 'id' : 'suggestions-spinner', + 'class' : "fa fa-lg fa-spinner fa-spin"}); + var loadingLabel = new Element('div', {'id' : 'generating-suggestions-label'}).update("Generating Suggestions"); + loadingContainer.insert(spinner).insert(loadingLabel); + widgetContainer.update(loadingContainer); + }, + onSuccess : function(response) { + annotationServiceResults = response.responseJSON.response; + getExistingPhenotypes(); + populateSuggestionData(); + }, + onComplete : function () { + popSuggestions(numSuggestionsToDisplay).forEach(function(suggestion) { + currentSuggestions[suggestion.id] = suggestion; + }); + drawSuggestionsUI(); } }); + } else { + annotationServiceResults = {}; + currentSuggestions = {}; + populateSuggestionData(); + drawSuggestionsUI(); } - - /** - * Queries server for annotations to the text, updates list of suggestions and - * displays suggestions in the UI. - */ - function updateAnnotations() { - if(text) { - currentSuggestions = {}; - var queryString = '?outputSyntax=plain'; - new Ajax.Request(new XWiki.Document('AnnotationService', 'PhenoTips').getURL('get') + queryString, - { - parameters : { - text : text - }, - onCreate : function() { - var loadingContainer = new Element('div', {'class' : 'loading-container'}); - var spinner = new Element('div', { - 'id' : 'suggestions-spinner', - 'class' : "fa fa-lg fa-spinner fa-spin"}); - var loadingLabel = new Element('div', {'id' : 'generating-suggestions-label'}).update("Generating Suggestions"); - loadingContainer.insert(spinner).insert(loadingLabel); - widgetContainer.update(loadingContainer); - }, - onSuccess : function(response) { - annotationServiceResults = response.responseJSON.response; - getExistingPhenotypes(); - populateSuggestionData(); - }, - onComplete : function () { - popSuggestions(numSuggestionsToDisplay).forEach(function(suggestion) { - currentSuggestions[suggestion.id] = suggestion; - }); - drawSuggestionsUI(); +}; + +/** + * Creates YesNoPicker element. + * + * @param {Object} suggestion Annotation object with the fields: 'start', 'end', 'id' and 'label'. + * @param {Element} suggestionElt UI Element of one suggestion. + * @param {Element} categoryElt Hidden UI Element containing term categories. + */ +function generateYesNoPicker(suggestion, suggestionElt, categoryElt) { + var phenotypePrefix = ($('prefix') && $('prefix').value || ''); + var yesNoPicker = YesNoPicker.generatePickerElement( + [ + {type: 'yes', name: phenotypePrefix + 'phenotype'}, + {type: 'no', name: phenotypePrefix + 'negative_phenotype'} + ], + suggestion.id, + suggestion.label, + true + ); + + var qsWidget = $('quick-phenotype-search'); + + if(qsWidget && qsWidget._suggestPicker) { + yesNoPicker.select('.yes', '.no').invoke('observe', 'click', function(event) { + var option = Event.findElement(event); + var input = option.down('input[type="checkbox"]') || option.previous('.yes-no-picker').down('.yes input[type="checkbox"]'); // defaults to 'Y' when clicking on the text + if (!input) {return;} + if (input.checked) { + var negative = option.hasClassName('no'); + var categoryClone = categoryElt.clone(true); + if (negative) { + categoryClone.insert(new Element('input', {type: 'hidden', name : 'fieldName', value : input.name})); } - }); - } else { - annotationServiceResults = {}; - currentSuggestions = {}; - populateSuggestionData(); - drawSuggestionsUI(); - } - }; - - /** - * Creates YesNoPicker element. - * - * @param {Object} suggestion Annotation object with the fields: 'start', 'end', 'id' and 'label'. - * @param {Element} suggestionElt UI Element of one suggestion. - * @param {Element} categoryElt Hidden UI Element containing term categories. - */ - function generateYesNoPicker(suggestion, suggestionElt, categoryElt) { - var phenotypePrefix = ($('prefix') && $('prefix').value || ''); - var yesNoPicker = YesNoPicker.generatePickerElement( - [ - {type: 'yes', name: phenotypePrefix + 'phenotype'}, - {type: 'no', name: phenotypePrefix + 'negative_phenotype'} - ], - suggestion.id, - suggestion.label, - true - ); - - var qsWidget = $('quick-phenotype-search'); - - if(qsWidget && qsWidget._suggestPicker) { - yesNoPicker.select('.yes', '.no').invoke('observe', 'click', function(event) { - var option = Event.findElement(event); - var input = option.down('input[type="checkbox"]') || option.previous('.yes-no-picker').down('.yes input[type="checkbox"]'); // defaults to 'Y' when clicking on the text - if (!input) {return;} - if (input.checked) { - var negative = option.hasClassName('no'); - var categoryClone = categoryElt.clone(true); - if (negative) { - categoryClone.insert(new Element('input', {type: 'hidden', name : 'fieldName', value : input.name})); - } - qsWidget._suggestPicker.silent = true; - qsWidget._suggestPicker.acceptSuggestion({'id' : suggestion.id, 'value' : suggestion.label, 'category' : categoryClone, 'negative' : negative}); - qsWidget._suggestPicker.silent = false; - new XWiki.widgets.Notification("$services.localization.render('phenotips.PatientSheetCode.added')".replace("__name__", suggestion.id), 'done'); - dismissSuggestion(suggestion.id, suggestionElt); - } else { - var existingValue = $(qsWidget.id + '_' + input.value); - if (existingValue) { - existingValue.checked = false; - new XWiki.widgets.Notification("$services.localization.render('phenotips.PatientSheetCode.removed')".replace("__name__", suggestion.id), 'done'); - } + qsWidget._suggestPicker.silent = true; + qsWidget._suggestPicker.acceptSuggestion({'id' : suggestion.id, 'value' : suggestion.label, 'category' : categoryClone, 'negative' : negative}); + qsWidget._suggestPicker.silent = false; + new XWiki.widgets.Notification("$services.localization.render('phenotips.PatientSheetCode.added')".replace("__name__", suggestion.id), 'done'); + dismissSuggestion(suggestion.id, suggestionElt); + } else { + var existingValue = $(qsWidget.id + '_' + input.value); + if (existingValue) { + existingValue.checked = false; + new XWiki.widgets.Notification("$services.localization.render('phenotips.PatientSheetCode.removed')".replace("__name__", suggestion.id), 'done'); } - }); - } - return yesNoPicker; + } + }); } + return yesNoPicker; +} - /** - * Creates one suggestion element, with a yes-no-picker, label, quote and info tooltip. - * - * @param {Object} suggestion Annotation object with the fields: 'start', 'end', 'id' and 'label'. - */ - function generateSuggestionUIElement(suggestion) { - var start = suggestion.startInSentence; - var end = suggestion.endInSentence; - var quoteStart = 0; - var quoteEnd = text.length; - var suggestionBox = new Element('div', {'class' : 'suggestion'}); - var termCategories = new Element("span", {"class" : "hidden term-category"}); - var sentence = suggestion.sentence; - suggestion['term_category'].forEach(function(category) { - termCategories.insert( - new Element('input', {'type' : 'hidden', 'value' : category}) - ); - }); - var yesNoPicker = generateYesNoPicker(suggestion, suggestionBox, termCategories); - var dismissButton = new Element('div', {'class' : 'hide-suggestion'}).update("✖"); - dismissButton.observe('click', function() { - dismissSuggestion(suggestion.id, suggestionBox); - }); - - return suggestionBox.insert( - new Element('div', {'class' : 'suggestion-headline'}).insert( - yesNoPicker - ).insert( - new Element("span", {"class" : "suggestion-term"}).update(suggestion.label) - ).insert( - termCategories - ).insert('<span class="xHelpButton fa fa-info-circle phenotype-info" title="' + suggestion.id + '"></span>') - ).insert( - new Element('div', {'class' : 'suggestion-quote'}).insert( - "..." + sentence.substring(quoteStart, start) +/** + * Creates one suggestion element, with a yes-no-picker, label, quote and info tooltip. + * + * @param {Object} suggestion Annotation object with the fields: 'start', 'end', 'id' and 'label'. + */ +function generateSuggestionUIElement(suggestion) { + var start = suggestion.startInSentence; + var end = suggestion.endInSentence; + var quoteStart = 0; + var quoteEnd = text.length; + var suggestionBox = new Element('div', {'class' : 'suggestion'}); + var termCategories = new Element("span", {"class" : "hidden term-category"}); + var sentence = suggestion.sentence; + suggestion['term_category'].forEach(function(category) { + termCategories.insert( + new Element('input', {'type' : 'hidden', 'value' : category}) + ); + }); + var yesNoPicker = generateYesNoPicker(suggestion, suggestionBox, termCategories); + var dismissButton = new Element('div', {'class' : 'hide-suggestion'}).update("✖"); + dismissButton.observe('click', function() { + dismissSuggestion(suggestion.id, suggestionBox); + }); + + return suggestionBox.insert( + new Element('div', {'class' : 'suggestion-headline'}).insert( + yesNoPicker ).insert( - new Element('span', {'class' : 'quoted-term'}).update(sentence.substring(start, end)) + new Element("span", {"class" : "suggestion-term"}).update(suggestion.label) ).insert( - sentence.substring(end, quoteEnd) + "..." - ) + termCategories + ).insert('<span class="xHelpButton fa fa-info-circle phenotype-info" title="' + suggestion.id + '"></span>') + ).insert( + new Element('div', {'class' : 'suggestion-quote'}).insert( + "..." + sentence.substring(quoteStart, start) ).insert( - dismissButton - ); - } - - /** - * Queries server and updates list of previously dismissed or considered suggestions. - * - * @param {function} callback Will be invoked when the request is complete. - */ - function fetchDismissedSuggestions(callback) { - new Ajax.Request(XWiki.currentDocument.getRestURL('objects/PhenoTips.DismissedSuggestionsClass/0/properties/terms'), { - 'method': 'get', - 'requestHeaders': {'Accept': 'application/json'}, - onSuccess: function(response) { - dismissedTermsKeys = response.responseJSON.value.split('|'); - dismissedTermsKeys.forEach(function(termId) { - dismissedTerms[termId] = true; - }); - existsDismissedSuggestionDataStore = true; - }, - onComplete: function(response) { - callback(); - } - }); - }; + new Element('span', {'class' : 'quoted-term'}).update(sentence.substring(start, end)) + ).insert( + sentence.substring(end, quoteEnd) + "..." + ) + ).insert( + dismissButton + ); +} - /** - * Add term with termId to list of dismissed suggestions and update the dismissed - * suggestions on the server. - * - * @param {string} termId Ontology ID of the phenotype term. - * @param {Element} suggestionElt UI Element of one suggestion. - */ - function dismissSuggestion(termId, suggestionElt) { - dismissedTerms[termId] = true; - var parameters = { - 'ajax': true, - 'form_token': $$("meta[name='form_token']")[0].content - }; - var delimitedDismissedTerms = Object.keys(dismissedTerms).join('|'); - if (existsDismissedSuggestionDataStore) { - parameters['PhenoTips.DismissedSuggestionsClass_0_terms'] = delimitedDismissedTerms; - } else { - parameters['classname'] = 'PhenoTips.DismissedSuggestionsClass'; - parameters['PhenoTips.DismissedSuggestionsClass_terms'] = delimitedDismissedTerms; - } - new Ajax.Request(XWiki.currentDocument.getURL((existsDismissedSuggestionDataStore ? 'save' : 'objectadd')), { - 'parameters': parameters, - onSuccess : function() { - existsDismissedSuggestionDataStore = true; - } - }); - var nextSuggestion = popNextSuggestion(); - delete currentSuggestions[termId]; - - if(nextSuggestion) { - currentSuggestions[nextSuggestion.id] = nextSuggestion; - suggestionElt.replace(generateSuggestionUIElement(nextSuggestion)); - Event.fire(document, 'xwiki:dom:updated', {'elements' : [suggestionElt]}); - } else { - suggestionElt.remove(); +/** + * Queries server and updates list of previously dismissed or considered suggestions. + * + * @param {function} callback Will be invoked when the request is complete. + */ +function fetchDismissedSuggestions(callback) { + new Ajax.Request(XWiki.currentDocument.getRestURL('objects/PhenoTips.DismissedSuggestionsClass/0/properties/terms'), { + 'method': 'get', + 'requestHeaders': {'Accept': 'application/json'}, + onSuccess: function(response) { + dismissedTermsKeys = response.responseJSON.value.split('|'); + dismissedTermsKeys.forEach(function(termId) { + dismissedTerms[termId] = true; + }); + existsDismissedSuggestionDataStore = true; + }, + onComplete: function(response) { + callback(); } - updateSuggestionCount(); - }; - - /** - * Returns a list of phenotypes already added for this patient. - */ - function getExistingPhenotypes() { - listedTerms = {}; - $$(".yes-no-picker").each(function(element) { - var yesInput = element.down('.yes input'); - var noInput = element.down('.no input'); - if (yesInput.name === noInput.name) { - // Not a phenotype - return; - } - var key = yesInput.value; - var existing = listedTerms[key]; - var enable = element.down('.na input') && !element.down('.na input').checked; - if (!enable) { - // not listed phenotype - return; - } - if (!existing) { - listedTerms[key] = true; - } - }); - return listedTerms; - }; + }); +}; - /** - * Generates suggestion list from annotations, but filtering out - * dismissed and existing phenotypes. - */ - function populateSuggestionData() { - suggestionBank = []; - var listedTerms = getExistingPhenotypes(); - Object.keys(annotationServiceResults).forEach(function(termId) { - if (!listedTerms[termId] && !dismissedTerms[termId]) { - suggestionBank.push(annotationServiceResults[termId]); - } - }); +/** + * Add term with termId to list of dismissed suggestions and update the dismissed + * suggestions on the server. + * + * @param {string} termId Ontology ID of the phenotype term. + * @param {Element} suggestionElt UI Element of one suggestion. + */ +function dismissSuggestion(termId, suggestionElt) { + dismissedTerms[termId] = true; + var parameters = { + 'ajax': true, + 'form_token': $$("meta[name='form_token']")[0].content }; - - /** - * Removes the next numSuggestions of suggestions from suggestionBank and returns them. - * @param {Number} numSuggestions The number of suggestions to remove. - */ - function popSuggestions(numSuggestions) { - var suggestionsToDisplay = []; - var limit = Math.min(numSuggestions, suggestionBank.length); - for (var i = 0; i < limit; i++) { - suggestionsToDisplay.push(suggestionBank.shift()); + var delimitedDismissedTerms = Object.keys(dismissedTerms).join('|'); + if (existsDismissedSuggestionDataStore) { + parameters['PhenoTips.DismissedSuggestionsClass_0_terms'] = delimitedDismissedTerms; + } else { + parameters['classname'] = 'PhenoTips.DismissedSuggestionsClass'; + parameters['PhenoTips.DismissedSuggestionsClass_terms'] = delimitedDismissedTerms; + } + new Ajax.Request(XWiki.currentDocument.getURL((existsDismissedSuggestionDataStore ? 'save' : 'objectadd')), { + 'parameters': parameters, + onSuccess : function() { + existsDismissedSuggestionDataStore = true; } - return suggestionsToDisplay; - }; - - /** - * Removes the next suggestion from suggestionBank and returns it. - */ - function popNextSuggestion() { - var suggestionList = popSuggestions(1); - if (suggestionList.length != 1) { - return null; + }); + var nextSuggestion = popNextSuggestion(); + delete currentSuggestions[termId]; + + if(nextSuggestion) { + currentSuggestions[nextSuggestion.id] = nextSuggestion; + suggestionElt.replace(generateSuggestionUIElement(nextSuggestion)); + Event.fire(document, 'xwiki:dom:updated', {'elements' : [suggestionElt]}); + } else { + suggestionElt.remove(); + } + updateSuggestionCount(); +}; + +/** + * Returns a list of phenotypes already added for this patient. + */ +function getExistingPhenotypes() { + listedTerms = {}; + $$(".yes-no-picker").each(function(element) { + var yesInput = element.down('.yes input'); + var noInput = element.down('.no input'); + if (yesInput.name === noInput.name) { + // Not a phenotype + return; + } + var key = yesInput.value; + var existing = listedTerms[key]; + var enable = element.down('.na input') && !element.down('.na input').checked; + if (!enable) { + // not listed phenotype + return; + } + if (!existing) { + listedTerms[key] = true; } - return suggestionList[0]; + }); + return listedTerms; +}; + +/** + * Generates suggestion list from annotations, but filtering out + * dismissed and existing phenotypes. + */ +function populateSuggestionData() { + suggestionBank = []; + var listedTerms = getExistingPhenotypes(); + Object.keys(annotationServiceResults).forEach(function(termId) { + if (!listedTerms[termId] && !dismissedTerms[termId]) { + suggestionBank.push(annotationServiceResults[termId]); + } + }); +}; + +/** + * Removes the next numSuggestions of suggestions from suggestionBank and returns them. + * @param {Number} numSuggestions The number of suggestions to remove. + */ +function popSuggestions(numSuggestions) { + var suggestionsToDisplay = []; + var limit = Math.min(numSuggestions, suggestionBank.length); + for (var i = 0; i < limit; i++) { + suggestionsToDisplay.push(suggestionBank.shift()); } + return suggestionsToDisplay; +}; + +/** + * Removes the next suggestion from suggestionBank and returns it. + */ +function popNextSuggestion() { + var suggestionList = popSuggestions(1); + if (suggestionList.length != 1) { + return null; + } + return suggestionList[0]; +} - /** - * Clears the UI and draws all the elements. - */ - function drawSuggestionsUI() { - var suggestionsContainer = new Element('div', {'id' : 'suggestions-container'}); - widgetContainer.update(suggestionsContainer); - //scrap what exists, create UI from 4 first elements in suggestions - var suggestionBoxes = []; - Object.keys(currentSuggestions).forEach(function(termId) { - suggestionBoxes.push(generateSuggestionUIElement(currentSuggestions[termId])); - }); - - //draw suggestion boxes - var suggestionList = new Element('ul', {"class" : "suggestions-list"}); - suggestionBoxes.forEach(function(suggestion) { - suggestionList.insert( - new Element('li', {"class" : "suggestion-list-item"}).insert(suggestion) - ) - }); - suggestionsContainer.update(suggestionList); - Event.fire(document, 'xwiki:dom:updated', {'elements' : [suggestionsContainer]}); - - //show progress counter - widgetContainer.insert(new Element("div", {"id" : "suggestion-count"})); - updateSuggestionCount(); - }; - - /** - * Updates the UI element that shows the progress of going through suggestions. - */ - function updateSuggestionCount() { - var numCurrentSuggestions = Object.keys(currentSuggestions).length; - var numTotalSuggestions = numCurrentSuggestions + suggestionBank.length; - if(numTotalSuggestions > 0) { - $("suggestion-count").update(numCurrentSuggestions + " of " + numTotalSuggestions + " suggestions"); - } else { - $("suggestion-count").update("No suggestions to display"); - } +/** + * Clears the UI and draws all the elements. + */ +function drawSuggestionsUI() { + var suggestionsContainer = new Element('div', {'id' : 'suggestions-container'}); + widgetContainer.update(suggestionsContainer); + //scrap what exists, create UI from 4 first elements in suggestions + var suggestionBoxes = []; + Object.keys(currentSuggestions).forEach(function(termId) { + suggestionBoxes.push(generateSuggestionUIElement(currentSuggestions[termId])); + }); + + //draw suggestion boxes + var suggestionList = new Element('ul', {"class" : "suggestions-list"}); + suggestionBoxes.forEach(function(suggestion) { + suggestionList.insert( + new Element('li', {"class" : "suggestion-list-item"}).insert(suggestion) + ) + }); + suggestionsContainer.update(suggestionList); + Event.fire(document, 'xwiki:dom:updated', {'elements' : [suggestionsContainer]}); + + //show progress counter + widgetContainer.insert(new Element("div", {"id" : "suggestion-count"})); + updateSuggestionCount(); +}; + +/** + * Updates the UI element that shows the progress of going through suggestions. + */ +function updateSuggestionCount() { + var numCurrentSuggestions = Object.keys(currentSuggestions).length; + var numTotalSuggestions = numCurrentSuggestions + suggestionBank.length; + if(numTotalSuggestions > 0) { + $("suggestion-count").update(numCurrentSuggestions + " of " + numTotalSuggestions + " suggestions"); + } else { + $("suggestion-count").update("No suggestions to display"); } -}); +} +}); + From c678d4c9b3d21065018313d9831f653c66251ec5 Mon Sep 17 00:00:00 2001 From: Joe C Date: Sun, 12 Jun 2016 10:26:41 -0400 Subject: [PATCH 04/14] [misc] A variety of minor UI improvements. These include: - Add a class to persist dismissed suggestions from notes. - Listen for changes on the medical history too. - Add a refresh button. - Implement pagination --- .../PhenoTips/Clinical Notes Annotation.xml | 178 ++++++++++++++---- .../PhenoTips/DismissedSuggestionsClass.xml | 66 +++++++ 2 files changed, 211 insertions(+), 33 deletions(-) create mode 100644 ui/src/main/resources/PhenoTips/DismissedSuggestionsClass.xml diff --git a/ui/src/main/resources/PhenoTips/Clinical Notes Annotation.xml b/ui/src/main/resources/PhenoTips/Clinical Notes Annotation.xml index 076884a..d41d15a 100644 --- a/ui/src/main/resources/PhenoTips/Clinical Notes Annotation.xml +++ b/ui/src/main/resources/PhenoTips/Clinical Notes Annotation.xml @@ -151,6 +151,11 @@ var currentSuggestions = {}; */ var existsDismissedSuggestionDataStore = false; +/** + * The index of the first suggestion being displayed at the moment. + */ +var currentFirstSuggestion = 0; + /** * Number of suggestions to display in the UI */ @@ -166,6 +171,20 @@ var text = ""; */ var widgetContainer = new Element('div', {"id" : 'annotation-widget-container'}); +/** + * A refresh button. + */ +var refreshButton = new Element('span', {"id" : 'annotation-refresh', "class" : 'fa fa-refresh xHelpButton', 'aria-hidden': 'true'}); + +refreshButton.observe('click', function() { + dismissedTerms = {}; + annotationServiceResults = {}; + suggestionBank = []; + currentSuggestions = {}; + persistDismissed(); + updateAnnotations(); +}); + //generate initial UI elements var panel = $$(".current-phenotype-selection")[0]; if(panel) { @@ -173,20 +192,40 @@ if(panel) { new Element('div', {"class" : "sub-panel"}).insert( new Element('h3', {"class" : "wikigeneratedheader"}).update( "<span><strong>Suggestions from Clinical Notes</strong></span>" - ) + ).insert(refreshButton) ).insert(widgetContainer); panel.insert({top : clinicalNotesSubPanel}); //listen for changes in indication for referral text box - var textArea = $("PhenoTips.PatientClass_0_indication_for_referral"); - text = textArea.value; + var referalIndication = $("PhenoTips.PatientClass_0_indication_for_referral"); + var medicalHistory = $("PhenoTips.PatientClass_0_medical_history"); + text = referalIndication.value + "\n" + medicalHistory.value; fetchDismissedSuggestions(updateAnnotations); - textArea.observe('blur', function() { - if(textArea.value != text) { - text = textArea.value; + referalIndication.observe('blur', function() { + if(!text.startsWith(referalIndication.value)) { + text = referalIndication.value + "\n" + medicalHistory.value; updateAnnotations(); } }); + medicalHistory.observe('blur', function() { + if(!text.endsWith(medicalHistory.value)) { + text = referalIndication.value + "\n" + medicalHistory.value; + updateAnnotations(); + } + }); +} + +/** + * Update the currentSuggestions dictionary, populating it with suggestions from the suggestion + * bank, then redrawing the UI + */ +function updateCurrentSuggestions() { + currentSuggestions = {}; + var end = currentFirstSuggestion + numSuggestionsToDisplay; + suggestionBank.slice(currentFirstSuggestion, end).forEach(function(suggestion, idx) { + currentSuggestions[suggestion.id] = idx + currentFirstSuggestion; + }); + drawSuggestionsUI(); } /** @@ -194,6 +233,10 @@ if(panel) { * displays suggestions in the UI. */ function updateAnnotations() { + /* This (resetting the currentFirstSuggestion) is gonna be a bit annoying if a user has advanced + * some pages and then revises the text box, but unfortunately it's really the only way to be + * sure we're not gonna paginate past the end. */ + currentFirstSuggestion = 0; if(text) { currentSuggestions = {}; var queryString = '?outputSyntax=plain'; @@ -216,12 +259,7 @@ function updateAnnotations() { getExistingPhenotypes(); populateSuggestionData(); }, - onComplete : function () { - popSuggestions(numSuggestionsToDisplay).forEach(function(suggestion) { - currentSuggestions[suggestion.id] = suggestion; - }); - drawSuggestionsUI(); - } + onComplete : updateCurrentSuggestions }); } else { annotationServiceResults = {}; @@ -285,7 +323,8 @@ function generateYesNoPicker(suggestion, suggestionElt, categoryElt) { * * @param {Object} suggestion Annotation object with the fields: 'start', 'end', 'id' and 'label'. */ -function generateSuggestionUIElement(suggestion) { +function generateSuggestionUIElement(suggestionIdx) { + var suggestion = suggestionBank[suggestionIdx]; var start = suggestion.startInSentence; var end = suggestion.endInSentence; var quoteStart = 0; @@ -348,19 +387,14 @@ function fetchDismissedSuggestions(callback) { }; /** - * Add term with termId to list of dismissed suggestions and update the dismissed - * suggestions on the server. - * - * @param {string} termId Ontology ID of the phenotype term. - * @param {Element} suggestionElt UI Element of one suggestion. + * Send the list of dismissed suggestions to the server so they're remembered. */ -function dismissSuggestion(termId, suggestionElt) { - dismissedTerms[termId] = true; +function persistDismissed() { + var delimitedDismissedTerms = Object.keys(dismissedTerms).join('|'); var parameters = { 'ajax': true, 'form_token': $$("meta[name='form_token']")[0].content }; - var delimitedDismissedTerms = Object.keys(dismissedTerms).join('|'); if (existsDismissedSuggestionDataStore) { parameters['PhenoTips.DismissedSuggestionsClass_0_terms'] = delimitedDismissedTerms; } else { @@ -373,12 +407,33 @@ function dismissSuggestion(termId, suggestionElt) { existsDismissedSuggestionDataStore = true; } }); - var nextSuggestion = popNextSuggestion(); +} + +/** + * Add term with termId to list of dismissed suggestions and update the dismissed + * suggestions on the server. + * + * @param {string} termId Ontology ID of the phenotype term. + * @param {Element} suggestionElt UI Element of one suggestion. + */ +function dismissSuggestion(termId, suggestionElt) { + dismissedTerms[termId] = true; + persistDismissed(); + //var nextSuggestion = popNextSuggestion(); + var deletedIdx = currentSuggestions[termId]; delete currentSuggestions[termId]; + suggestionBank.splice(deletedIdx, 1); + Object.keys(currentSuggestions).forEach(function(key) { + if (currentSuggestions[key] > deletedIdx) { + currentSuggestions[key] -= 1; + } + }); + var nextIdx = currentFirstSuggestion + numSuggestionsToDisplay - 1; - if(nextSuggestion) { - currentSuggestions[nextSuggestion.id] = nextSuggestion; - suggestionElt.replace(generateSuggestionUIElement(nextSuggestion)); + if(nextIdx < suggestionBank.length) { + var nextSuggestion = suggestionBank[nextIdx]; + currentSuggestions[nextSuggestion.id] = nextIdx; + suggestionElt.replace(generateSuggestionUIElement(nextIdx)); Event.fire(document, 'xwiki:dom:updated', {'elements' : [suggestionElt]}); } else { suggestionElt.remove(); @@ -456,6 +511,9 @@ function popNextSuggestion() { function drawSuggestionsUI() { var suggestionsContainer = new Element('div', {'id' : 'suggestions-container'}); widgetContainer.update(suggestionsContainer); + + + //scrap what exists, create UI from 4 first elements in suggestions var suggestionBoxes = []; Object.keys(currentSuggestions).forEach(function(termId) { @@ -464,16 +522,35 @@ function drawSuggestionsUI() { //draw suggestion boxes var suggestionList = new Element('ul', {"class" : "suggestions-list"}); - suggestionBoxes.forEach(function(suggestion) { - suggestionList.insert( - new Element('li', {"class" : "suggestion-list-item"}).insert(suggestion) - ) + suggestionBoxes.forEach(function(suggestion) { + suggestionList.insert( + new Element('li', {"class" : "suggestion-list-item"}).insert(suggestion) + ) }); suggestionsContainer.update(suggestionList); Event.fire(document, 'xwiki:dom:updated', {'elements' : [suggestionsContainer]}); //show progress counter widgetContainer.insert(new Element("div", {"id" : "suggestion-count"})); + + if (currentFirstSuggestion > 0) { + suggestionsContainer.insert({'top': new Element('span', {'class': 'fa fa-chevron-left navigation'}).observe('click', function() { + var prevPage = currentFirstSuggestion - numSuggestionsToDisplay; + currentFirstSuggestion = Math.max(prevPage, 0); + updateCurrentSuggestions(); + }).insert(new Element('input', {"value": "1", "type": "hidden"}))}); + } + + + if ((currentFirstSuggestion + numSuggestionsToDisplay) < suggestionBank.length) { + suggestionsContainer.insert(new Element('span', {'class': 'fa fa-chevron-right navigation'}).observe('click', function() { + var nextPage = currentFirstSuggestion + numSuggestionsToDisplay; + currentFirstSuggestion = Math.min(nextPage, suggestionBank.length - numSuggestionsToDisplay); + currentFirstSuggestion = Math.max(currentFirstSuggestion, 0); + updateCurrentSuggestions(); + }).insert(new Element('input', {"value": "2", "type": "hidden"}))); + } + updateSuggestionCount(); }; @@ -482,9 +559,12 @@ function drawSuggestionsUI() { */ function updateSuggestionCount() { var numCurrentSuggestions = Object.keys(currentSuggestions).length; - var numTotalSuggestions = numCurrentSuggestions + suggestionBank.length; + var numTotalSuggestions = suggestionBank.length; + var end = currentFirstSuggestion + numCurrentSuggestions; + /* Arrays are 0 indexed, but users are not. */ + var start = currentFirstSuggestion + 1; if(numTotalSuggestions > 0) { - $("suggestion-count").update(numCurrentSuggestions + " of " + numTotalSuggestions + " suggestions"); + $("suggestion-count").update(start + " to " + end + " of " + numTotalSuggestions + " suggestions"); } else { $("suggestion-count").update("No suggestions to display"); } @@ -582,7 +662,9 @@ function updateSuggestionCount() { forbid - .loading-container { + #template("colorThemeInit.vm") + +.loading-container { text-align: center; margin: 1em; } @@ -596,6 +678,7 @@ function updateSuggestionCount() { -webkit-column-count: 2; /* Chrome, Safari, Opera */ -moz-column-count: 2; /* Firefox */ column-count: 2; + display: table; } #suggestions-container .suggestions-list { margin: 0 2em 0 2em; @@ -603,6 +686,9 @@ function updateSuggestionCount() { #suggestions-container .suggestion-list-item { list-style-type: none; + display: table-cell; + float: left; + width: 50%; } #suggestions-container .suggestion { @@ -643,16 +729,42 @@ margin: 0; cursor: pointer; } +#annotation-refresh { + float: right; +} + #suggestion-count { text-align: center; margin-bottom: 1em; + } + +#suggestions-container > ul { + padding-bottom: 0.3em; + padding-left: 1em; + width: 90%; +} + +#suggestions-container > * { + display: table-cell; + vertical-align: middle; +} + +#suggestions-container > span { + text-align: center; +} + +#suggestions-container > .navigation:hover { + background-color: rgba(0,0,0,0.1); + color: $theme.linkColor; + cursor: pointer; + transition: all 0.2s linear; } - + 1 currentPage diff --git a/ui/src/main/resources/PhenoTips/DismissedSuggestionsClass.xml b/ui/src/main/resources/PhenoTips/DismissedSuggestionsClass.xml new file mode 100644 index 0000000..d4819af --- /dev/null +++ b/ui/src/main/resources/PhenoTips/DismissedSuggestionsClass.xml @@ -0,0 +1,66 @@ + + + + + + PhenoTips + DismissedSuggestionsClass + + + 0 + xwiki:XWiki.Admin + 1401822127000 + PhenoTips.WebHome + xwiki:XWiki.Admin + xwiki:XWiki.Admin + 1401822127000 + 1401822127000 + 1.1 + + <comment/> + <minorEdit>false</minorEdit> + <syntaxId>xwiki/2.1</syntaxId> + <hidden>true</hidden> + <content/> + <class> + <name>PhenoTips.DismissedSuggestionsClass</name> + <customClass/> + <customMapping/> + <defaultViewSheet/> + <defaultEditSheet/> + <defaultWeb/> + <nameField/> + <validationScript/> + <terms> + <contenttype>PureText</contenttype> + <customDisplay/> + <disabled>0</disabled> + <editor>PureText</editor> + <name>terms</name> + <number>1</number> + <picker>0</picker> + <prettyName>Terms</prettyName> + <unmodifiable>0</unmodifiable> + <validationMessage/> + <validationRegExp/> + <classType>com.xpn.xwiki.objects.classes.TextAreaClass</classType> + </terms> + </class> +</xwikidoc> + From 8713b5511d4a6f59547111cd1a2044fc133f3f32 Mon Sep 17 00:00:00 2001 From: Jose <jose.cortesvarela@mail.utoronto.ca> Date: Fri, 10 Jun 2016 16:53:18 -0400 Subject: [PATCH 05/14] Add a scigraph based clinical text annotation module. --- api/pom.xml | 5 +- .../textanalysis/TermAnnotation.java | 9 + .../textanalysis/TermAnnotationService.java | 13 +- .../TermAnnotationSentenceDetector.java | 4 + .../script/TermAnnotationScriptService.java | 2 +- .../TermAnnotationSentenceDetectorTest.java | 4 +- generic-rest/pom.xml | 147 ++++++++++++ .../textanalysis/internal/AnnotationAPI.java | 93 ++++++++ .../internal/AnnotationAPIImpl.java | 135 +++++++++++ .../GenericRESTAnnotationService.java | 98 ++++++++ .../textanalysis/internal/RESTWrapper.java | 154 ++++++++++++ .../internal/RESTWrapperImpl.java | 82 +++++++ .../main/resources/META-INF/components.txt | 3 + .../GenericRESTAnnotationServiceTest.java | 185 +++++++++++++++ pom.xml | 3 +- scigraph-service/pom.xml | 36 +++ scigraph-service/war/pom.xml | 145 ++++++++++++ .../annotation/PTEntityProcessor.java | 102 ++++++++ .../annotation/PTShingleProducer.java | 158 +++++++++++++ .../annotation/PunctuationFilter.java | 94 ++++++++ .../services/PTSciGraphApplication.java | 117 ++++++++++ .../scigraph/services/PTSciGraphModule.java | 82 +++++++ .../scigraph/vocabulary/PTVocabularyImpl.java | 221 ++++++++++++++++++ .../phenotips/scigraphwar/ScigraphWebApp.java | 48 ++++ .../war/src/main/resources/load.yaml | 48 ++++ .../war/src/main/resources/server.yaml | 34 +++ .../war/src/main/webapp/WEB-INF/web.xml | 32 +++ ui/pom.xml | 10 +- 28 files changed, 2054 insertions(+), 10 deletions(-) create mode 100644 generic-rest/pom.xml create mode 100644 generic-rest/src/main/java/org/phenotips/textanalysis/internal/AnnotationAPI.java create mode 100644 generic-rest/src/main/java/org/phenotips/textanalysis/internal/AnnotationAPIImpl.java create mode 100644 generic-rest/src/main/java/org/phenotips/textanalysis/internal/GenericRESTAnnotationService.java create mode 100644 generic-rest/src/main/java/org/phenotips/textanalysis/internal/RESTWrapper.java create mode 100644 generic-rest/src/main/java/org/phenotips/textanalysis/internal/RESTWrapperImpl.java create mode 100644 generic-rest/src/main/resources/META-INF/components.txt create mode 100644 generic-rest/src/test/java/org/phenotips/textanalysis/internal/GenericRESTAnnotationServiceTest.java create mode 100644 scigraph-service/pom.xml create mode 100644 scigraph-service/war/pom.xml create mode 100644 scigraph-service/war/src/main/java/io/scigraph/annotation/PTEntityProcessor.java create mode 100644 scigraph-service/war/src/main/java/io/scigraph/annotation/PTShingleProducer.java create mode 100644 scigraph-service/war/src/main/java/io/scigraph/annotation/PunctuationFilter.java create mode 100644 scigraph-service/war/src/main/java/io/scigraph/services/PTSciGraphApplication.java create mode 100644 scigraph-service/war/src/main/java/io/scigraph/services/PTSciGraphModule.java create mode 100644 scigraph-service/war/src/main/java/io/scigraph/vocabulary/PTVocabularyImpl.java create mode 100644 scigraph-service/war/src/main/java/org/phenotips/scigraphwar/ScigraphWebApp.java create mode 100644 scigraph-service/war/src/main/resources/load.yaml create mode 100644 scigraph-service/war/src/main/resources/server.yaml create mode 100644 scigraph-service/war/src/main/webapp/WEB-INF/web.xml diff --git a/api/pom.xml b/api/pom.xml index c8b3392..5d8b877 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -23,7 +23,7 @@ <parent> <groupId>org.phenotips</groupId> <artifactId>clinical-text-analysis-extension</artifactId> - <version>1.0-SNAPSHOT</version> + <version>1.3-SNAPSHOT</version> </parent> <artifactId>clinical-text-analysis-extension-api</artifactId> <name>PhenoTips - Clinical Text Analysis - Java API</name> @@ -104,4 +104,7 @@ <scope>test</scope> </dependency> </dependencies> + <properties> + <coverage.instructionRatio>0.16</coverage.instructionRatio> + </properties> </project> diff --git a/api/src/main/java/org/phenotips/textanalysis/TermAnnotation.java b/api/src/main/java/org/phenotips/textanalysis/TermAnnotation.java index 11d0b4a..5e7b082 100644 --- a/api/src/main/java/org/phenotips/textanalysis/TermAnnotation.java +++ b/api/src/main/java/org/phenotips/textanalysis/TermAnnotation.java @@ -33,10 +33,19 @@ public class TermAnnotation implements Comparable<TermAnnotation> private final VocabularyTerm mTerm; + /** + * The sentence this term annotation appears in. + */ private String sentence; + /** + * The start of this annotation within the sentence. + */ private long startInSentence; + /** + * The end of this annotation within the sentence. + */ private long endInSentence; /** diff --git a/api/src/main/java/org/phenotips/textanalysis/TermAnnotationService.java b/api/src/main/java/org/phenotips/textanalysis/TermAnnotationService.java index 38a2549..d42b524 100644 --- a/api/src/main/java/org/phenotips/textanalysis/TermAnnotationService.java +++ b/api/src/main/java/org/phenotips/textanalysis/TermAnnotationService.java @@ -45,7 +45,7 @@ public interface TermAnnotationService * @version $Id$ * @since 1.0M1 */ - public class AnnotationException extends Exception + class AnnotationException extends Exception { /** * Constructs a new AnnotationException with the specified detail message. @@ -56,5 +56,16 @@ public AnnotationException(String message) { super(message); } + + /** + * Constructs a new AnnotationException with the specified detail message and cause. + * + * @param message the detail message + * @param cause the cause + */ + public AnnotationException(String message, Exception cause) + { + super(message, cause); + } } } diff --git a/api/src/main/java/org/phenotips/textanalysis/internal/TermAnnotationSentenceDetector.java b/api/src/main/java/org/phenotips/textanalysis/internal/TermAnnotationSentenceDetector.java index b0610b1..a3252dc 100644 --- a/api/src/main/java/org/phenotips/textanalysis/internal/TermAnnotationSentenceDetector.java +++ b/api/src/main/java/org/phenotips/textanalysis/internal/TermAnnotationSentenceDetector.java @@ -27,6 +27,8 @@ /** * Takes in lists of term annotations, as well as the text they appear in and * assigns sentences to them from the text. + * + * @version $Id$ */ public class TermAnnotationSentenceDetector { @@ -45,7 +47,9 @@ public void detectSentences(List<TermAnnotation> annotations, String text) while (currentSentence != BreakIterator.DONE && currentAnnotation < annotations.size()) { TermAnnotation annotation = annotations.get(currentAnnotation); int nextSentence = sentences.next(); + /* next() pushes the iterator forward, so bring it back */ sentences.previous(); + /* Does this annotation fall within the current sentence? */ if (annotation.getStartPos() >= currentSentence && annotation.getStartPos() < nextSentence) { long start = annotation.getStartPos() - currentSentence; long end = annotation.getEndPos() - currentSentence; diff --git a/api/src/main/java/org/phenotips/textanalysis/script/TermAnnotationScriptService.java b/api/src/main/java/org/phenotips/textanalysis/script/TermAnnotationScriptService.java index 7142713..8d0d845 100644 --- a/api/src/main/java/org/phenotips/textanalysis/script/TermAnnotationScriptService.java +++ b/api/src/main/java/org/phenotips/textanalysis/script/TermAnnotationScriptService.java @@ -44,7 +44,7 @@ public class TermAnnotationScriptService implements ScriptService { @Inject - @Named("biolark") + @Named("genericREST") private TermAnnotationService service; /** diff --git a/api/src/test/java/org/phenotips/textanalysis/internal/TermAnnotationSentenceDetectorTest.java b/api/src/test/java/org/phenotips/textanalysis/internal/TermAnnotationSentenceDetectorTest.java index 6a9e63a..fb37534 100644 --- a/api/src/test/java/org/phenotips/textanalysis/internal/TermAnnotationSentenceDetectorTest.java +++ b/api/src/test/java/org/phenotips/textanalysis/internal/TermAnnotationSentenceDetectorTest.java @@ -33,7 +33,6 @@ public class TermAnnotationSentenceDetectorTest { - private TermAnnotationSentenceDetector client; /* This is where you miss heredocs. */ @@ -64,6 +63,9 @@ public class TermAnnotationSentenceDetectorTest SENTENCE3 = TEXT.substring(343); } + /** + * Test that we're able to have one annotation per sentence. + */ @Test public void testBasic() { diff --git a/generic-rest/pom.xml b/generic-rest/pom.xml new file mode 100644 index 0000000..1d34adc --- /dev/null +++ b/generic-rest/pom.xml @@ -0,0 +1,147 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<!-- + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see http://www.gnu.org/licenses/ +--> + +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.phenotips</groupId> + <artifactId>clinical-text-analysis-extension</artifactId> + <version>1.3-SNAPSHOT</version> + </parent> + <artifactId>clinical-text-analysis-extension-generic-rest</artifactId> + <name>PhenoTips - Clinical Text Analysis - Generic REST implementation for the Java APIs</name> + <repositories> + <repository> + <id>CRBS</id> + <name>CRBS Maven Repo</name> + <url>http://maven.crbs.ucsd.edu/nexus/content/repositories/NIF-snapshot/</url> + </repository> + </repositories> + <dependencies> + <dependency> + <groupId>org.xwiki.commons</groupId> + <artifactId>xwiki-commons-component-api</artifactId> + <version>${xwiki.version}</version> + </dependency> + <dependency> + <groupId>org.apache.commons</groupId> + <artifactId>commons-lang3</artifactId> + </dependency> + <dependency> + <groupId>org.xwiki.platform</groupId> + <artifactId>xwiki-platform-bridge</artifactId> + <version>${xwiki.version}</version> + </dependency> + <dependency> + <groupId>org.xwiki.platform</groupId> + <artifactId>xwiki-platform-extension-distribution</artifactId> + <version>${xwiki.version}</version> + </dependency> + <dependency> + <groupId>org.xwiki.platform</groupId> + <artifactId>xwiki-platform-model</artifactId> + <version>${xwiki.version}</version> + </dependency> + <dependency> + <groupId>org.xwiki.platform</groupId> + <artifactId>xwiki-platform-oldcore</artifactId> + <version>${xwiki.version}</version> + <exclusions> + <exclusion> + <groupId>commons-validator</groupId> + <artifactId>commons-validator</artifactId> + </exclusion> + </exclusions> + </dependency> + <dependency> + <groupId>org.xwiki.platform</groupId> + <artifactId>xwiki-platform-query-manager</artifactId> + <version>${xwiki.version}</version> + </dependency> + <dependency> + <groupId>org.xwiki.commons</groupId> + <artifactId>xwiki-commons-context</artifactId> + <version>${xwiki.version}</version> + </dependency> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>clinical-text-analysis-extension-api</artifactId> + <version>${project.version}</version> + <exclusions> + <exclusion> + <groupId>commons-validator</groupId> + <artifactId>commons-validator</artifactId> + </exclusion> + </exclusions> + </dependency> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>phenotips-constants</artifactId> + <version>${phenotips.version}</version> + </dependency> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>vocabularies-api</artifactId> + <version>${phenotips.version}</version> + </dependency> + <dependency> + <groupId>net.sf.json-lib</groupId> + <artifactId>json-lib</artifactId> + <classifier>jdk15</classifier> + <version>2.3</version> + </dependency> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-api</artifactId> + </dependency> + <!-- Test dependencies --> + <dependency> + <groupId>org.xwiki.commons</groupId> + <artifactId>xwiki-commons-tool-test-component</artifactId> + <version>${xwiki.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>javax.servlet</groupId> + <artifactId>javax.servlet-api</artifactId> + </dependency> + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-all</artifactId> + <version>1.10.8</version> + </dependency> + + <!-- Scigraph dependencies --> + <dependency> + <groupId>com.fasterxml.jackson.dataformat</groupId> + <artifactId>jackson-dataformat-yaml</artifactId> + <!-- TODO: What's the best way to get the proper version? --> + <version>2.5.4</version> + </dependency> + <dependency> + <groupId>org.apache.httpcomponents</groupId> + <artifactId>fluent-hc</artifactId> + <version>4.4.1</version> + </dependency> + </dependencies> + <properties> + <coverage.instructionRatio>0.16</coverage.instructionRatio> + </properties> +</project> diff --git a/generic-rest/src/main/java/org/phenotips/textanalysis/internal/AnnotationAPI.java b/generic-rest/src/main/java/org/phenotips/textanalysis/internal/AnnotationAPI.java new file mode 100644 index 0000000..8bdb328 --- /dev/null +++ b/generic-rest/src/main/java/org/phenotips/textanalysis/internal/AnnotationAPI.java @@ -0,0 +1,93 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see http://www.gnu.org/licenses/ + */ +package org.phenotips.textanalysis.internal; + +import org.xwiki.component.annotation.Role; + +import java.io.InputStream; +import java.util.Map; + +/** + * Interacts with an annotation REST API endpoint, with methods as defined in {@link GenericRESTAnnotationService}. + * + * @version $Id$ + */ +@Role +public interface AnnotationAPI +{ + /** + * Execute a post request to the method given, taking content to be the json body of the request. + * @param method the method + * @param content the content, to be interpreted as containing a json object + * @return the response + * @throws ServiceException if there's an error accessing the method + */ + InputStream postJson(String method, InputStream content) throws ServiceException; + + /** + * Post the form given to the method given. + * @param method the method + * @param params the form parameters + * @return the response + * @throws ServiceException if there's an error accessing the method + */ + InputStream postForm(String method, Map<String, String> params) throws ServiceException; + + /** + * Send an empty post to the method given. + * @param method the method + * @return the response + * @throws ServiceException if there's an error accessing the method + */ + InputStream postEmpty(String method) throws ServiceException; + + /** + * Send an empty get to the method given. + * @param method the method + * @return the response + * @throws ServiceException if there's an error accessing the method + */ + InputStream getEmpty(String method) throws ServiceException; + + /** + * An exception returned by SciGraph. + * + * @version $Id$ + */ + class ServiceException extends Exception + { + /** + * CTOR. + * @param message the message + */ + public ServiceException(String message) + { + super(message); + } + + /** + * CTOR with cause. + * @param message the message + * @param cause the cause. + */ + public ServiceException(String message, Exception cause) + { + super(message, cause); + } + } +} diff --git a/generic-rest/src/main/java/org/phenotips/textanalysis/internal/AnnotationAPIImpl.java b/generic-rest/src/main/java/org/phenotips/textanalysis/internal/AnnotationAPIImpl.java new file mode 100644 index 0000000..c154c99 --- /dev/null +++ b/generic-rest/src/main/java/org/phenotips/textanalysis/internal/AnnotationAPIImpl.java @@ -0,0 +1,135 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see http://www.gnu.org/licenses/ + */ +package org.phenotips.textanalysis.internal; + +import org.xwiki.component.annotation.Component; +import org.xwiki.configuration.ConfigurationSource; + +import java.io.IOException; +import java.io.InputStream; + +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; + +import java.nio.charset.Charset; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import javax.inject.Inject; +import javax.inject.Named; + +import org.apache.http.NameValuePair; +import org.apache.http.client.fluent.Request; +import org.apache.http.entity.ContentType; +import org.apache.http.message.BasicNameValuePair; + + +/** + * Implements interacting with an annotation REST API. + * + * @version $Id$ + */ +@Component +public class AnnotationAPIImpl implements AnnotationAPI +{ + + /** + * The default URL of the annotation service. + */ + private static final String DEFAULT_BASE_URL = "http://localhost:8080/scigraph/scigraph/"; + + /** + * The charset to use when sending requests. + */ + private static final Charset CHARSET = Charset.forName("UTF-8"); + + /** + * The global configuration. + */ + @Inject + @Named("xwikicfg") + private ConfigurationSource configuration; + + @Override + public InputStream postJson(String method, InputStream content) throws ServiceException + { + try { + URI uri = getAbsoluteURI(method); + return Request.Post(uri). + bodyStream(content, ContentType.APPLICATION_JSON). + execute().returnContent().asStream(); + } catch (IOException | URISyntaxException e) { + throw new ServiceException(e.getMessage(), e); + } + } + + @Override + public InputStream postForm(String method, Map<String, String> params) throws ServiceException + { + try { + URI uri = getAbsoluteURI(method); + List<NameValuePair> list = new ArrayList<>(params.size()); + for (Map.Entry<String, String> entry : params.entrySet()) { + NameValuePair pair = new BasicNameValuePair(entry.getKey(), entry.getValue()); + list.add(pair); + } + return Request.Post(uri). + bodyForm(list, CHARSET). + execute().returnContent().asStream(); + } catch (IOException | URISyntaxException e) { + throw new ServiceException(e.getMessage(), e); + } + } + + @Override + public InputStream postEmpty(String method) throws ServiceException + { + try { + URI uri = getAbsoluteURI(method); + return Request.Post(uri).execute().returnContent().asStream(); + } catch (IOException | URISyntaxException e) { + throw new ServiceException(e.getMessage(), e); + } + } + + @Override + public InputStream getEmpty(String method) throws ServiceException + { + try { + URI uri = getAbsoluteURI(method); + return Request.Get(uri).execute().returnContent().asStream(); + } catch (IOException | URISyntaxException e) { + throw new ServiceException(e.getMessage(), e); + } + } + + /** + * Get the uri to access a method. + * @param method the name of the method + * @return the corresponding uri. + */ + private URI getAbsoluteURI(String method) throws URISyntaxException, MalformedURLException { + URL base = new URL(configuration.getProperty("phenotips.textanalysis.internal.serviceURL", DEFAULT_BASE_URL)); + URL absolute = new URL(base, method); + return absolute.toURI(); + } +} diff --git a/generic-rest/src/main/java/org/phenotips/textanalysis/internal/GenericRESTAnnotationService.java b/generic-rest/src/main/java/org/phenotips/textanalysis/internal/GenericRESTAnnotationService.java new file mode 100644 index 0000000..ce0c7bd --- /dev/null +++ b/generic-rest/src/main/java/org/phenotips/textanalysis/internal/GenericRESTAnnotationService.java @@ -0,0 +1,98 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see http://www.gnu.org/licenses/ + */ +package org.phenotips.textanalysis.internal; + +import org.phenotips.textanalysis.TermAnnotation; +import org.phenotips.textanalysis.TermAnnotationService; +import org.phenotips.vocabulary.VocabularyManager; +import org.phenotips.vocabulary.VocabularyTerm; + +import org.xwiki.component.annotation.Component; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Singleton; + +/** + * Implementation of {@link TermAnnotationService} using a generic REST API endpoint. + * To use this with any given RESTful service it must offer a compatible API. At the moment, + * this consists of one method, annotations/entities, that accepts parameters as a URLEncoded form + * (non-ideal but needed for scigraph support). The parameters are: + * + * <pre> + * { + * "content" : "the text to use", + * "includeCat" : "the category that returned ontology elements should belong to" + * } + * </pre> + * + * The service must respond as follows: + * <pre> + * [ + * { + * "start" : "the first position of the annotation", + * "end" : "the last position of the annotation", + * "token" : { + * "id" : "the phenotype's id", + * } + * } + * ] + * </pre> + * + * @version $Id$ + */ +@Component +@Named("genericREST") +@Singleton +public class GenericRESTAnnotationService implements TermAnnotationService +{ + /** + * The ontology used to look up phenotypes. + */ + @Inject + private VocabularyManager vocabularies; + + /** + * The wrapper that will actually interact with the service. + */ + @Inject + private RESTWrapper wrapper; + + @Override + public List<TermAnnotation> annotate(String text) throws AnnotationException + { + List<RESTWrapper.RESTAnnotation> annotations = wrapper.annotate(text); + List<TermAnnotation> retval = new ArrayList<>(annotations.size()); + for (RESTWrapper.RESTAnnotation annotation : annotations) { + String termId = annotation.getToken().getId().replace("hpo:", "").replace("_", ":"); + VocabularyTerm term = this.vocabularies.resolveTerm(termId); + if (term != null) { + long start = annotation.getStart(); + long end = annotation.getEnd(); + retval.add(new TermAnnotation(start, end, term)); + } + } + return retval; + } +} diff --git a/generic-rest/src/main/java/org/phenotips/textanalysis/internal/RESTWrapper.java b/generic-rest/src/main/java/org/phenotips/textanalysis/internal/RESTWrapper.java new file mode 100644 index 0000000..d7508bf --- /dev/null +++ b/generic-rest/src/main/java/org/phenotips/textanalysis/internal/RESTWrapper.java @@ -0,0 +1,154 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see http://www.gnu.org/licenses/ + */ +package org.phenotips.textanalysis.internal; + +import org.phenotips.textanalysis.TermAnnotationService; + +import org.xwiki.component.annotation.Role; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * Wraps annotation initialization and calls, offering a single interface to annotate text. + * TODO: this is probably an unnecessary abstraction that could be merged into GenericRESTAnnotationService + * + * @version $Id$ + */ +@Role +public interface RESTWrapper +{ + + /** + * Annotates the text given. + * @param text the string to annotate + * @return List of annotations + * @throws TermAnnotationService.AnnotationException if something goes wrong + */ + List<RESTAnnotation> annotate(String text) throws TermAnnotationService.AnnotationException; + + /** + * A text annotation as returned by the service in use. + * + * @version $Id$ + */ + @JsonIgnoreProperties(ignoreUnknown = true) + final class RESTAnnotation + { + /** + * The token corresponding to the annotation; in phenotips terms, this is the term. + */ + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Token + { + /** + * The token's id. + */ + private String id; + + /** + * Return the id. + * @return the id + */ + public String getId() + { + return id; + } + + /** + * Set the id. + * @param id the id + */ + public void setId(String id) + { + this.id = id; + } + } + + /** + * The token for this annotation. + */ + private Token token; + + /** + * The end of the annotation. + */ + private int end; + + /** + * The start of the annotation. + */ + private int start; + + /** + * Get the token. + * @return the token + */ + public Token getToken() + { + return token; + } + + /** + * Set the token. + * @param token the token + */ + public void setToken(Token token) + { + this.token = token; + } + + /** + * Get the end of the annotation. + * @return the end + */ + public int getEnd() + { + return end; + } + + /** + * Set the end of the annotation. + * @param end the end + */ + public void setEnd(int end) + { + this.end = end; + } + + /** + * Get the start of the annotation. + * @return the start + */ + public int getStart() + { + return start; + } + + /** + * Set the start of the annotation. + * @param start the start + */ + public void setStart(int start) + { + this.start = start; + } + } +} + diff --git a/generic-rest/src/main/java/org/phenotips/textanalysis/internal/RESTWrapperImpl.java b/generic-rest/src/main/java/org/phenotips/textanalysis/internal/RESTWrapperImpl.java new file mode 100644 index 0000000..0297e14 --- /dev/null +++ b/generic-rest/src/main/java/org/phenotips/textanalysis/internal/RESTWrapperImpl.java @@ -0,0 +1,82 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see http://www.gnu.org/licenses/ + */ +package org.phenotips.textanalysis.internal; + +import org.phenotips.textanalysis.TermAnnotationService; + +import org.xwiki.component.annotation.Component; +import org.xwiki.component.phase.Initializable; +import org.xwiki.component.phase.InitializationException; + +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + + +/** + * Wrapper component implementation for a generic annotation service going through a REST API. + * + * @version $Id$ + */ +@Component +@Singleton +public class RESTWrapperImpl implements RESTWrapper, Initializable +{ + /** + * The category to search in. + */ + private static final String CATEGORY = "abnormality"; + + /** + * The object for API interaction with scigraph. + */ + @Inject + private AnnotationAPI api; + + /** + * The object mapper to use for json parsing. + */ + private ObjectMapper mapper; + + @Override + public void initialize() throws InitializationException { + mapper = new ObjectMapper(); + } + + @Override + public List<RESTAnnotation> annotate(String text) throws TermAnnotationService.AnnotationException { + try { + Map<String, String> params = new HashMap<>(2); + params.put("content", text); + params.put("includeCat", CATEGORY); + InputStream is = api.postForm("annotations/entities", params); + TypeReference reference = new TypeReference<List<RESTAnnotation>>() { }; + return mapper.readValue(is, reference); + } catch (IOException | AnnotationAPI.ServiceException e) { + throw new TermAnnotationService.AnnotationException(e.getMessage(), e); + } + } +} diff --git a/generic-rest/src/main/resources/META-INF/components.txt b/generic-rest/src/main/resources/META-INF/components.txt new file mode 100644 index 0000000..809848d --- /dev/null +++ b/generic-rest/src/main/resources/META-INF/components.txt @@ -0,0 +1,3 @@ +org.phenotips.textanalysis.internal.GenericRESTAnnotationService +org.phenotips.textanalysis.internal.RESTWrapperImpl +org.phenotips.textanalysis.internal.AnnotationAPIImpl diff --git a/generic-rest/src/test/java/org/phenotips/textanalysis/internal/GenericRESTAnnotationServiceTest.java b/generic-rest/src/test/java/org/phenotips/textanalysis/internal/GenericRESTAnnotationServiceTest.java new file mode 100644 index 0000000..7224176 --- /dev/null +++ b/generic-rest/src/test/java/org/phenotips/textanalysis/internal/GenericRESTAnnotationServiceTest.java @@ -0,0 +1,185 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see http://www.gnu.org/licenses/ + */ +package org.phenotips.textanalysis.internal; + +import org.phenotips.textanalysis.TermAnnotation; +import org.phenotips.textanalysis.TermAnnotationService; +import org.phenotips.textanalysis.TermAnnotationService.AnnotationException; +import org.phenotips.vocabulary.VocabularyManager; +import org.phenotips.vocabulary.VocabularyTerm; + +import org.xwiki.component.manager.ComponentLookupException; +import org.xwiki.test.mockito.MockitoComponentMockingRule; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.io.IOException; +import java.io.Reader; +import java.nio.CharBuffer; + +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Matchers; +import org.mockito.ArgumentMatcher; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Matchers.anyBoolean; +import static org.mockito.Matchers.eq; +import static org.mockito.Matchers.argThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class GenericRESTAnnotationServiceTest +{ + private TermAnnotationService client; + + /** + * Mocker for GenericRESTAnnotationService component. + */ + @Rule + public final MockitoComponentMockingRule<TermAnnotationService> mocker = + new MockitoComponentMockingRule<TermAnnotationService>(GenericRESTAnnotationService.class); + + /** + * Test for cases where there's only one annotation in the text. + * + * @throws ComponentLookupException if the mocked component doesn't exist + * @throws AnnotationException if the annotation process failed + * @throws IOException if annotateEntities throws (hopefully never) + */ + @Test + public void testSingleAnnotation() throws AnnotationException, ComponentLookupException, IOException + { + client = this.mocker.getComponentUnderTest(); + String term = "blue eyes"; + String text = "The lady has "; + int start = text.length(); + text += term; + int end = start + term.length(); + String termId = "test id"; + + List<RESTWrapper.RESTAnnotation> result = new LinkedList<RESTWrapper.RESTAnnotation>(); + + /* Mock SciGraph wrapper */ + RESTWrapper wrapper = this.mocker.getInstance(RESTWrapper.class); + RESTWrapper.RESTAnnotation.Token token = new RESTWrapper.RESTAnnotation.Token(); + token.setId(termId); + RESTWrapper.RESTAnnotation annotation = new RESTWrapper.RESTAnnotation(); + annotation.setToken(token); + annotation.setStart(start); + annotation.setEnd(end); + result.add(annotation); + when(wrapper.annotate(text)).thenReturn(result); + + // Mock Ontology Manager + VocabularyManager vocabularyManager = this.mocker.getInstance(VocabularyManager.class); + VocabularyTerm t = mock(VocabularyTerm.class); + when(t.getId()).thenReturn(termId); + when(vocabularyManager.resolveTerm(termId)).thenReturn(t); + + List<TermAnnotation> expected = new LinkedList<TermAnnotation>(); + expected.add(new TermAnnotation(start, end, t)); + + List<TermAnnotation> actual = client.annotate(text); + + assertEquals(expected, actual); + } + + /** + * Test for cases where two terms overlap in the text. + * + * @throws ComponentLookupException if the mocked component doesn't exist + * @throws AnnotationException if the annotation process failed + * @throws IOException if annotateEntities throws (hopefully never) + */ + @Test + public void testOverlappingAnnotations() throws AnnotationException, ComponentLookupException, IOException + { + client = this.mocker.getComponentUnderTest(); + String term1 = "blue eyes"; + String term2 = "eyes"; + String text = "The layd has "; + int start1 = text.length(); + int start2 = text.length() + "blue ".length(); + int end1 = start1 + term1.length(); + int end2 = start2 + term2.length(); + String termId1 = "id1"; + String termId2 = "id2"; + text += term1; + + List<RESTWrapper.RESTAnnotation> result = new LinkedList<RESTWrapper.RESTAnnotation>(); + + RESTWrapper.RESTAnnotation.Token token1 = new RESTWrapper.RESTAnnotation.Token(); + token1.setId(termId1); + RESTWrapper.RESTAnnotation annotation1 = new RESTWrapper.RESTAnnotation(); + annotation1.setToken(token1); + annotation1.setStart(start1); + annotation1.setEnd(end1); + result.add(annotation1); + + RESTWrapper.RESTAnnotation.Token token2 = new RESTWrapper.RESTAnnotation.Token(); + token2.setId(termId2); + RESTWrapper.RESTAnnotation annotation2 = new RESTWrapper.RESTAnnotation(); + annotation2.setToken(token2); + annotation2.setStart(start2); + annotation2.setEnd(end2); + result.add(annotation2); + + /* Mock SciGraph wrapper */ + RESTWrapper wrapper = this.mocker.getInstance(RESTWrapper.class); + when(wrapper.annotate(text)).thenReturn(result); + + /* Mock Ontology wrapper */ + VocabularyManager vocabularyManager = this.mocker.getInstance(VocabularyManager.class); + VocabularyTerm t1 = mock(VocabularyTerm.class); + when(t1.getId()).thenReturn(termId1); + when(vocabularyManager.resolveTerm(termId1)).thenReturn(t1); + VocabularyTerm t2 = mock(VocabularyTerm.class); + when(t2.getId()).thenReturn(termId2); + when(vocabularyManager.resolveTerm(termId2)).thenReturn(t2); + + List<TermAnnotation> expected = new LinkedList<TermAnnotation>(); + expected.add(new TermAnnotation(start1, end1, t1)); + expected.add(new TermAnnotation(start2, end2, t2)); + + List<TermAnnotation> actual = client.annotate(text); + assertEquals(expected, actual); + } + + /** + * Test for cases where we're annotating empty text. + * + * @throws ComponentLookupException if the mocked component doesn't exist + * @throws AnnotationException if the annotation process failed + * @throws IOException if annotateEntities throws (hopefully never) + */ + @Test + public void testAnnotateEmpty() throws AnnotationException, ComponentLookupException, IOException + { + client = this.mocker.getComponentUnderTest(); + String text = ""; + List<RESTWrapper.RESTAnnotation> result = new LinkedList<RESTWrapper.RESTAnnotation>(); + + RESTWrapper wrapper = this.mocker.getInstance(RESTWrapper.class); + when(wrapper.annotate(text)).thenReturn(result); + + List<TermAnnotation> expected = new LinkedList<TermAnnotation>(); + assertEquals(expected, client.annotate(text)); + } +} diff --git a/pom.xml b/pom.xml index 19b637e..6d4174a 100644 --- a/pom.xml +++ b/pom.xml @@ -27,7 +27,6 @@ <relativePath /> </parent> <artifactId>clinical-text-analysis-extension</artifactId> - <version>1.0-SNAPSHOT</version> <packaging>pom</packaging> <name>PhenoTips - Clinical Text Analysis</name> @@ -41,6 +40,8 @@ <module>biolark</module> <module>ui</module> <module>pageobjects</module> + <module>scigraph-service</module> + <module>generic-rest</module> </modules> <profiles> diff --git a/scigraph-service/pom.xml b/scigraph-service/pom.xml new file mode 100644 index 0000000..2453970 --- /dev/null +++ b/scigraph-service/pom.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<!-- + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see http://www.gnu.org/licenses/ +--> + +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.phenotips</groupId> + <artifactId>clinical-text-analysis-extension</artifactId> + <version>1.3-SNAPSHOT</version> + </parent> + <artifactId>scigraph-service</artifactId> + <packaging>pom</packaging> + <name>Scigraph standalone war parent</name> + + <modules> + <module>war</module> + </modules> +</project> + diff --git a/scigraph-service/war/pom.xml b/scigraph-service/war/pom.xml new file mode 100644 index 0000000..4f6c465 --- /dev/null +++ b/scigraph-service/war/pom.xml @@ -0,0 +1,145 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<!-- + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see http://www.gnu.org/licenses/ +--> + +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.phenotips</groupId> + <artifactId>scigraph-service</artifactId> + <version>1.3-SNAPSHOT</version> + </parent> + <artifactId>scigraph-war</artifactId> + <packaging>war</packaging> + <name>Scigraph standalone war</name> + + <dependencies> + <dependency> + <groupId>ch.qos.logback</groupId> + <artifactId>logback-classic</artifactId> + <version>1.1.7</version> + <scope>runtime</scope> + </dependency> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>jcl-over-slf4j</artifactId> + <scope>runtime</scope> + </dependency> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>log4j-over-slf4j</artifactId> + <scope>runtime</scope> + </dependency> + <dependency> + <groupId>io.scigraph</groupId> + <artifactId>scigraph-core</artifactId> + <version>1.5-alpha-1</version> + <type>jar</type> + </dependency> + <dependency> + <groupId>io.scigraph</groupId> + <artifactId>scigraph-entity</artifactId> + <version>1.5-alpha-1</version> + <type>jar</type> + </dependency> + <dependency> + <groupId>io.scigraph</groupId> + <artifactId>scigraph-services</artifactId> + <version>1.5-alpha-1</version> + <type>jar</type> + <exclusions> + <exclusion> + <groupId>javax.ws.rs</groupId> + <artifactId>jsr311-api</artifactId> + </exclusion> + </exclusions> + </dependency> + <dependency> + <groupId>com.fasterxml.jackson.core</groupId> + <artifactId>jackson-annotations</artifactId> + <version>2.5.1</version> + <scope>runtime</scope> + </dependency> + <dependency> + <groupId>com.fasterxml.jackson.core</groupId> + <artifactId>jackson-annotations</artifactId> + <version>2.5.1</version> + <scope>compile</scope> + </dependency> + <dependency> + <groupId>io.dropwizard</groupId> + <artifactId>dropwizard-core</artifactId> + <version>0.8.1</version> + </dependency> + <dependency> + <groupId>io.dropwizard</groupId> + <artifactId>dropwizard-assets</artifactId> + <version>0.8.1</version> + </dependency> + <dependency> + <groupId>be.fluid-it.tools.dropwizard</groupId> + <artifactId>wizard-in-a-box</artifactId> + <version>0.8-1-1</version> + <exclusions> + <exclusion> + <groupId>io.dropwizard</groupId> + <artifactId>*</artifactId> + </exclusion> + <exclusion> + <groupId>com.fasterxml.jackson.*</groupId> + <artifactId>*</artifactId> + </exclusion> + </exclusions> + </dependency> + </dependencies> + + <build> + <finalName>scigraph-war</finalName> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-war-plugin</artifactId> + <configuration> + <failOnMissingWebXml>false</failOnMissingWebXml> + </configuration> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-dependency-plugin</artifactId> + <version>2.3</version> + <executions> + <execution> + <phase>package</phase> + <goals><goal>copy</goal></goals> + <configuration> + <artifactItems> + <artifactItem> + <groupId>org.eclipse.jetty</groupId> + <artifactId>jetty-runner</artifactId> + <version>9.2.9.v20150224</version> + <destFileName>jetty-runner.jar</destFileName> + </artifactItem> + </artifactItems> + </configuration> + </execution> + </executions> + </plugin> + </plugins> + </build> +</project> diff --git a/scigraph-service/war/src/main/java/io/scigraph/annotation/PTEntityProcessor.java b/scigraph-service/war/src/main/java/io/scigraph/annotation/PTEntityProcessor.java new file mode 100644 index 0000000..073ec18 --- /dev/null +++ b/scigraph-service/war/src/main/java/io/scigraph/annotation/PTEntityProcessor.java @@ -0,0 +1,102 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see http://www.gnu.org/licenses/ + */ +package io.scigraph.annotation; + +import java.io.ByteArrayInputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.UnsupportedEncodingException; + +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.regex.Pattern; + +import javax.inject.Inject; + +import org.apache.lucene.analysis.Analyzer; +import org.apache.lucene.analysis.LengthFilter; +import org.apache.lucene.analysis.TokenStream; +import org.apache.lucene.analysis.Tokenizer; +import org.apache.lucene.analysis.WhitespaceTokenizer; + +import io.scigraph.lucene.LuceneUtils; +import io.scigraph.lucene.PatternReplaceFilter; + +/** + * An entity processor for use within Phenotips. + * + * @version $Id$ + */ +public class PTEntityProcessor extends EntityProcessorImpl +{ + + /** + * The character encoding to use. + */ + private static final String ENCODING = "UTF-8"; + + /** + * CTOR. + * @param recognizer the injected recognizer. + */ + @Inject + public PTEntityProcessor(EntityRecognizer recognizer) + { + super(recognizer); + } + + @Override + BlockingQueue<List<Token<String>>> startShingleProducer(String content) + { + BlockingQueue<List<Token<String>>> queue = new LinkedBlockingQueue<List<Token<String>>>(); + Reader r; + try { + r = new InputStreamReader(new ByteArrayInputStream(content.getBytes(ENCODING)), ENCODING); + } catch (UnsupportedEncodingException e) { + /* The encoding is hardcoded, and it's the pretty standard utf-8, so if it's not + * supported that's a problem that should probably be fixed. */ + throw new RuntimeException(e.getMessage(), e); + } + ShingleProducer producer = new PTShingleProducer(new PTAnalyzer(), r, queue); + Thread t = new Thread(producer, "Shingle Producer Thread"); + t.start(); + return queue; + } + + /** + * A lucene analyzer used to tokenize and filter input text. + * + * @version $Id$ + */ + public static class PTAnalyzer extends Analyzer + { + @Override + public TokenStream tokenStream(String fieldName, Reader reader) { + Tokenizer tokenizer = new WhitespaceTokenizer(LuceneUtils.getVersion(), reader); + TokenStream result = new PunctuationFilter(tokenizer); + result = new PatternReplaceFilter(result, + Pattern.compile("^([\\.!\\?,:;\"'\\(\\)]*)(.*?)([\\.!\\?,:;\"'\\(\\)]*)$"), "$2", true); + result = new PatternReplaceFilter(result, Pattern.compile("'s"), "s", true); + /* Makes no sense to have lone symbols hanging around, and they'll trip up the lucene parser. */ + result = new PatternReplaceFilter(result, Pattern.compile("^\\W$"), "", true); + result = new LengthFilter(false, result, 1, Integer.MAX_VALUE); + return result; + } + } +} diff --git a/scigraph-service/war/src/main/java/io/scigraph/annotation/PTShingleProducer.java b/scigraph-service/war/src/main/java/io/scigraph/annotation/PTShingleProducer.java new file mode 100644 index 0000000..7ed178e --- /dev/null +++ b/scigraph-service/war/src/main/java/io/scigraph/annotation/PTShingleProducer.java @@ -0,0 +1,158 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see http://www.gnu.org/licenses/ + */ +package io.scigraph.annotation; + +import java.io.IOException; +import java.io.Reader; +import java.util.Deque; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.annotation.concurrent.NotThreadSafe; + +import org.apache.lucene.analysis.Analyzer; +import org.apache.lucene.analysis.TokenStream; +import org.apache.lucene.analysis.tokenattributes.CharTermAttribute; +import org.apache.lucene.analysis.tokenattributes.FlagsAttribute; +import org.apache.lucene.analysis.tokenattributes.OffsetAttribute; + + +/** + * Splits text into shingles, with no shingle containing more than one grammatical clause + * (ie they stop at punctuation). + * + * @version $Id$ + */ +@NotThreadSafe +public class PTShingleProducer extends ShingleProducer +{ + /** + * Our logger. + */ + private static final Logger LOGGER = Logger.getLogger(ShingleProducer.class.getName()); + + /** + * The analyzer. + */ + private Analyzer analyzer; + + /** + * The reader containing the text. + */ + private Reader reader; + + /** + * The maximum length of a shingle. + */ + private int shingleCount; + + /** + * The blocking queue to use to feed back to the annotating thread. + */ + private final BlockingQueue<List<Token<String>>> queue; + + /** + * CTOR. + * @param analyzer the analyzer to use. + * @param reader the reader containing text. + * @param queue the queue to use to feed back to the annotating thread. + */ + public PTShingleProducer(Analyzer analyzer, Reader reader, BlockingQueue<List<Token<String>>> queue) + { + this(analyzer, reader, queue, DEFAULT_SHINGLE_COUNT); + } + + /** + * CTOR. + * @param analyzer the analyzer to use. + * @param reader the reader containing text. + * @param queue the queue to use to feed back to the annotating thread. + * @param shingleCount the maximum length of any given shingle. + */ + public PTShingleProducer(Analyzer analyzer, Reader reader, BlockingQueue<List<Token<String>>> queue, + int shingleCount) + { + super(analyzer, reader, queue, shingleCount); + this.analyzer = analyzer; + this.reader = reader; + this.shingleCount = shingleCount; + this.queue = queue; + } + + /** + * Exhaust the TokenStream given, placing it in the buffer as we go along. The buffer will be periodically + * emptied into our queue over time, but it will not be emptied after the stream is exhausted; in other + * words, the buffer may come back with some elements still inside it. + * @param stream the tokenstream to read + * @param offset the offset attribute of the stream + * @param term the term attribute of the stream + * @param flags the flags attribute of the stream + * @param buffer the buffer to put tokens into + * @throws IOException if the tokenstream throws + * @throws InterruptedException if insertion into the shared queue fails + */ + private void exhaustStream(TokenStream stream, OffsetAttribute offset, CharTermAttribute term, + FlagsAttribute flags, Deque<Token<String>> buffer) + throws IOException, InterruptedException + { + boolean punctuation = false; + while (punctuation || stream.incrementToken()) { + if (!punctuation) { + Token<String> token = new Token<String>(term.toString(), offset.startOffset(), + offset.endOffset()); + buffer.offer(token); + punctuation = PunctuationFilter.isPunctuationSet(flags.getFlags()); + if (!punctuation && buffer.size() < shingleCount) { + // Fill the buffer first, before offering anything to the queue + continue; + } + } + addBufferToQueue(buffer); + if (punctuation || shingleCount == buffer.size()) { + buffer.pop(); + } + if (punctuation && buffer.isEmpty()) { + punctuation = false; + } + } + } + + @Override + public void run() { + Deque<Token<String>> buffer = new LinkedList<>(); + try { + TokenStream stream = analyzer.tokenStream("", reader); + OffsetAttribute offset = stream.getAttribute(OffsetAttribute.class); + CharTermAttribute term = stream.getAttribute(CharTermAttribute.class); + FlagsAttribute flags = stream.getAttribute(FlagsAttribute.class); + exhaustStream(stream, offset, term, flags, buffer); + while (!buffer.isEmpty()) { + addBufferToQueue(buffer); + buffer.pop(); + } + queue.put(END_TOKEN); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Failed to produce shingle", e); + } + } +} diff --git a/scigraph-service/war/src/main/java/io/scigraph/annotation/PunctuationFilter.java b/scigraph-service/war/src/main/java/io/scigraph/annotation/PunctuationFilter.java new file mode 100644 index 0000000..e8e3e74 --- /dev/null +++ b/scigraph-service/war/src/main/java/io/scigraph/annotation/PunctuationFilter.java @@ -0,0 +1,94 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see http://www.gnu.org/licenses/ + */ +package io.scigraph.annotation; + +import java.io.IOException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.lucene.analysis.TokenFilter; +import org.apache.lucene.analysis.TokenStream; +import org.apache.lucene.analysis.tokenattributes.CharTermAttribute; +import org.apache.lucene.analysis.tokenattributes.FlagsAttribute; + +/** + * Flags any words that end with a punctuation mark. + * + * @version $Id$ + */ +public final class PunctuationFilter extends TokenFilter +{ + /** + * The flag set when a word ends with punctuation. + */ + public static final int PUNCTUATION_FLAG = 0x1; + + /** + * The current term. + */ + private final CharTermAttribute termAtt = addAttribute(CharTermAttribute.class); + + /** + * The current flags attribute. + */ + private final FlagsAttribute flattribute = addAttribute(FlagsAttribute.class); + + /** + * The pattern we use to figure out if there's punctuation. + */ + private final Pattern pattern = Pattern.compile("^(.*?)([\\.!\\?,:;\"'\\(\\)]+)$"); + + /** + * Our string matcher. + */ + private final Matcher m; + + /** + * CTOR. + * @param in the input token stream. + */ + public PunctuationFilter(TokenStream in) + { + super(in); + m = pattern.matcher(termAtt); + } + + /** + * Return whether the flags given has the punctuation bit set. + * @param flags the flags to look at + * @return whether the punctuation flag is set + */ + public static boolean isPunctuationSet(int flags) + { + return (PUNCTUATION_FLAG & flags) == PUNCTUATION_FLAG; + } + + @Override + public boolean incrementToken() throws IOException + { + if (!input.incrementToken()) { + return false; + } + m.reset(); + if (m.find()) { + int flags = flattribute.getFlags(); + flattribute.setFlags(flags | PUNCTUATION_FLAG); + } + return true; + } +} diff --git a/scigraph-service/war/src/main/java/io/scigraph/services/PTSciGraphApplication.java b/scigraph-service/war/src/main/java/io/scigraph/services/PTSciGraphApplication.java new file mode 100644 index 0000000..97f859a --- /dev/null +++ b/scigraph-service/war/src/main/java/io/scigraph/services/PTSciGraphApplication.java @@ -0,0 +1,117 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see http://www.gnu.org/licenses/ + */ +/* We need access to some protected stuff in the MainApplication, so there's no + * choice but to place this in this package. No biggie, since we're already dumping + * services in that package anyway. */ +package io.scigraph.services; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutionException; + +import org.apache.commons.io.FileUtils; + +import io.dropwizard.assets.AssetsBundle; +import io.dropwizard.setup.Bootstrap; +import io.dropwizard.views.ViewBundle; +import io.scigraph.owlapi.loader.BatchOwlLoader; +import io.scigraph.owlapi.loader.OwlLoadConfiguration; +import io.scigraph.owlapi.loader.OwlLoadConfigurationLoader; +import io.scigraph.services.configuration.ApplicationConfiguration; + +import ru.vyarus.dropwizard.guice.GuiceBundle; + +/** + * A Scigraph application for use with phenotips. + * + * @version $Id$ + */ +public class PTSciGraphApplication extends MainApplication +{ + /** + * The load config object. + */ + private OwlLoadConfiguration config; + + /** + * Construct a new PTSciGraphApplication. + * @param loadConfig the location of the load configuration. + */ + public PTSciGraphApplication(String loadConfig) + { + try { + OwlLoadConfigurationLoader owlLoadConfigurationLoader = + new OwlLoadConfigurationLoader(new File(loadConfig)); + config = owlLoadConfigurationLoader.loadConfig(); + } catch (IOException e) { + throw new RuntimeException(e.getMessage(), e); + } + } + + /** + * Load the HPO into a scigraph. + */ + private void loadOntology() + { + File graph = new File(config.getGraphConfiguration().getLocation()); + /* There's no other way to reindex, so unfortunately we will have to delete everything every time */ + FileUtils.deleteQuietly(graph); + try { + /* In theory SciGraph can download the url on its own. In theory. + * In practice, it tends to crash about half the time, because it streams the + * file and hits EOF while reading... So we have to hack around this by + * downloading the thing ourselves. + */ + URL url = new URL(config.getOntologies().get(0).url()); + Path temp = Files.createTempFile("hpoLoad", "owl"); + FileUtils.copyURLToFile(url, temp.toFile()); + config.getOntologies().get(0).setUrl(temp.toString()); + BatchOwlLoader.load(config); + } catch (ExecutionException | InterruptedException | IOException e) { + /* TODO is this the best way to deal with this? */ + throw new RuntimeException(e.getMessage(), e); + } + } + + @Override + public void initialize(Bootstrap<ApplicationConfiguration> bootstrap) + { + loadOntology(); + /* Sadly, sadly, there's no way to remove a bundle from bootstrap, so we can't just + * super.initialize here, as we'd like. Instead we copy paste (!) the code, just to + * replace the SciGraphApplicationModule down there. + */ + bootstrap.addBundle(new AssetsBundle("/swagger/", "/docs", "index.html")); + bootstrap.addBundle(new ViewBundle<ApplicationConfiguration>() { + @Override + public Map<String, Map<String, String>> getViewConfiguration( + ApplicationConfiguration configuration) { + return new HashMap<>(); + } + }); + bootstrap.addBundle(GuiceBundle.builder() + .enableAutoConfig("io.scigraph.services") + .injectorFactory(factory).modules(new PTSciGraphModule(new SciGraphApplicationModule())) + .build()); + } +} diff --git a/scigraph-service/war/src/main/java/io/scigraph/services/PTSciGraphModule.java b/scigraph-service/war/src/main/java/io/scigraph/services/PTSciGraphModule.java new file mode 100644 index 0000000..13302b4 --- /dev/null +++ b/scigraph-service/war/src/main/java/io/scigraph/services/PTSciGraphModule.java @@ -0,0 +1,82 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see http://www.gnu.org/licenses/ + */ +package io.scigraph.services; + +import com.google.inject.AbstractModule; +import com.google.inject.util.Modules; + +import io.scigraph.annotation.EntityProcessor; +import io.scigraph.annotation.PTEntityProcessor; +import io.scigraph.services.configuration.ApplicationConfiguration; +import io.scigraph.vocabulary.PTVocabularyImpl; +import io.scigraph.vocabulary.Vocabulary; + +import ru.vyarus.dropwizard.guice.module.support.ConfigurationAwareModule; + +/** + * A guice module for scigraph usage within phenotips. + * + * @version $Id$ + */ +public class PTSciGraphModule extends AbstractModule implements ConfigurationAwareModule<ApplicationConfiguration> +{ + /* Hackage time. We need to override certain modules, but we also need to allow the SciGraphApplication module + * to be configured. So we can't just use Modules.override and be done with it. Instead, we have to wrap + * the SciGraphApplicationModule instance entirely, setting its configuration when requested. Then when + * configuring this module, we do the override. Has to be an inner module or we'd recurse on configure() + * forever. + */ + /** + * The wrapped module. + */ + private SciGraphApplicationModule wrapped; + + /** + * Construct a new PTSciGraphModule wrapping the scigraph application module given. + * @param wrapped the wrapped module. + */ + public PTSciGraphModule(SciGraphApplicationModule wrapped) + { + this.wrapped = wrapped; + } + + @Override + public void configure() + { + install(Modules.override(wrapped).with(new InnerPTSGModule())); + } + + @Override + public void setConfiguration(ApplicationConfiguration config) + { + wrapped.setConfiguration(config); + } + + /** + * The module that actually does bindings to phenotips classes. + */ + public static final class InnerPTSGModule extends AbstractModule + { + @Override + public void configure() + { + bind(Vocabulary.class).to(PTVocabularyImpl.class); + bind(EntityProcessor.class).to(PTEntityProcessor.class); + } + } +} diff --git a/scigraph-service/war/src/main/java/io/scigraph/vocabulary/PTVocabularyImpl.java b/scigraph-service/war/src/main/java/io/scigraph/vocabulary/PTVocabularyImpl.java new file mode 100644 index 0000000..c821e2c --- /dev/null +++ b/scigraph-service/war/src/main/java/io/scigraph/vocabulary/PTVocabularyImpl.java @@ -0,0 +1,221 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see http://www.gnu.org/licenses/ + */ +package io.scigraph.vocabulary; + +import java.io.IOException; + +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.annotation.Nullable; + +import javax.inject.Inject; + +import org.apache.commons.lang3.StringUtils; +import org.apache.lucene.index.Term; +import org.apache.lucene.queryParser.ParseException; +import org.apache.lucene.queryParser.QueryParser; +import org.apache.lucene.search.BooleanClause.Occur; +import org.apache.lucene.search.BooleanQuery; +import org.apache.lucene.search.FuzzyQuery; +import org.apache.lucene.search.spans.SpanMultiTermQueryWrapper; +import org.apache.lucene.search.spans.SpanNearQuery; +import org.apache.lucene.search.spans.SpanQuery; + +import org.neo4j.graphdb.GraphDatabaseService; +import org.neo4j.graphdb.Node; +import org.neo4j.graphdb.Transaction; +import org.neo4j.graphdb.index.IndexHits; + +import io.scigraph.frames.Concept; +import io.scigraph.frames.NodeProperties; +import io.scigraph.lucene.LuceneUtils; +import io.scigraph.neo4j.NodeTransformer; +import io.scigraph.neo4j.bindings.IndicatesNeo4jGraphLocation; +import io.scigraph.owlapi.curies.CurieUtil; + +/** + * A scigraph vocabulary for use with phenotips. + * + * @version $Id$ + */ +public class PTVocabularyImpl extends VocabularyNeo4jImpl +{ + /** + * The similarity score used when searching. + */ + public static final float SIMILARITY = 0.7f; + + /** + * The length of the prefix that must match when searching. + */ + public static final int FUZZY_PREFIX = 1; + + /** + * Our logger. + */ + private static final Logger LOGGER = Logger.getLogger(PTVocabularyImpl.class.getName()); + + /** + * The neo4j graph we'll be querying. + */ + private GraphDatabaseService graph; + + /** + * A transformer to turn nodes into concepts. + */ + private NodeTransformer transformer; + + /** + * CTOR. To be used by injectors. + * @param graph the injected graph database service + * @param neo4jLocation the location of neo4j + * @param curieUtil the util to resolve curies + * @param transformer the transformer to turn nodes into concepts. + * @throws IOException if the parent throws + */ + @Inject + public PTVocabularyImpl(GraphDatabaseService graph, @Nullable @IndicatesNeo4jGraphLocation String neo4jLocation, + CurieUtil curieUtil, NodeTransformer transformer) throws IOException { + super(graph, neo4jLocation, curieUtil, transformer); + this.graph = graph; + this.transformer = transformer; + } + + /** + * Create a fuzzy phrase query for the text and field given. + * @param text the text to query + * @param field the field the text should appear in + * @param fuzzy the fuzzy factor + * @param boost the boost for the query + */ + private SpanNearQuery createSpanQuery(String text, String field, float fuzzy, float boost) + { + String[] parts = ("^ " + text + " $").split(" "); + SpanQuery[] clauses = new SpanQuery[parts.length]; + for (int i = 0; i < parts.length; i++) { + /* There's a limit of 5 terms, which we set just to speed everything along. */ + clauses[i] = new SpanMultiTermQueryWrapper<FuzzyQuery>( + new FuzzyQuery(new Term(field, parts[i]), fuzzy, FUZZY_PREFIX, 5)); + } + /* Slop of 0 and inOrder of true basically turns this into a lucene phrase query */ + SpanNearQuery q = new SpanNearQuery(clauses, 0, true); + q.setBoost(boost); + return q; + } + + /** + * Add a label:text match to the boolean query given, matching exactly, fuzzily, and with + * a full-text search, using the boosts given. + * If any of the boosts are 0, that subquery will not be added. + * @param parser the parser that's being used + * @param target the query to add to. + * @param field the field to query on + * @param text the text to match + * @param exactBoost the boost for an exact match + * @param fuzzyBoost the boost for a fuzzy match + * @param fullTextBoost the boost for a full-text search match + * @throws ParseException if badly written... + */ + private void addToQuery(QueryParser parser, BooleanQuery target, String field, String text, + float exactBoost, float fuzzyBoost, float fullTextBoost) throws ParseException + { + String exactQuery = String.format("\"\\^ %s $\"", text); + String prefix = field + ":"; + if (exactBoost > 0) { + target.add(LuceneUtils.getBoostedQuery(parser, prefix + exactQuery, exactBoost), Occur.SHOULD); + } + if (fuzzyBoost > 0) { + target.add(createSpanQuery(text, field, SIMILARITY, fuzzyBoost), Occur.SHOULD); + } + if (fullTextBoost > 0) { + String query = prefix + StringUtils.join(text.split(" "), " " + prefix); + target.add(LuceneUtils.getBoostedQuery(parser, query, fullTextBoost), Occur.SHOULD); + } + } + + /* We don't wanna override limitHits since other methods should still work the way they do, so + * just call it filter hits. + */ + /** + * Filter the hit list given and return the concepts that should be sent back to the user. + * @param hits the hits + * @return the list of concepts to return. + */ + private List<Concept> filterHits(IndexHits<Node> hits) + { + /* We're gonna increase this as we go along, so if a phrase returns very few results, it'll return + * what it has, but if there's tons and tons of results we'll only return good stuff. + * This really only works because the hits come back sorted by order of relevance. + */ + float threshold = 0.11f; + int count = 2; + List<Concept> result = new ArrayList<Concept>(); + for (Node n : hits) { + float score = hits.currentScore(); + Concept c = transformer.apply(n); + if (score >= threshold) { + result.add(c); + threshold += (0.8 / (count)); + count *= 2; + } + } + return result; + } + + @Override + public List<Concept> getConceptsFromTerm(Query query) + { + /* This stuff is a translated-ish version of the field boosting you can find in + * org.phenotips.solr.internal.HumanPhenotypeOntology + * Because we only use scigraph for text annotation, we ignore loads and loads of parameters + * from the query passed in, such as isIncludeSynonyms/Abbreviations/Acronyms, depcreation of + * concepts, and the limit. */ + BooleanQuery finalQuery = new BooleanQuery(); + try { + String text = query.getInput().toLowerCase(); + QueryParser parser = getQueryParser(); + /* The boost for full-text (i.e. non-phrase, non-exact) matching will depend on the length + * of the phrase fed in. This way, we try to limit irrelevant matches (for instance so that + * "slow" in "slow growth" doesn't match something like "slow onset"). Similarly, we'll + * significantly punish the boost if we should be dealing with a single word, simply because + * we could be dealing with descriptive words such as "abnormality" or "syndrome" that we + * don't want popping up. */ + float wordCountScore = text.split(" ").length == 1 ? 0.5f : 1.4f; + float textBoost; + + textBoost = Math.min((text.length() - 1) * wordCountScore, 20.0f); + addToQuery(parser, finalQuery, NodeProperties.LABEL, text, 100.0f, 36.0f, textBoost); + + textBoost = Math.min((text.length() - 2) * (wordCountScore - 0.2f), 15.0f); + addToQuery(parser, finalQuery, Concept.SYNONYM, text, 70.0f, 25.0f, textBoost); + + addToQuery(parser, finalQuery, "comment", text, 0.0f, 5.0f, 3.0f); + } catch (ParseException e) { + LOGGER.log(Level.WARNING, "Failed to parse query", e); + } + try (Transaction tx = graph.beginTx()) { + IndexHits<Node> hits = graph.index().getNodeAutoIndexer().getAutoIndex().query(finalQuery); + List<Concept> result = filterHits(hits); + tx.success(); + return result; + } + } +} diff --git a/scigraph-service/war/src/main/java/org/phenotips/scigraphwar/ScigraphWebApp.java b/scigraph-service/war/src/main/java/org/phenotips/scigraphwar/ScigraphWebApp.java new file mode 100644 index 0000000..d997173 --- /dev/null +++ b/scigraph-service/war/src/main/java/org/phenotips/scigraphwar/ScigraphWebApp.java @@ -0,0 +1,48 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see http://www.gnu.org/licenses/ + */ +package org.phenotips.scigraphwar; + +import javax.servlet.annotation.WebListener; + +import be.fluid_it.tools.dropwizard.box.WebApplication; + +import io.scigraph.services.PTSciGraphApplication; +import io.scigraph.services.configuration.ApplicationConfiguration; + + +/** + * The webapp. + * + * @version $Id$ + */ +@WebListener +public class ScigraphWebApp extends WebApplication<ApplicationConfiguration> +{ + private static String loadConfig; + + static { + loadConfig = ScigraphWebApp.class.getClassLoader().getResource("load.yaml").getPath(); + } + /** + * CTOR. + */ + public ScigraphWebApp() + { + super(new PTSciGraphApplication(loadConfig), "server.yaml"); + } +} diff --git a/scigraph-service/war/src/main/resources/load.yaml b/scigraph-service/war/src/main/resources/load.yaml new file mode 100644 index 0000000..1779039 --- /dev/null +++ b/scigraph-service/war/src/main/resources/load.yaml @@ -0,0 +1,48 @@ +graphConfiguration: + location: ./hpo + indexedNodeProperties: + - category + - label + - synonym + - comment + exactNodeProperties: + - label + - synonym + curies: + rdfs: 'http://www.w3.org/2000/01/rdf-schema#' + hpo: 'http://purl.obolibrary.org/obo/' + oboInOwl: 'http://www.geneontology.org/formats/oboInOwl#' + + # Set Neo4j configuration options + neo4jConfig: + dump_configuration : true + dbms.pagecache.memory : 1G + +# The number of threads dedicated to reading ontologies +producerThreadCount: 4 + +# The number of threads dedicated to processing ontology axioms +consumerThreadCount: 4 + +ontologies: + - url: https://compbio.charite.de/jenkins/job/hpo/lastStableBuild/artifact/hp/hp.owl + +categories: + http://purl.obolibrary.org/obo/HP_0000001 : phenotype + http://purl.obolibrary.org/obo/HP_0000118 : abnormality + +mappedProperties: + - name: label + properties: + - rdfs:label + - name: comment + properties: + - rdfs:comment + - hpo:IAO_0000115 + - hpo:HP_0040005 + - name: synonym + properties: + - oboInOwl:hasExactSynonym + - oboInOwl:hasBroadSynonym + - oboInOwl:hasRelatedSynonym + - oboInOwl:hasNarrowSynonym diff --git a/scigraph-service/war/src/main/resources/server.yaml b/scigraph-service/war/src/main/resources/server.yaml new file mode 100644 index 0000000..1ea9625 --- /dev/null +++ b/scigraph-service/war/src/main/resources/server.yaml @@ -0,0 +1,34 @@ +server: + type: bridge + applicationContextPath: /scigraph + adminContextPath: /admin + +logging: + level: INFO + +graphConfiguration: + location: ./hpo + curies: + hpo: 'http://purl.obolibrary.org/obo/' + oboInOwl: 'http://www.geneontology.org/formats/oboInOwl#' + indexedNodeProperties: + - category + - label + - fragment + - synonym + - comment + exactNodeProperties: + - label + - synonym + +serviceMetadata: + name: 'HPO' + view: { + url: 'http://localhost:9000/scigraph/refine/view/{{id}}' + } + preview: { + url: 'http://localhost:9000/scigraph/refine/preview/{{id}}', + width: 400, + height: 400 + } + diff --git a/scigraph-service/war/src/main/webapp/WEB-INF/web.xml b/scigraph-service/war/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 0000000..e0aa335 --- /dev/null +++ b/scigraph-service/war/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<!-- + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see http://www.gnu.org/licenses/ +--> + +<web-app xmlns="http://java.sun.com/xml/ns/javaee" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" + version="3.0"> + + + <listener> + <listener-class>org.phenotips.scigraphwar.ScigraphWebApp</listener-class> + </listener> + +</web-app> + diff --git a/ui/pom.xml b/ui/pom.xml index 2ad8a29..f347b6f 100644 --- a/ui/pom.xml +++ b/ui/pom.xml @@ -23,7 +23,7 @@ <parent> <groupId>org.phenotips</groupId> <artifactId>clinical-text-analysis-extension</artifactId> - <version>1.0-SNAPSHOT</version> + <version>1.3-SNAPSHOT</version> </parent> <artifactId>clinical-text-analysis-extension-ui</artifactId> <name>PhenoTips - Clinical Text Analysis - User Interface Components</name> @@ -38,10 +38,10 @@ <dependencies> <dependency> - <groupId>${project.groupId}</groupId> - <artifactId>clinical-text-analysis-extension-biolark</artifactId> - <version>${project.version}</version> - <scope>runtime</scope> + <groupId>${project.groupId}</groupId> + <artifactId>clinical-text-analysis-extension-generic-rest</artifactId> + <version>${project.version}</version> + <scope>runtime</scope> </dependency> </dependencies> From 9877e6a30c3f1d7143dff82f3d212d0ac8691fe8 Mon Sep 17 00:00:00 2001 From: Joe C <escozzia@gmail.com> Date: Sun, 12 Jun 2016 15:29:37 -0400 Subject: [PATCH 06/14] [misc] Had to re-release scigraph to get scigraph-services. --- scigraph-service/war/pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scigraph-service/war/pom.xml b/scigraph-service/war/pom.xml index 4f6c465..94484c6 100644 --- a/scigraph-service/war/pom.xml +++ b/scigraph-service/war/pom.xml @@ -49,19 +49,19 @@ <dependency> <groupId>io.scigraph</groupId> <artifactId>scigraph-core</artifactId> - <version>1.5-alpha-1</version> + <version>1.5-alpha-2</version> <type>jar</type> </dependency> <dependency> <groupId>io.scigraph</groupId> <artifactId>scigraph-entity</artifactId> - <version>1.5-alpha-1</version> + <version>1.5-alpha-2</version> <type>jar</type> </dependency> <dependency> <groupId>io.scigraph</groupId> <artifactId>scigraph-services</artifactId> - <version>1.5-alpha-1</version> + <version>1.5-alpha-2</version> <type>jar</type> <exclusions> <exclusion> From 76da891b1d6bd2df556f3319f85c77706f6e1a94 Mon Sep 17 00:00:00 2001 From: Joe C <escozzia@gmail.com> Date: Tue, 14 Jun 2016 10:58:37 -0400 Subject: [PATCH 07/14] [misc] Remove some imports --- .../textanalysis/internal/GenericRESTAnnotationService.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/generic-rest/src/main/java/org/phenotips/textanalysis/internal/GenericRESTAnnotationService.java b/generic-rest/src/main/java/org/phenotips/textanalysis/internal/GenericRESTAnnotationService.java index ce0c7bd..7f25aa3 100644 --- a/generic-rest/src/main/java/org/phenotips/textanalysis/internal/GenericRESTAnnotationService.java +++ b/generic-rest/src/main/java/org/phenotips/textanalysis/internal/GenericRESTAnnotationService.java @@ -25,10 +25,7 @@ import org.xwiki.component.annotation.Component; import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashSet; import java.util.List; -import java.util.Set; import javax.inject.Inject; import javax.inject.Named; From c26272574d1158b92115d310e6cfa21824e6d460 Mon Sep 17 00:00:00 2001 From: Sergiu Dumitriu <sergiu@phenotips.org> Date: Tue, 14 Jun 2016 13:22:09 -0400 Subject: [PATCH 08/14] [misc] Reset version to 1.0-SNAPSHOT --- api/pom.xml | 2 +- generic-rest/pom.xml | 2 +- pom.xml | 2 +- scigraph-service/pom.xml | 2 +- scigraph-service/war/pom.xml | 2 +- ui/pom.xml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/api/pom.xml b/api/pom.xml index 5d8b877..ece8803 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -23,7 +23,7 @@ <parent> <groupId>org.phenotips</groupId> <artifactId>clinical-text-analysis-extension</artifactId> - <version>1.3-SNAPSHOT</version> + <version>1.0-SNAPSHOT</version> </parent> <artifactId>clinical-text-analysis-extension-api</artifactId> <name>PhenoTips - Clinical Text Analysis - Java API</name> diff --git a/generic-rest/pom.xml b/generic-rest/pom.xml index 1d34adc..cde9e5b 100644 --- a/generic-rest/pom.xml +++ b/generic-rest/pom.xml @@ -23,7 +23,7 @@ <parent> <groupId>org.phenotips</groupId> <artifactId>clinical-text-analysis-extension</artifactId> - <version>1.3-SNAPSHOT</version> + <version>1.0-SNAPSHOT</version> </parent> <artifactId>clinical-text-analysis-extension-generic-rest</artifactId> <name>PhenoTips - Clinical Text Analysis - Generic REST implementation for the Java APIs</name> diff --git a/pom.xml b/pom.xml index 6d4174a..f016641 100644 --- a/pom.xml +++ b/pom.xml @@ -23,7 +23,7 @@ <parent> <groupId>org.phenotips</groupId> <artifactId>phenotips-components</artifactId> - <version>1.3-SNAPSHOT</version> + <version>1.0-SNAPSHOT</version> <relativePath /> </parent> <artifactId>clinical-text-analysis-extension</artifactId> diff --git a/scigraph-service/pom.xml b/scigraph-service/pom.xml index 2453970..781ebb4 100644 --- a/scigraph-service/pom.xml +++ b/scigraph-service/pom.xml @@ -23,7 +23,7 @@ <parent> <groupId>org.phenotips</groupId> <artifactId>clinical-text-analysis-extension</artifactId> - <version>1.3-SNAPSHOT</version> + <version>1.0-SNAPSHOT</version> </parent> <artifactId>scigraph-service</artifactId> <packaging>pom</packaging> diff --git a/scigraph-service/war/pom.xml b/scigraph-service/war/pom.xml index 94484c6..e146f69 100644 --- a/scigraph-service/war/pom.xml +++ b/scigraph-service/war/pom.xml @@ -23,7 +23,7 @@ <parent> <groupId>org.phenotips</groupId> <artifactId>scigraph-service</artifactId> - <version>1.3-SNAPSHOT</version> + <version>1.0-SNAPSHOT</version> </parent> <artifactId>scigraph-war</artifactId> <packaging>war</packaging> diff --git a/ui/pom.xml b/ui/pom.xml index f347b6f..6b372dd 100644 --- a/ui/pom.xml +++ b/ui/pom.xml @@ -23,7 +23,7 @@ <parent> <groupId>org.phenotips</groupId> <artifactId>clinical-text-analysis-extension</artifactId> - <version>1.3-SNAPSHOT</version> + <version>1.0-SNAPSHOT</version> </parent> <artifactId>clinical-text-analysis-extension-ui</artifactId> <name>PhenoTips - Clinical Text Analysis - User Interface Components</name> From 436eb0619a6a7673e03636e2489ce39e84afe90d Mon Sep 17 00:00:00 2001 From: Sergiu Dumitriu <sergiu@phenotips.org> Date: Tue, 14 Jun 2016 13:23:31 -0400 Subject: [PATCH 09/14] [misc] Use 1.3-SNAPSHOT for upstream --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index f016641..6d4174a 100644 --- a/pom.xml +++ b/pom.xml @@ -23,7 +23,7 @@ <parent> <groupId>org.phenotips</groupId> <artifactId>phenotips-components</artifactId> - <version>1.0-SNAPSHOT</version> + <version>1.3-SNAPSHOT</version> <relativePath /> </parent> <artifactId>clinical-text-analysis-extension</artifactId> From 5fcc6e1eb94cf816e8c0438a972f2f4396a097ee Mon Sep 17 00:00:00 2001 From: Sergiu Dumitriu <sergiu@phenotips.org> Date: Tue, 14 Jun 2016 13:47:04 -0400 Subject: [PATCH 10/14] [cleanup] Removed unused dependencies --- api/pom.xml | 53 ----------------------------------- biolark/pom.xml | 4 --- generic-rest/pom.xml | 67 -------------------------------------------- 3 files changed, 124 deletions(-) diff --git a/api/pom.xml b/api/pom.xml index ece8803..dbe1eb1 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -28,21 +28,6 @@ <artifactId>clinical-text-analysis-extension-api</artifactId> <name>PhenoTips - Clinical Text Analysis - Java API</name> <dependencies> - <dependency> - <groupId>${project.groupId}</groupId> - <artifactId>patient-access-rules-api</artifactId> - <version>${phenotips.version}</version> - </dependency> - <dependency> - <groupId>${project.groupId}</groupId> - <artifactId>component-registry</artifactId> - <version>${phenotips.version}</version> - </dependency> - <dependency> - <groupId>${project.groupId}</groupId> - <artifactId>patient-data-api</artifactId> - <version>${phenotips.version}</version> - </dependency> <dependency> <groupId>${project.groupId}</groupId> <artifactId>vocabularies-api</artifactId> @@ -53,49 +38,11 @@ <artifactId>xwiki-commons-component-api</artifactId> <version>${xwiki.version}</version> </dependency> - <dependency> - <groupId>org.xwiki.commons</groupId> - <artifactId>xwiki-commons-observation-api</artifactId> - <version>${xwiki.version}</version> - </dependency> <dependency> <groupId>org.xwiki.commons</groupId> <artifactId>xwiki-commons-script</artifactId> <version>${xwiki.version}</version> </dependency> - <dependency> - <groupId>org.apache.commons</groupId> - <artifactId>commons-lang3</artifactId> - </dependency> - <dependency> - <groupId>org.xwiki.platform</groupId> - <artifactId>xwiki-platform-mailsender</artifactId> - <version>${xwiki.version}</version> - </dependency> - <dependency> - <groupId>org.xwiki.platform</groupId> - <artifactId>xwiki-platform-model</artifactId> - <version>${xwiki.version}</version> - </dependency> - <dependency> - <groupId>org.xwiki.platform</groupId> - <artifactId>xwiki-platform-oldcore</artifactId> - <version>${xwiki.version}</version> - </dependency> - <dependency> - <groupId>org.hibernate</groupId> - <artifactId>hibernate-core</artifactId> - <version>3.6.9.Final</version> - </dependency> - <dependency> - <groupId>org.hibernate.javax.persistence</groupId> - <artifactId>hibernate-jpa-2.0-api</artifactId> - <version>1.0.1.Final</version> - </dependency> - <dependency> - <groupId>org.slf4j</groupId> - <artifactId>slf4j-api</artifactId> - </dependency> <!-- Test dependencies --> <dependency> <groupId>org.xwiki.commons</groupId> diff --git a/biolark/pom.xml b/biolark/pom.xml index c27d02e..b44a562 100644 --- a/biolark/pom.xml +++ b/biolark/pom.xml @@ -33,10 +33,6 @@ <artifactId>xwiki-commons-component-api</artifactId> <version>${xwiki.version}</version> </dependency> - <dependency> - <groupId>org.apache.commons</groupId> - <artifactId>commons-lang3</artifactId> - </dependency> <dependency> <groupId>org.xwiki.platform</groupId> <artifactId>xwiki-platform-bridge</artifactId> diff --git a/generic-rest/pom.xml b/generic-rest/pom.xml index cde9e5b..334a70d 100644 --- a/generic-rest/pom.xml +++ b/generic-rest/pom.xml @@ -27,59 +27,12 @@ </parent> <artifactId>clinical-text-analysis-extension-generic-rest</artifactId> <name>PhenoTips - Clinical Text Analysis - Generic REST implementation for the Java APIs</name> - <repositories> - <repository> - <id>CRBS</id> - <name>CRBS Maven Repo</name> - <url>http://maven.crbs.ucsd.edu/nexus/content/repositories/NIF-snapshot/</url> - </repository> - </repositories> <dependencies> <dependency> <groupId>org.xwiki.commons</groupId> <artifactId>xwiki-commons-component-api</artifactId> <version>${xwiki.version}</version> </dependency> - <dependency> - <groupId>org.apache.commons</groupId> - <artifactId>commons-lang3</artifactId> - </dependency> - <dependency> - <groupId>org.xwiki.platform</groupId> - <artifactId>xwiki-platform-bridge</artifactId> - <version>${xwiki.version}</version> - </dependency> - <dependency> - <groupId>org.xwiki.platform</groupId> - <artifactId>xwiki-platform-extension-distribution</artifactId> - <version>${xwiki.version}</version> - </dependency> - <dependency> - <groupId>org.xwiki.platform</groupId> - <artifactId>xwiki-platform-model</artifactId> - <version>${xwiki.version}</version> - </dependency> - <dependency> - <groupId>org.xwiki.platform</groupId> - <artifactId>xwiki-platform-oldcore</artifactId> - <version>${xwiki.version}</version> - <exclusions> - <exclusion> - <groupId>commons-validator</groupId> - <artifactId>commons-validator</artifactId> - </exclusion> - </exclusions> - </dependency> - <dependency> - <groupId>org.xwiki.platform</groupId> - <artifactId>xwiki-platform-query-manager</artifactId> - <version>${xwiki.version}</version> - </dependency> - <dependency> - <groupId>org.xwiki.commons</groupId> - <artifactId>xwiki-commons-context</artifactId> - <version>${xwiki.version}</version> - </dependency> <dependency> <groupId>${project.groupId}</groupId> <artifactId>clinical-text-analysis-extension-api</artifactId> @@ -91,26 +44,11 @@ </exclusion> </exclusions> </dependency> - <dependency> - <groupId>${project.groupId}</groupId> - <artifactId>phenotips-constants</artifactId> - <version>${phenotips.version}</version> - </dependency> <dependency> <groupId>${project.groupId}</groupId> <artifactId>vocabularies-api</artifactId> <version>${phenotips.version}</version> </dependency> - <dependency> - <groupId>net.sf.json-lib</groupId> - <artifactId>json-lib</artifactId> - <classifier>jdk15</classifier> - <version>2.3</version> - </dependency> - <dependency> - <groupId>org.slf4j</groupId> - <artifactId>slf4j-api</artifactId> - </dependency> <!-- Test dependencies --> <dependency> <groupId>org.xwiki.commons</groupId> @@ -122,11 +60,6 @@ <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> </dependency> - <dependency> - <groupId>org.mockito</groupId> - <artifactId>mockito-all</artifactId> - <version>1.10.8</version> - </dependency> <!-- Scigraph dependencies --> <dependency> From 450df05c9f25ec71d8996facba96ac55b1bfe68f Mon Sep 17 00:00:00 2001 From: Sergiu Dumitriu <sergiu@phenotips.org> Date: Tue, 14 Jun 2016 15:35:28 -0400 Subject: [PATCH 11/14] [misc] Use the right version --- pom.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/pom.xml b/pom.xml index 6d4174a..bd1e619 100644 --- a/pom.xml +++ b/pom.xml @@ -27,6 +27,7 @@ <relativePath /> </parent> <artifactId>clinical-text-analysis-extension</artifactId> + <version>1.0-SNAPSHOT</version> <packaging>pom</packaging> <name>PhenoTips - Clinical Text Analysis</name> From 0e1f3421b2d34b30b87af0573f7fd43eeef0cd0e Mon Sep 17 00:00:00 2001 From: Joe C <escozzia@gmail.com> Date: Tue, 14 Jun 2016 19:36:49 -0400 Subject: [PATCH 12/14] [misc] CSS fix to solve spacing issue. Cheers to @marta- for suggesting. --- ui/src/main/resources/PhenoTips/Clinical Notes Annotation.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/ui/src/main/resources/PhenoTips/Clinical Notes Annotation.xml b/ui/src/main/resources/PhenoTips/Clinical Notes Annotation.xml index d41d15a..12bab50 100644 --- a/ui/src/main/resources/PhenoTips/Clinical Notes Annotation.xml +++ b/ui/src/main/resources/PhenoTips/Clinical Notes Annotation.xml @@ -699,7 +699,6 @@ function updateSuggestionCount() { -o-column-break-inside: avoid; column-break-inside: avoid; display: table; - width: 17.5em; height: 5em; } From d1e85b9f71d4f282cda12355e43823f145ddaed4 Mon Sep 17 00:00:00 2001 From: Jose <jose.cortesvarela@mail.utoronto.ca> Date: Wed, 22 Jun 2016 13:53:03 -0400 Subject: [PATCH 13/14] [misc] Don't throw an error if certain panels are missing. --- .../PhenoTips/Clinical Notes Annotation.xml | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/ui/src/main/resources/PhenoTips/Clinical Notes Annotation.xml b/ui/src/main/resources/PhenoTips/Clinical Notes Annotation.xml index 12bab50..968ff48 100644 --- a/ui/src/main/resources/PhenoTips/Clinical Notes Annotation.xml +++ b/ui/src/main/resources/PhenoTips/Clinical Notes Annotation.xml @@ -187,7 +187,12 @@ refreshButton.observe('click', function() { //generate initial UI elements var panel = $$(".current-phenotype-selection")[0]; -if(panel) { +var suggestionSourcesIds = ['indication_for_referral', 'medical_history']; +var suggestionSources = $$('textarea[name$="' + suggestionSourcesIds.join('"], textarea[name$="') + '"]'); +var _getSourceTexts = function() { + return suggestionSources.pluck('value').join('.\n'); +} +if (panel && suggestionSources.size() > 0) { var clinicalNotesSubPanel = new Element('div', {"class" : "sub-panel"}).insert( new Element('h3', {"class" : "wikigeneratedheader"}).update( @@ -196,20 +201,14 @@ if(panel) { ).insert(widgetContainer); panel.insert({top : clinicalNotesSubPanel}); - //listen for changes in indication for referral text box - var referalIndication = $("PhenoTips.PatientClass_0_indication_for_referral"); - var medicalHistory = $("PhenoTips.PatientClass_0_medical_history"); - text = referalIndication.value + "\n" + medicalHistory.value; + // Init + text = _getSourceTexts(); fetchDismissedSuggestions(updateAnnotations); - referalIndication.observe('blur', function() { - if(!text.startsWith(referalIndication.value)) { - text = referalIndication.value + "\n" + medicalHistory.value; - updateAnnotations(); - } - }); - medicalHistory.observe('blur', function() { - if(!text.endsWith(medicalHistory.value)) { - text = referalIndication.value + "\n" + medicalHistory.value; + //listen for changes in indication for referral text box + suggestionSources.invoke('observe', 'blur', function() { + var newText = _getSourceTexts(); + if (newText != text) { + text = newText; updateAnnotations(); } }); From 25403fc67278741dd1e1002ebd9cf0d3dced66bb Mon Sep 17 00:00:00 2001 From: Jose <jose.cortesvarela@mail.utoronto.ca> Date: Wed, 22 Jun 2016 14:41:23 -0400 Subject: [PATCH 14/14] Make the punctuation filter more robust, and split on newlines. --- .../io/scigraph/annotation/PTEntityProcessor.java | 4 ++++ .../io/scigraph/annotation/PunctuationFilter.java | 13 +++++++------ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/scigraph-service/war/src/main/java/io/scigraph/annotation/PTEntityProcessor.java b/scigraph-service/war/src/main/java/io/scigraph/annotation/PTEntityProcessor.java index 073ec18..d93aa4a 100644 --- a/scigraph-service/war/src/main/java/io/scigraph/annotation/PTEntityProcessor.java +++ b/scigraph-service/war/src/main/java/io/scigraph/annotation/PTEntityProcessor.java @@ -65,6 +65,10 @@ public PTEntityProcessor(EntityRecognizer recognizer) BlockingQueue<List<Token<String>>> startShingleProducer(String content) { BlockingQueue<List<Token<String>>> queue = new LinkedBlockingQueue<List<Token<String>>>(); + /* This is a bit of a hack to make sure newlines get treated as sentence ends - unfortunately it's + * necessary because we tokenize by whitespace, so by the time we get to the punctuation filter + * it's too late. */ + content = content.replaceAll("\\r?\\n", ". "); Reader r; try { r = new InputStreamReader(new ByteArrayInputStream(content.getBytes(ENCODING)), ENCODING); diff --git a/scigraph-service/war/src/main/java/io/scigraph/annotation/PunctuationFilter.java b/scigraph-service/war/src/main/java/io/scigraph/annotation/PunctuationFilter.java index e8e3e74..a61d12f 100644 --- a/scigraph-service/war/src/main/java/io/scigraph/annotation/PunctuationFilter.java +++ b/scigraph-service/war/src/main/java/io/scigraph/annotation/PunctuationFilter.java @@ -38,6 +38,12 @@ public final class PunctuationFilter extends TokenFilter */ public static final int PUNCTUATION_FLAG = 0x1; + /** + * The pattern we use to figure out if there's punctuation. + */ + private static final Pattern PATTERN = Pattern.compile("^(.*?)([\\p{Punct}]+)$", + Pattern.UNICODE_CHARACTER_CLASS); + /** * The current term. */ @@ -48,11 +54,6 @@ public final class PunctuationFilter extends TokenFilter */ private final FlagsAttribute flattribute = addAttribute(FlagsAttribute.class); - /** - * The pattern we use to figure out if there's punctuation. - */ - private final Pattern pattern = Pattern.compile("^(.*?)([\\.!\\?,:;\"'\\(\\)]+)$"); - /** * Our string matcher. */ @@ -65,7 +66,7 @@ public final class PunctuationFilter extends TokenFilter public PunctuationFilter(TokenStream in) { super(in); - m = pattern.matcher(termAtt); + m = PATTERN.matcher(termAtt); } /**