/* * Copyright 2012 * Ubiquitous Knowledge Processing (UKP) Lab and FG Language Technology * Technische Universität Darmstadt * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package de.tudarmstadt.ukp.clarin.webanno.api.annotation.adapter; import static de.tudarmstadt.ukp.clarin.webanno.api.annotation.util.WebAnnoCasUtil.selectOverlapping; import static org.apache.uima.fit.util.CasUtil.selectFS; import java.io.Serializable; 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.lang3.StringUtils; import org.apache.uima.cas.CAS; 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.annotation.exception.MultipleSentenceCoveredException; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.model.VID; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.util.WebAnnoCasUtil; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationFeature; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; import de.tudarmstadt.ukp.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 * */ public class ChainAdapter implements TypeAdapter, AutomationTypeAdapter { // private final Logger log = LoggerFactory.getLogger(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); } } public int addSpan( JCas aJCas, int aBegin, int aEnd, AnnotationFeature aFeature, String aLabelValue) throws MultipleSentenceCoveredException { List<Token> tokens = WebAnnoCasUtil.selectOverlapping(aJCas, Token.class, aBegin, aEnd); if (!WebAnnoCasUtil.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 WebAnnoCasUtil.getAddr(newLink); } // get feature Value of existing span annotation public Serializable getSpan(JCas aJCas, int aBegin, int aEnd, AnnotationFeature aFeature, String aLabelValue) { List<Token> tokens = selectOverlapping(aJCas, Token.class, aBegin, aEnd); int begin = tokens.get(0).getBegin(); int end = tokens.get(tokens.size() - 1).getEnd(); String baseName = StringUtils.substringBeforeLast(getAnnotationTypeName(), CHAIN) + LINK; Type linkType = CasUtil.getType(aJCas.getCas(), baseName); for (AnnotationFS fs : CasUtil.selectCovered(aJCas.getCas(), linkType, begin, end)) { if (fs.getBegin() == aBegin && fs.getEnd() == aEnd) { return SpanAdapter.getFeatureValue(fs, aFeature); } } return null; } 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 (WebAnnoCasUtil.isSame(originNext, aTargetFs)) { WebAnnoCasUtil.setFeature(aOriginFs, aFeature, aValue); } // adjacent - target links to origin else if (WebAnnoCasUtil.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 (!WebAnnoCasUtil.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); WebAnnoCasUtil.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 WebAnnoCasUtil.getAddr(aOriginFs); } @Override public void delete(JCas aJCas, VID aVid) { if (aVid.getSubId() == VID.NONE) { deleteSpan(aJCas, aVid.getId()); } else { deleteArc(aJCas, aVid.getId()); } } private void deleteArc(JCas aJCas, int aAddress) { AnnotationFS linkToDelete = WebAnnoCasUtil.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); } private void deleteSpan(JCas aJCas, int aAddress) { Type chainType = getAnnotationType(aJCas.getCas()); AnnotationFS linkToDelete = WebAnnoCasUtil.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 (WebAnnoCasUtil.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); // Delete middle link aJCas.removeFsFromIndexes(linkToDelete); } 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 List<String> getAnnotation(Sentence aSentence, AnnotationFeature aFeature) { 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 = WebAnnoCasUtil.selectByAddr(aJcas, FeatureStructure.class, aAddress); WebAnnoCasUtil.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 (WebAnnoCasUtil.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); WebAnnoCasUtil.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 (WebAnnoCasUtil.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)); } /** * 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(); } public String getLinkNextFeatureName() { return linkNextFeatureName; } public String getChainFirstFeatureName() { return chainFirstFeatureName; } }