/**
* Copyright (C) 2012-2013 Selventa, Inc.
*
* This file is part of the OpenBEL Framework.
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The OpenBEL Framework 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 Lesser General Public
* License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with the OpenBEL Framework. If not, see <http://www.gnu.org/licenses/>.
*
* Additional Terms under LGPL v3:
*
* This license does not authorize you and you are prohibited from using the
* name, trademarks, service marks, logos or similar indicia of Selventa, Inc.,
* or, in the discretion of other licensors or authors of the program, the
* name, trademarks, service marks, logos or similar indicia of such authors or
* licensors, in any marketing or advertising materials relating to your
* distribution of the program or any covered product. This restriction does
* not waive or limit your obligation to keep intact all copyright notices set
* forth in the program as delivered to you.
*
* If you distribute the program in whole or in part, or any modified version
* of the program, and you assume contractual liability to the recipient with
* respect to the program or modified version, then you will indemnify the
* authors and licensors of the program for any liabilities that these
* contractual assumptions directly impose on those licensors and authors.
*/
package org.openbel.framework.common.belscript;
import static org.openbel.framework.common.BELUtilities.hasItems;
import static org.openbel.framework.common.Strings.UTF_8;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.charset.Charset;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Stack;
import org.openbel.bel.model.BELDocumentProperty;
import org.openbel.framework.common.BELRuntimeException;
import org.openbel.framework.common.BELUtilities;
import org.openbel.framework.common.enums.ExitCode;
import org.openbel.framework.common.model.*;
public class BELScriptExporter {
private static final String DEFAULT_NAMESPACE_PREFIX = "DEFNS"; //BEL script format supports having a prefix for default namespace
private static final String EVIDENCE_ANNOTATION_NAME = "Evidence"; //built-in annotation name for evidence
private static final String CITATION_ANNOTATION_NAME = "Citation"; //built-in annotation name for citation
private static final int TARGET_STATEMENT_GROUP_LEVEL = 1; //BEL Script only support 1 level of statement group
private boolean useShortForm = false;
private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
private int statementGroupCounter = 0;
public boolean isUseShortForm() {
return useShortForm;
}
public void setUseShortForm(boolean useShortForm) {
this.useShortForm = useShortForm;
}
public void export(Document document, OutputStream os)
throws BELRuntimeException, IOException {
if (document != null && os != null) {
final Writer writer =
new OutputStreamWriter(os, Charset.forName(UTF_8));
writeSectionComment("Document Properties Section", writer);
writeDocumentProperties(document.getHeader(), writer);
writeSectionComment("Definitions Section", writer);
writeNamespaces(document.getNamespaceGroup(), writer);
writeAnnotationDefinitions(document.getDefinitions(), writer);
writeSectionComment("Statements Section", writer);
Map<String, List<String>> inheritedAnnotationMap =
new HashMap<String, List<String>>(); //for inheritance of annotations by child statemnet groups
Map<String, List<String>> currentAnnotationMap =
new HashMap<String, List<String>>(); //for tracking current annotations in belscript
Stack<StatementGroup> statementGroupStack =
new Stack<StatementGroup>();
for (StatementGroup statementGroup : document.getStatementGroups()) {
writeStatementGroup(statementGroup, inheritedAnnotationMap,
currentAnnotationMap, statementGroupStack, writer);
}
writer.flush();
}
}
protected void writeDocumentProperties(Header header, Writer writer)
throws IOException {
writeDocumentProperty(BELDocumentProperty.NAME.getName(),
header.getName(), writer);
writeDocumentProperty(BELDocumentProperty.DESCRIPTION.getName(),
header.getDescription(), writer);
writeDocumentProperty(BELDocumentProperty.VERSION.getName(),
header.getVersion(), writer);
writeDocumentProperty(BELDocumentProperty.COPYRIGHT.getName(),
header.getCopyright(), writer);
writeDocumentProperty(BELDocumentProperty.DISCLAIMER.getName(),
header.getDisclaimer(), writer);
writeDocumentProperty(BELDocumentProperty.AUTHOR.getName(),
header.getAuthors(), writer);
writeDocumentProperty(BELDocumentProperty.LICENSE.getName(),
header.getLicenses(), writer);
writeDocumentProperty(BELDocumentProperty.CONTACT_INFO.getName(),
header.getContactInfo(), writer);
writer.write("\n");
}
protected void writeDocumentProperty(String propertyName, String value,
Writer writer) throws IOException {
if (value != null) {
writer.write("SET DOCUMENT " + propertyName + " = "
+ doubleQuote(value) + "\n");
}
}
protected void writeDocumentProperty(String propertyName,
List<String> values, Writer writer) throws IOException {
if (values != null && values.size() > 0) {
writeDocumentProperty(propertyName, BELUtilities.join(values, "|"),
writer);
}
}
protected void
writeNamespaces(NamespaceGroup namespaceGroup, Writer writer)
throws IOException {
if (namespaceGroup != null) {
writeDefaultNamespace(DEFAULT_NAMESPACE_PREFIX,
namespaceGroup.getDefaultResourceLocation(), writer);
final List<Namespace> nsl = namespaceGroup.getNamespaces();
if (hasItems(nsl)) {
for (Namespace namespace : nsl) {
writeNamespace(namespace, writer);
}
}
writer.write("\n");
}
}
protected void writeDefaultNamespace(String prefix, String location,
Writer writer) throws IOException {
if (location != null && prefix != null) {
writer.write("DEFINE DEFAULT NAMESPACE " + prefix + " AS URL "
+ doubleQuote(location) + "\n");
}
}
protected void writeNamespace(Namespace namespace, Writer writer)
throws IOException {
if (namespace != null) {
writer.write("DEFINE NAMESPACE " + namespace.getPrefix()
+ " AS URL " + doubleQuote(namespace.getResourceLocation())
+ "\n");
}
}
protected void writeAnnotationDefinitions(
List<AnnotationDefinition> annotationDefinitions, Writer writer)
throws IOException {
if (annotationDefinitions != null) {
for (AnnotationDefinition annotationDefinition : annotationDefinitions) {
writeAnnotationDefinition(annotationDefinition, writer);
}
writer.write("\n");
}
}
protected void writeAnnotationDefinition(
AnnotationDefinition annotationDefinition, Writer writer)
throws IOException {
if (annotationDefinition != null) {
writer.write("DEFINE ANNOTATION " + annotationDefinition.getId()
+ " AS ");
if (annotationDefinition.getURL() != null) {
writer.write(" URL "
+ doubleQuote(annotationDefinition.getURL()) + "\n");
} else {
switch (annotationDefinition.getType()) {
case ENUMERATION:
writer.write(" LIST "
+ formatListValues(annotationDefinition.getEnums(),
true) + "\n");
break;
case REGULAR_EXPRESSION:
writer.write(" PATTERN "
+ doubleQuote(annotationDefinition.getValue()) + "\n");
break;
default:
throw new BELRuntimeException(
"Unrecognized annotation type: "
+ annotationDefinition.getType(),
ExitCode.CONVERSION_FAILURE);
}
}
}
}
protected String formatListValues(List<String> values,
boolean alwaysDoubleQuoteValue) {
StringBuilder sb = new StringBuilder();
sb.append("{");
if (values != null) {
Iterator<String> v = values.iterator();
while (v.hasNext()) {
String value = v.next();
boolean isAlphaNumeric = BELUtilities.isAlphanumeric(value);
if (alwaysDoubleQuoteValue || isAlphaNumeric) {
sb.append(doubleQuote(value));
}
if (v.hasNext()) {
sb.append(",");
}
}
}
sb.append("}");
return sb.toString();
}
protected String doubleQuote(String value) {
if (value != null) {
String escapedString =
value.replaceAll("([^\\\\])\\\"", "$1\\\\\"").replaceAll(
"^\\\"", "\\\\\"");
return "\"" + escapedString + "\"";
}
return "";
}
protected void writeSectionComment(String comment, Writer writer)
throws IOException {
writer.write("##################################################################################\n");
writer.write("# " + comment + "\n");
writer.write("\n");
}
protected void writeStatementGroup(StatementGroup statementGroup,
Map<String, List<String>> inheritedAnnotationMap,
Map<String, List<String>> currentAnnotationMap,
Stack<StatementGroup> statementGroupStack, Writer writer)
throws IOException {
if (statementGroup != null) {
Map<String, List<String>> immediateAnnotationMap =
createAnnotationMap(statementGroup.getAnnotationGroup());
Map<String, List<String>> effectiveAnnotationMap =
getEffectiveAnnotationMap(inheritedAnnotationMap,
immediateAnnotationMap);
statementGroupStack.push(statementGroup);
Map<String, List<String>> currentAnnotationMapSnapshot = null;
if (statementGroupStack.size() == TARGET_STATEMENT_GROUP_LEVEL) {
String sgname;
if (statementGroup.getName() != null) {
sgname = "\"" + statementGroup.getName() + "\"";
} else {
sgname = "\"Group " + ++statementGroupCounter + "\"";
}
writer.write("SET STATEMENT_GROUP = " + sgname + "\n");
//save existing annotation statement, when group is unset, restore to this state
currentAnnotationMapSnapshot =
new HashMap<String, List<String>>();
currentAnnotationMapSnapshot.putAll(currentAnnotationMap);
}
//process all statements in this group
List<Statement> statements = statementGroup.getStatements();
if (statements != null) {
for (Statement statement : statements) {
writeStatement(statement, effectiveAnnotationMap,
currentAnnotationMap, writer);
}
}
//process all child statement groups
List<StatementGroup> childGroups =
statementGroup.getStatementGroups();
if (childGroups != null) {
for (StatementGroup group : childGroups) {
writeStatementGroup(group, effectiveAnnotationMap,
currentAnnotationMap, statementGroupStack, writer);
}
}
if (statementGroupStack.size() == TARGET_STATEMENT_GROUP_LEVEL) {
writer.write("\nUNSET STATEMENT_GROUP\n\n");
//restore annotation state when statement group ends
currentAnnotationMap.clear();
currentAnnotationMap.putAll(currentAnnotationMapSnapshot);
}
statementGroupStack.pop();
}
}
protected void writeStatement(Statement statement,
Map<String, List<String>> inheritedAnnotationMap,
Map<String, List<String>> currentAnnotationMap, Writer writer)
throws IOException {
if (statement != null) {
Map<String, List<String>> immediateAnnotationMap =
createAnnotationMap(statement.getAnnotationGroup());
Map<String, List<String>> effectiveAnnotationMap =
getEffectiveAnnotationMap(inheritedAnnotationMap,
immediateAnnotationMap);
writeAnnotations(effectiveAnnotationMap, currentAnnotationMap,
writer);
if (isUseShortForm()) {
writer.write(statement.toBELShortForm());
} else {
writer.write(statement.toBELLongForm());
}
if (BELUtilities.hasLength(statement.getComment())) {
writer.write(" //");
writer.write(statement.getComment());
}
writer.write("\n");
}
}
protected Map<String, List<String>> createAnnotationMap(
AnnotationGroup annotationGroup) {
Map<String, List<String>> annotationMap =
new HashMap<String, List<String>>();
if (annotationGroup != null) {
//add citation
if (annotationGroup.getCitation() != null) {
List<String> citation =
getCitationList(annotationGroup.getCitation());
annotationMap.put(CITATION_ANNOTATION_NAME, citation);
}
//add evidence line
if (annotationGroup.getEvidence() != null) {
List<String> evidence = new ArrayList<String>();
evidence.add(annotationGroup.getEvidence().getValue()
.replaceAll("\n", " "));
annotationMap.put(EVIDENCE_ANNOTATION_NAME, evidence);
}
//add all other annotations
List<Annotation> annotations = annotationGroup.getAnnotations();
if (annotations != null) {
for (Annotation annotation : annotations) {
String name = annotation.getDefinition().getId();
List<String> values = annotationMap.get(name);
if (values == null) {
values = new ArrayList<String>();
annotationMap.put(name, values);
}
values.add(annotation.getValue());
}
}
}
return annotationMap;
}
protected List<String> getCitationList(Citation c) {
List<String> citation = new ArrayList<String>();
citation.add(c.getType().getDisplayValue()); //1st element is type, required
citation.add(c.getName()); //2nd element is name, required
citation.add(c.getReference()); //3, required reference id
if (c.getDate() != null) {
citation.add(dateFormat.format(c.getDate().getTime())); //4, optional time
} else {
citation.add("");
}
if (c.getAuthors() != null) {
citation.add(BELUtilities.join(c.getAuthors(), "|")); //5, optional | separated authors
} else {
citation.add("");
}
if (c.getComment() != null) {
citation.add(c.getComment()); //6, optional comment
} else {
citation.add("");
}
return citation;
}
/**
* Resolves the effective set of annotation by combining the inherited annotations with the immediate annotations associated
* with the statement or statement group. If an annotation existing in both the inherited map and the immediate map, the immediate
* annotation overrides the inherited.
*
* @param inheritedAnnotationMap
* @param immediateAnnotationMap
* @return
*/
protected Map<String, List<String>> getEffectiveAnnotationMap(
Map<String, List<String>> inheritedAnnotationMap,
Map<String, List<String>> immediateAnnotationMap) {
//resolve effective annotations
Map<String, List<String>> effectiveAnnotationMap =
new HashMap<String, List<String>>();
//take all inherited annotation
effectiveAnnotationMap.putAll(inheritedAnnotationMap);
//add or override with annotations from the immediate entity
effectiveAnnotationMap.putAll(immediateAnnotationMap);
return effectiveAnnotationMap;
}
/**
* Writes a series of BEL script control statements via the writer to match current annotations to the effective
* annotations. At the end of this function, the content of currentAnnotationMap will be equivalent to the
* content of effectiveAnnotationMap.
*
* @param effectiveAnnotationMap
* @param currentAnnotationMap
* @param writer
* @throws IOException
*/
protected void writeAnnotations(
Map<String, List<String>> effectiveAnnotationMap,
Map<String, List<String>> currentAnnotationMap, Writer writer)
throws IOException, BELRuntimeException {
List<Map.Entry<String, List<String>>> entriesToSet =
new ArrayList<Map.Entry<String, List<String>>>();
for (Map.Entry<String, List<String>> effEntry : effectiveAnnotationMap
.entrySet()) {
if (!currentAnnotationMap.containsKey(effEntry.getKey())) {
//set annotation that are unique to the effective map
entriesToSet.add(effEntry);
} else {
List<String> currentValues =
currentAnnotationMap.get(effEntry.getKey());
List<String> effectiveValues = effEntry.getValue();
if (currentValues == null || effectiveValues == null) {
throw new BELRuntimeException("Invalid annotation value",
ExitCode.PARSE_ERROR);
}
//set annotation that exist in both map, but values are different in effective map
//this will override the annotation
if (!currentValues.equals(effectiveValues)) {
entriesToSet.add(effEntry);
}
}
}
//remove annotations that are no longer in the effective set
currentAnnotationMap.keySet()
.removeAll(effectiveAnnotationMap.keySet());
if (!currentAnnotationMap.keySet().isEmpty() || !entriesToSet.isEmpty()) {
writer.write("\n");
//unset
for (String name : currentAnnotationMap.keySet()) {
unsetAnnotation(name, writer);
}
//set
for (Map.Entry<String, List<String>> entry : entriesToSet) {
setAnnotation(entry.getKey(), entry.getValue(), writer);
}
writer.write("\n");
}
//current annotation map now reflects the effective annotations
currentAnnotationMap.clear();
currentAnnotationMap.putAll(effectiveAnnotationMap);
}
protected void
setAnnotation(String name, List<String> values, Writer writer)
throws IOException, BELRuntimeException {
if (values == null || values.isEmpty()) {
throw new BELRuntimeException("Invalid annotation value",
ExitCode.PARSE_ERROR);
}
writer.write("SET " + name + " = ");
if (values.size() == 1) {
writer.write(doubleQuote(values.get(0)));
} else {
writer.write(formatListValues(values, true));
}
writer.write("\n");
}
protected void unsetAnnotation(String name, Writer writer)
throws IOException {
writer.write("UNSET " + name);
writer.write("\n");
}
}