Java tutorial
/******************************************************************************* * Copyright 2012 * Ubiquitous Knowledge Processing (UKP) Lab and FG Language Technology * Technische Universitt Darmstadt * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. ******************************************************************************/ package de.tudarmstadt.ukp.clarin.webanno.brat.controller; import static java.util.Arrays.asList; import static org.apache.uima.fit.util.CasUtil.selectFS; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import org.apache.commons.lang.StringUtils; import org.apache.uima.cas.CAS; import org.apache.uima.cas.Feature; import org.apache.uima.cas.FeatureStructure; import org.apache.uima.cas.Type; import org.apache.uima.cas.text.AnnotationFS; import org.apache.uima.fit.util.CasUtil; import org.apache.uima.jcas.JCas; import de.tudarmstadt.ukp.clarin.webanno.api.WebAnnoConst; import de.tudarmstadt.ukp.clarin.webanno.brat.annotation.BratAnnotatorModel; import de.tudarmstadt.ukp.clarin.webanno.brat.display.model.Argument; import de.tudarmstadt.ukp.clarin.webanno.brat.display.model.Entity; import de.tudarmstadt.ukp.clarin.webanno.brat.display.model.Offsets; import de.tudarmstadt.ukp.clarin.webanno.brat.display.model.Relation; import de.tudarmstadt.ukp.clarin.webanno.brat.message.GetDocumentResponse; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationFeature; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; import de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.Sentence; import de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.Token; /** * A class that is used to create Brat chain to CAS and vice-versa * * @author Seid Muhie Yimam */ public class ChainAdapter implements TypeAdapter, AutomationTypeAdapter { // private final Log log = LogFactory.getLog(getClass()); public static final String CHAIN = "Chain"; public static final String LINK = "Link"; private final long layerId; /** * The UIMA type name. */ private String annotationTypeName; /** * The feature of an UIMA annotation for the first span in the chain */ private final String chainFirstFeatureName; /** * The feature of an UIMA annotation for the next span in the chain */ private final String linkNextFeatureName; // private boolean singleTokenBehavior = false; private boolean deletable; private boolean linkedListBehavior; private AnnotationLayer layer; private Map<String, AnnotationFeature> features; public ChainAdapter(AnnotationLayer aLayer, long aLayerId, String aTypeName, String aLabelFeatureName, String aFirstFeatureName, String aNextFeatureName, Collection<AnnotationFeature> aFeatures) { layer = aLayer; layerId = aLayerId; annotationTypeName = aTypeName; chainFirstFeatureName = aFirstFeatureName; linkNextFeatureName = aNextFeatureName; features = new LinkedHashMap<String, AnnotationFeature>(); for (AnnotationFeature f : aFeatures) { features.put(f.getName(), f); } } /** * Add annotations from the CAS, which is controlled by the window size, to the brat response * {@link GetDocumentResponse} * * @param aJcas * The JCAS object containing annotations * @param aResponse * A brat response containing annotations in brat protocol * @param aBratAnnotatorModel * Data model for brat annotations * @param aColoringStrategy * the coloring strategy to render this layer (ignored) */ @Override public void render(JCas aJcas, List<AnnotationFeature> aFeatures, GetDocumentResponse aResponse, BratAnnotatorModel aBratAnnotatorModel, ColoringStrategy aColoringStrategy) { // Get begin and end offsets of window content int windowBegin = BratAjaxCasUtil .selectByAddr(aJcas, Sentence.class, aBratAnnotatorModel.getSentenceAddress()).getBegin(); int windowEnd = BratAjaxCasUtil.selectByAddr(aJcas, Sentence.class, BratAjaxCasUtil.getLastSentenceAddressInDisplayWindow(aJcas, aBratAnnotatorModel.getSentenceAddress(), aBratAnnotatorModel.getPreferences().getWindowSize())) .getEnd(); // Find the features for the arc and span labels - it is possible that we do not find a // feature for arc/span labels because they may have been disabled. AnnotationFeature spanLabelFeature = null; AnnotationFeature arcLabelFeature = null; for (AnnotationFeature f : aFeatures) { if (WebAnnoConst.COREFERENCE_TYPE_FEATURE.equals(f.getName())) { spanLabelFeature = f; } if (WebAnnoConst.COREFERENCE_RELATION_FEATURE.equals(f.getName())) { arcLabelFeature = f; } } // At this point arc and span feature labels must have been found! If not, the later code // will crash. Type chainType = getAnnotationType(aJcas.getCas()); Feature chainFirst = chainType.getFeatureByBaseName(chainFirstFeatureName); int colorIndex = 0; // Iterate over the chains for (FeatureStructure chainFs : selectFS(aJcas.getCas(), chainType)) { AnnotationFS linkFs = (AnnotationFS) chainFs.getFeatureValue(chainFirst); AnnotationFS prevLinkFs = null; // Every chain is supposed to have a different color String color = ColoringStrategy.PALETTE_NORMAL_FILTERED[colorIndex % ColoringStrategy.PALETTE_NORMAL_FILTERED.length]; // The color index is updated even for chains that have no visible links in the current // window because we would like the chain color to be independent of visibility. In // particular the color of a chain should not change when switching pages/scrolling. colorIndex++; // Iterate over the links of the chain while (linkFs != null) { Feature linkNext = linkFs.getType().getFeatureByBaseName(linkNextFeatureName); AnnotationFS nextLinkFs = (AnnotationFS) linkFs.getFeatureValue(linkNext); // Is link after window? If yes, we can skip the rest of the chain if (linkFs.getBegin() >= windowEnd) { break; // Go to next chain } // Is link before window? We only need links that being within the window and that // end within the window if (!(linkFs.getBegin() >= windowBegin) && (linkFs.getEnd() <= windowEnd)) { // prevLinkFs remains null until we enter the window linkFs = nextLinkFs; continue; // Go to next link } String bratTypeName = TypeUtil.getBratTypeName(this); // Render span { String bratLabelText = TypeUtil.getBratLabelText(this, linkFs, (spanLabelFeature != null) ? asList(spanLabelFeature) : Collections.EMPTY_LIST); Offsets offsets = new Offsets(linkFs.getBegin() - windowBegin, linkFs.getEnd() - windowBegin); aResponse.addEntity(new Entity(BratAjaxCasUtil.getAddr(linkFs), bratTypeName, offsets, bratLabelText, color)); } // Render arc (we do this on prevLinkFs because then we easily know that the current // and last link are within the window ;) if (prevLinkFs != null) { String bratLabelText = null; if (linkedListBehavior && arcLabelFeature != null) { // Render arc label bratLabelText = TypeUtil.getBratLabelText(this, prevLinkFs, asList(arcLabelFeature)); } else { // Render only chain type bratLabelText = TypeUtil.getBratLabelText(this, prevLinkFs, Collections.EMPTY_LIST); } List<Argument> argumentList = asList(new Argument("Arg1", BratAjaxCasUtil.getAddr(prevLinkFs)), new Argument("Arg2", BratAjaxCasUtil.getAddr(linkFs))); aResponse.addRelation(new Relation(BratAjaxCasUtil.getAddr(prevLinkFs), bratTypeName, argumentList, bratLabelText, color)); } // if (BratAjaxCasUtil.isSame(linkFs, nextLinkFs)) { // log.error("Loop in CAS detected, aborting rendering of chains"); // break; // } prevLinkFs = linkFs; linkFs = nextLinkFs; } } } public int addSpan(JCas aJCas, int aBegin, int aEnd, AnnotationFeature aFeature, String aLabelValue) throws MultipleSentenceCoveredException { List<Token> tokens = BratAjaxCasUtil.selectOverlapping(aJCas, Token.class, aBegin, aEnd); if (!BratAjaxCasUtil.isSameSentence(aJCas, aBegin, aEnd)) { throw new MultipleSentenceCoveredException( "Annotation coveres multiple sentences, " + "limit your annotation to single sentence!"); } // update the begin and ends (no sub token selection) int begin = tokens.get(0).getBegin(); int end = tokens.get(tokens.size() - 1).getEnd(); // Add the link annotation on the span AnnotationFS newLink = newLink(aJCas, begin, end, aFeature, aLabelValue); // The added link is a new chain on its own - add the chain head FS newChain(aJCas, newLink); return BratAjaxCasUtil.getAddr(newLink); } public int addArc(JCas aJCas, AnnotationFS aOriginFs, AnnotationFS aTargetFs, AnnotationFeature aFeature, String aValue) { // Determine if the links are adjacent. If so, just update the arc label AnnotationFS originNext = getNextLink(aOriginFs); AnnotationFS targetNext = getNextLink(aTargetFs); // adjacent - origin links to target if (BratAjaxCasUtil.isSame(originNext, aTargetFs)) { BratAjaxCasUtil.setFeature(aOriginFs, aFeature, aValue); } // adjacent - target links to origin else if (BratAjaxCasUtil.isSame(targetNext, aOriginFs)) { if (linkedListBehavior) { throw new IllegalStateException("Cannot change direction of a link within a chain"); // BratAjaxCasUtil.setFeature(aTargetFs, aFeature, aValue); } else { // in set mode there are no arc labels anyway } } // if origin and target are not adjacent else { FeatureStructure originChain = getChainForLink(aJCas, aOriginFs); FeatureStructure targetChain = getChainForLink(aJCas, aTargetFs); AnnotationFS targetPrev = getPrevLink(targetChain, aTargetFs); if (!BratAjaxCasUtil.isSame(originChain, targetChain)) { if (linkedListBehavior) { // if the two links are in different chains then split the chains up at the // origin point and target point and create a new link betweek origin and target // the tail of the origin chain becomes a new chain // if originFs has a next, then split of the origin chain up // the rest becomes its own chain if (originNext != null) { newChain(aJCas, originNext); // we set originNext below // we set the arc label below } // if targetFs has a prev, then split it off if (targetPrev != null) { setNextLink(targetPrev, null); } // if it has no prev then we fully append the target chain to the origin chain // and we can remove the target chain head else { aJCas.removeFsFromIndexes(targetChain); } // connect the rest of the target chain to the origin chain setNextLink(aOriginFs, aTargetFs); BratAjaxCasUtil.setFeature(aOriginFs, aFeature, aValue); } else { // collect all the links List<AnnotationFS> links = new ArrayList<AnnotationFS>(); links.addAll(collectLinks(originChain)); links.addAll(collectLinks(targetChain)); // sort them ascending by begin and descending by end (default UIMA order) Collections.sort(links, new AnnotationComparator()); // thread them AnnotationFS prev = null; for (AnnotationFS link : links) { if (prev != null) { // Set next link setNextLink(prev, link); // // Clear arc label - it makes no sense in this mode // setLabel(prev, aFeature, null); } prev = link; } // make sure the last link terminates the chain setNextLink(links.get(links.size() - 1), null); // the chain head needs to point to the first link setFirstLink(originChain, links.get(0)); // we don't need the second chain head anymore aJCas.removeFsFromIndexes(targetChain); } } else { // if the two links are in the same chain, we just ignore the action if (linkedListBehavior) { throw new IllegalStateException( "Cannot connect two spans that are already part of the same chain"); } } } // We do not actually create a new FS for the arc. Features are set on the originFS. return BratAjaxCasUtil.getAddr(aOriginFs); } @Override public void delete(JCas aJCas, int aAddress) { // BEGIN HACK - ISSUE 933 if (isArc) { deleteArc(aJCas, aAddress); } else { deleteSpan(aJCas, aAddress); } // END HACK - ISSUE 933 } public void deleteArc(JCas aJCas, int aAddress) { AnnotationFS linkToDelete = BratAjaxCasUtil.selectByAddr(aJCas, AnnotationFS.class, aAddress); // Create the tail chain // We know that there must be a next link, otherwise no arc would have been rendered! newChain(aJCas, getNextLink(linkToDelete)); // Disconnect the tail from the head setNextLink(linkToDelete, null); } public void deleteSpan(JCas aJCas, int aAddress) { Type chainType = getAnnotationType(aJCas.getCas()); AnnotationFS linkToDelete = BratAjaxCasUtil.selectByAddr(aJCas, AnnotationFS.class, aAddress); // case 1 "removing first link": we keep the existing chain head and just remove the // first element // // case 2 "removing middle link": the new chain consists of the rest, the old chain head // remains // // case 3 "removing the last link": the old chain head remains and the last element of the // chain is removed. // To know which case we have, we first need to find the chain containing the element to // be deleted. FeatureStructure oldChainFs = null; AnnotationFS prevLinkFs = null; chainLoop: for (FeatureStructure chainFs : selectFS(aJCas.getCas(), chainType)) { AnnotationFS linkFs = getFirstLink(chainFs); prevLinkFs = null; // Reset when entering new chain! // Now we seek the link within the current chain while (linkFs != null) { if (BratAjaxCasUtil.isSame(linkFs, linkToDelete)) { oldChainFs = chainFs; break chainLoop; } prevLinkFs = linkFs; linkFs = getNextLink(linkFs); } } // Did we find the chain?! if (oldChainFs == null) { throw new IllegalArgumentException( "Chain link with address [" + aAddress + "] not found in any chain!"); } AnnotationFS followingLinkToDelete = getNextLink(linkToDelete); if (prevLinkFs == null) { // case 1: first element removed setFirstLink(oldChainFs, followingLinkToDelete); aJCas.removeFsFromIndexes(linkToDelete); // removed last element form chain? if (followingLinkToDelete == null) { aJCas.removeFsFromIndexes(oldChainFs); } } else if (followingLinkToDelete == null) { // case 3: removing the last link (but not leaving the chain empty) setNextLink(prevLinkFs, null); aJCas.removeFsFromIndexes(linkToDelete); } else if (prevLinkFs != null && followingLinkToDelete != null) { // case 2: removing a middle link // Set up new chain for rest newChain(aJCas, followingLinkToDelete); // Cut off from old chain setNextLink(prevLinkFs, null); } else { throw new IllegalStateException("Unexpected situation while removing link. Please contact developers."); } } @Override public long getTypeId() { return layerId; } @Override public Type getAnnotationType(CAS cas) { return CasUtil.getType(cas, annotationTypeName); } @Override public String getAnnotationTypeName() { return annotationTypeName; } public void setDeletable(boolean deletable) { this.deletable = deletable; } @Override public boolean isDeletable() { return deletable; } @Override public String getAttachFeatureName() { return null; } @Override public void deleteBySpan(JCas aJCas, AnnotationFS fs, int aBegin, int aEnd) { // TODO Auto-generated method stub } @Override public List<String> getAnnotation(JCas aJcas, AnnotationFeature aFeature, int begin, int end) { return new ArrayList<String>(); } @Override public void delete(JCas aJCas, AnnotationFeature aFeature, int aBegin, int aEnd, Object aValue) { // TODO Auto-generated method stub } @Override public String getAttachTypeName() { // TODO Auto-generated method stub return null; } @Override public void updateFeature(JCas aJcas, AnnotationFeature aFeature, int aAddress, Object aValue) { FeatureStructure fs = BratAjaxCasUtil.selectByAddr(aJcas, FeatureStructure.class, aAddress); BratAjaxCasUtil.setFeature(fs, aFeature, aValue); } /** * Find the chain head for the given link. * * @param aJCas the CAS. * @param aLink the link to search the chain for. * @return the chain. */ private FeatureStructure getChainForLink(JCas aJCas, AnnotationFS aLink) { Type chainType = getAnnotationType(aJCas.getCas()); for (FeatureStructure chainFs : selectFS(aJCas.getCas(), chainType)) { AnnotationFS linkFs = getFirstLink(chainFs); // Now we seek the link within the current chain while (linkFs != null) { if (BratAjaxCasUtil.isSame(linkFs, aLink)) { return chainFs; } linkFs = getNextLink(linkFs); } } // This should never happen unless the data in the CAS has been created erratically throw new IllegalArgumentException("Link not part of any chain"); } private List<AnnotationFS> collectLinks(FeatureStructure aChain) { List<AnnotationFS> links = new ArrayList<AnnotationFS>(); // Now we seek the link within the current chain AnnotationFS linkFs = (AnnotationFS) aChain .getFeatureValue(aChain.getType().getFeatureByBaseName(chainFirstFeatureName)); while (linkFs != null) { links.add(linkFs); linkFs = getNextLink(linkFs); } return links; } /** * Sort ascending by begin and descending by end. */ private static class AnnotationComparator implements Comparator<AnnotationFS> { @Override public int compare(AnnotationFS arg0, AnnotationFS arg1) { int beginDiff = arg0.getBegin() - arg1.getBegin(); if (beginDiff == 0) { return arg1.getEnd() - arg0.getEnd(); } else { return beginDiff; } } } /** * Create a new chain head feature structure. Already adds the chain to the CAS. */ private FeatureStructure newChain(JCas aJCas, AnnotationFS aFirstLink) { Type chainType = getAnnotationType(aJCas.getCas()); FeatureStructure newChain = aJCas.getCas().createFS(chainType); newChain.setFeatureValue(chainType.getFeatureByBaseName(chainFirstFeatureName), aFirstLink); aJCas.addFsToIndexes(newChain); return newChain; } /** * Create a new link annotation. Already adds the chain to the CAS. */ private AnnotationFS newLink(JCas aJCas, int aBegin, int aEnd, AnnotationFeature aFeature, String aLabelValue) { String baseName = StringUtils.substringBeforeLast(getAnnotationTypeName(), CHAIN) + LINK; Type linkType = CasUtil.getType(aJCas.getCas(), baseName); AnnotationFS newLink = aJCas.getCas().createAnnotation(linkType, aBegin, aEnd); BratAjaxCasUtil.setFeature(newLink, aFeature, aLabelValue); aJCas.getCas().addFsToIndexes(newLink); return newLink; } /** * Set the first link of a chain in the chain head feature structure. */ private void setFirstLink(FeatureStructure aChain, AnnotationFS aLink) { aChain.setFeatureValue(aChain.getType().getFeatureByBaseName(chainFirstFeatureName), aLink); } /** * Get the first link of a chain from the chain head feature structure. */ private AnnotationFS getFirstLink(FeatureStructure aChain) { return (AnnotationFS) aChain.getFeatureValue(aChain.getType().getFeatureByBaseName(chainFirstFeatureName)); } /** * Get the chain link before the given link within the given chain. The given link must be part * of the given chain. * * @param aChain * a chain head feature structure. * @param aLink * a link. * @return the link before the given link or null if the given link is the first link of the * chain. */ private AnnotationFS getPrevLink(FeatureStructure aChain, AnnotationFS aLink) { AnnotationFS prevLink = null; AnnotationFS curLink = getFirstLink(aChain); while (curLink != null) { if (BratAjaxCasUtil.isSame(curLink, aLink)) { break; } prevLink = curLink; curLink = getNextLink(curLink); } return prevLink; } /** * Set the link following the current link. */ private void setNextLink(AnnotationFS aLink, AnnotationFS aNext) { aLink.setFeatureValue(aLink.getType().getFeatureByBaseName(linkNextFeatureName), aNext); } /** * Get the link following the current link. */ private AnnotationFS getNextLink(AnnotationFS aLink) { return (AnnotationFS) aLink.getFeatureValue(aLink.getType().getFeatureByBaseName(linkNextFeatureName)); } // BEGIN HACK - ISSUE 933 private boolean isArc = false; public void setArc(boolean aIsArc) { isArc = aIsArc; } // END HACK - ISSUE 933 /** * Controls whether the chain behaves like a linked list or like a set. When operating as a * set, chains are automatically threaded and no arrows and labels are displayed on arcs. * When operating as a linked list, chains are not threaded and arrows and labels are displayed * on arcs. * * @param aBehaveLikeSet whether to behave like a set. */ public void setLinkedListBehavior(boolean aBehaveLikeSet) { linkedListBehavior = aBehaveLikeSet; } public boolean isLinkedListBehavior() { return linkedListBehavior; } @Override public AnnotationLayer getLayer() { return layer; } @Override public Collection<AnnotationFeature> listFeatures() { return features.values(); } }