package nl.knaw.huygens.alexandria.textgraph;
/*
* #%L
* alexandria-main
* =======
* Copyright (C) 2015 - 2017 Huygens ING (KNAW)
* =======
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this program. If not, see
* <http://www.gnu.org/licenses/gpl-3.0.html>.
* #L%
*/
import java.text.MessageFormat;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicReference;
import javax.ws.rs.BadRequestException;
import javax.xml.xpath.XPathExpressionException;
import org.apache.commons.lang3.StringUtils;
import nl.knaw.huygens.alexandria.api.model.Annotator;
import nl.knaw.huygens.alexandria.api.model.text.TextRangeAnnotation;
import nl.knaw.huygens.alexandria.api.model.text.TextRangeAnnotation.AbsolutePosition;
import nl.knaw.huygens.alexandria.api.model.text.TextRangeAnnotation.Position;
import nl.knaw.huygens.alexandria.exception.ConflictException;
import nl.knaw.huygens.alexandria.service.AlexandriaService;
import nl.knaw.huygens.alexandria.util.XMLUtil;
import nl.knaw.huygens.tei.QueryableDocument;
public class TextRangeAnnotationValidatorFactory {
private AlexandriaService service;
private UUID resourceUUID;
public TextRangeAnnotationValidatorFactory(AlexandriaService service, UUID resourceUUID) {
this.service = service;
this.resourceUUID = resourceUUID;
}
public void calculateAbsolutePosition(TextRangeAnnotation newTextRangeAnnotation, String annotatedText) {
Position relativePosition = newTextRangeAnnotation.getPosition();
AbsolutePosition absolutePosition = new AbsolutePosition();
if (relativePosition.getXmlId().isPresent()) {
absolutePosition.setXmlId(relativePosition.getXmlId().get())//
.setOffset(relativePosition.getOffset().orElse(1))//
.setLength(relativePosition.getLength().orElse(annotatedText.length()));
} else {
// Log.info("relpos={}", relativePosition);
UUID targetAnnotationUUID = relativePosition.getTargetAnnotationId()//
.orElseThrow(() -> new BadRequestException("Position has neither xmlId nor targetAnnotationId, it needs one or the other."));
TextRangeAnnotation targetTextRangeAnnotation = service.readTextRangeAnnotation(resourceUUID, targetAnnotationUUID)//
.orElseThrow(() -> new BadRequestException("targetAnnotationId " + targetAnnotationUUID + " does not refer to an existing annotation on resource " + resourceUUID + "."));
AbsolutePosition targetAbsolutePosition = targetTextRangeAnnotation.getAbsolutePosition();
absolutePosition.setXmlId(targetAbsolutePosition.getXmlId())//
.setOffset(targetAbsolutePosition.getOffset())//
.setLength(targetAbsolutePosition.getLength())//
;
newTextRangeAnnotation.setUseOffset(targetTextRangeAnnotation.getUseOffset());
}
// Log.info("absolutePosition={}", absolutePosition);
newTextRangeAnnotation.setAbsolutePosition(absolutePosition);
}
public void validate(TextRangeAnnotation textRangeAnnotation) {
validateName(textRangeAnnotation.getName());
validateAnnotator(textRangeAnnotation.getAnnotator());
validateAttributes(textRangeAnnotation.getAttributes());
if (service.nonNestingOverlapWithExistingTextRangeAnnotationForResource(textRangeAnnotation, resourceUUID)) {
throw new ConflictException("Overlapping annotations with the same name and responsibility.");
}
}
public void validate(TextRangeAnnotation newTextRangeAnnotation, TextRangeAnnotation existingTextRangeAnnotation) {
validateNewAttributes(newTextRangeAnnotation, existingTextRangeAnnotation);
validateNewName(newTextRangeAnnotation, existingTextRangeAnnotation);
validateNewPosition(newTextRangeAnnotation, existingTextRangeAnnotation);
validateNewAnnotator(newTextRangeAnnotation, existingTextRangeAnnotation);
validate(newTextRangeAnnotation);
}
public static String getAnnotatedText(Position position, String xml) {
QueryableDocument qDocument = getQueryableDocument(xml);
AtomicReference<String> annotated = new AtomicReference<>("");
position.getXmlId().ifPresent(xmlId -> {
validate(qDocument, //
"count(//*[@xml:id='" + xmlId + "'])", //
1d, //
"The text does not contain an element with the specified xml:id."//
);
String xpath = MessageFormat.format("string(//*[@xml:id=''{0}''])", xmlId);
if (position.getOffset().isPresent()) {
xpath = "substring(//*[@xml:id='" + xmlId + "']," + position.getOffset().get() + "," + position.getLength().orElse(-1) + ")";
validate(qDocument, //
"string-length(" + xpath + ")", //
new Double(position.getLength().get()), //
"The specified offset/length is illegal."//
);
}
try {
// Log.debug("xpath = {}", xpath);
annotated.set(qDocument.evaluateXPathToString(xpath));
} catch (XPathExpressionException e) {
e.printStackTrace();
}
});
return annotated.get().replace(QueryableDocumentCache.AMPERSAND_PLACEHOLDER, "&");
}
private static QueryableDocument getQueryableDocument(String xml) {
// String processedXml = xml.replace("&", QueryableDocumentCache.AMPERSAND_PLACEHOLDER);
// return QueryableDocument.createFromXml(processedXml, true);
return QueryableDocumentCache.get(xml);
}
private void validateNewPosition(TextRangeAnnotation newTextRangeAnnotation, TextRangeAnnotation existingTextRangeAnnotation) {
Position existingPosition = existingTextRangeAnnotation.getPosition();
Position newPosition = newTextRangeAnnotation.getPosition();
boolean positionsAreEquivalent = newPosition.getXmlId().equals(existingPosition.getXmlId());
if (newPosition.getOffset().isPresent()) {
positionsAreEquivalent = positionsAreEquivalent && newPosition.getOffset().get().equals(existingPosition.getOffset().get());
}
if (newPosition.getLength().isPresent()) {
positionsAreEquivalent = positionsAreEquivalent && newPosition.getLength().get().equals(existingPosition.getLength().get());
}
if (!positionsAreEquivalent) {
throw new BadRequestException("You're not allowed to change the annotation position");
}
}
private void validateNewAnnotator(TextRangeAnnotation newTextRangeAnnotation, TextRangeAnnotation existingTextRangeAnnotation) {
String existingAnnotator = existingTextRangeAnnotation.getAnnotator();
String newAnnotator = newTextRangeAnnotation.getAnnotator();
if (!newAnnotator.equals(existingAnnotator)) {
throw new BadRequestException("You're not allowed to change the annotator '" + existingAnnotator + "'");
}
}
private void validateNewName(TextRangeAnnotation newTextRangeAnnotation, TextRangeAnnotation existingTextRangeAnnotation) {
String existingName = existingTextRangeAnnotation.getName();
String newName = newTextRangeAnnotation.getName();
if (!newName.equals(existingName)) {
throw new BadRequestException("You're not allowed to change the annotation name '" + existingName + "'");
}
}
private void validateNewAttributes(TextRangeAnnotation newTextRangeAnnotation, TextRangeAnnotation existingTextRangeAnnotation) {
Set<String> existingAttributeNames = existingTextRangeAnnotation.getAttributes().keySet();
Set<String> newAttributeNames = newTextRangeAnnotation.getAttributes().keySet();
if (!newAttributeNames.equals(existingAttributeNames)) {
throw new BadRequestException("You're only allowed to change existing attributes " + existingAttributeNames);
}
}
private void validateAnnotator(String annotator) {
if (StringUtils.isEmpty(annotator)) {
throw new BadRequestException("No annotator specified.");
}
Optional<String> validAnnotator = service.readResourceAnnotators(resourceUUID).stream()//
.map(Annotator::getCode)//
.filter(annotator::equals)//
.findAny();
if (!validAnnotator.isPresent()) {
throw new BadRequestException("Resource has no annotator with code '" + annotator + "'.");
}
}
private static void validate(QueryableDocument qDocument, String xpath, Double expectation, String errorMessage) {
// Log.info("xpath = '{}'", xpath);
try {
Double evaluation = qDocument.evaluateXPathToDouble(xpath);
// Log.info("evaluation = {}", evaluation);
if (!evaluation.equals(expectation)) {
throw new BadRequestException(errorMessage);
}
} catch (XPathExpressionException e) {
e.printStackTrace();
throw new BadRequestException(errorMessage);
}
}
private void validateAttributes(Map<String, String> attributes) {
attributes.keySet().forEach(this::validateName);
}
private void validateName(String elementName) {
List<String> validationErrors = XMLUtil.validateElementName(elementName);
if (!validationErrors.isEmpty()) {
throw new BadRequestException("The specified annotation name is illegal.");
}
}
}