/**
* 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.ws.dialect;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;
import org.openbel.framework.api.Dialect;
import org.openbel.framework.api.Equivalencer;
import org.openbel.framework.api.EquivalencerException;
import org.openbel.framework.api.Kam;
import org.openbel.framework.api.Kam.KamNode;
import org.openbel.framework.api.internal.KAMStoreDaoImpl.BelTerm;
import org.openbel.framework.api.KAMStore;
import org.openbel.framework.api.KAMStoreException;
import org.openbel.framework.common.bel.parser.BELParser;
import org.openbel.framework.common.model.BELObject;
import org.openbel.framework.common.model.Namespace;
import org.openbel.framework.common.model.Parameter;
import org.openbel.framework.common.model.Term;
import org.openbel.framework.common.protonetwork.model.SkinnyUUID;
/**
* {@link Dialect} implementation that allows control over the {@link Namespace}
* s used in conversion. An ordered list of {@link Namespace}s can be assigned
* to each of the {@link NamespaceDomain}s; labeling for each {@link Parameter}
* in a supporting term will use these priorities to determine a String value.
* If a value cannot be found for a {@link Parameter} in the desired namespace,
* the next namespace in the list will be attempted. When all preferences are
* exhausted the label will default to the default label of the term.<br>
* This implementation also allows definition of the {@link BELSyntax} to be used
* and whether or not to strip the namespace prefixes from the returned labels.<br>
* <br>
* Construction of this class is limited to
* {@link DialectFactory#createCustomDialect(org.openbel.framework.core.kamstore.data.jdbc.KAMCatalogDao.KamInfo, List, List, List, BELSyntax, boolean)}
* <br>
* This implementation caches labels for nodes and is thus suitable only for a
* single {@link Kam}. Usage across {@link Kam}s is not guarded against but will
* cause unpredictable results.<br>
* This implementation is thread-safe.
*
* @author Steve Ungerer
*/
public class CustomDialect implements Dialect {
private final KAMStore kAMStore;
private final Equivalencer equivalencer = new Equivalencer();
private boolean removeNamespacePrefix = false;
private boolean displayLongForm = false;
private List<Namespace> geneNamespaces = new ArrayList<Namespace>(0);
private List<Namespace> bpNamespaces = new ArrayList<Namespace>(0);
private List<Namespace> chemNamespaces = new ArrayList<Namespace>(0);
private Map<String, String> labelCache =
new ConcurrentHashMap<String, String>();
private Map<String, Namespace> kamNamespaces; // prefix : namespace
private Map<String, NamespaceDomain> nsDomains; // prefix : domain
private boolean initialized;
private int hashCode;
/**
* Construct a new instance of a {@link CustomDialect}.
* Post-construction the appropriate mutation must occur with
* {@link #initialize()} being invoked prior to usage.
*
* @param kAMStore
* @throws KAMStoreException
*/
CustomDialect(KAMStore kAMStore) throws KAMStoreException {
this.kAMStore = kAMStore;
}
/**
* Set the {@link Namespace} priority for {@link NamespaceDomain#Gene}
* namespaces.
*
* @param geneNamespaces
*/
void setGeneNamespaces(List<Namespace> geneNamespaces) {
if (initialized) {
throw new IllegalStateException(
"Dialect already initialized; mutation not allowed");
}
this.geneNamespaces = geneNamespaces;
}
/**
* Set the {@link Namespace} priority for
* {@link NamespaceDomain#BiologicalProcess} namespaces.
*
* @param bpNamespaces
*/
void setBpNamespaces(List<Namespace> bpNamespaces) {
if (initialized) {
throw new IllegalStateException(
"Dialect already initialized; mutation not allowed");
}
this.bpNamespaces = bpNamespaces;
}
/**
* Set the {@link Namespace} priority for {@link NamespaceDomain#Chemical}
* namespaces.
*
* @param chemNamespaces
*/
void setChemNamespaces(List<Namespace> chemNamespaces) {
if (initialized) {
throw new IllegalStateException(
"Dialect already initialized; mutation not allowed");
}
this.chemNamespaces = chemNamespaces;
}
/**
* Set the {@link Namespace} {@link Map} used in finding resource locations
* for a namespace prefix.<br>
* Map key: the namespace prefix used in the {@link Kam}<br>
* Map value: resource location of the namespace.
*
* @param kamNamespaces
*/
void setKamNamespaces(Map<String, Namespace> kamNamespaces) {
if (initialized) {
throw new IllegalStateException(
"Dialect already initialized; mutation not allowed");
}
this.kamNamespaces = kamNamespaces;
}
/**
* Set the {@link NamespaceDomain} {@link Map} used to determine the domain
* of a particular {@link Namespace}.<br>
* Map key: the namespace prefix used in the {@link Kam}<br>
* Map value: {@link NamespaceDomain} of the namespace.
*
* @param nsDomains
*/
void setNsDomains(Map<String, NamespaceDomain> nsDomains) {
if (initialized) {
throw new IllegalStateException(
"Dialect already initialized; mutation not allowed");
}
this.nsDomains = nsDomains;
}
/**
* Set the dialect to display labels in BEL long form
*
* @param displayLongForm
*/
void setDisplayLongForm(boolean displayLongForm) {
if (initialized) {
throw new IllegalStateException(
"Dialect already initialized; mutation not allowed");
}
this.displayLongForm = displayLongForm;
}
/**
* Set the dialect to strip namespace prefixes from labels.<br>
* Note that prefixes will remain in place if none of the user-configured
* namespaces can be used to display the label and the default label is
* used.
*
* @param removeNamespacePrefix
*/
void setRemoveNamespacePrefix(boolean removeNamespacePrefix) {
if (initialized) {
throw new IllegalStateException(
"Dialect already initialized; mutation not allowed");
}
this.removeNamespacePrefix = removeNamespacePrefix;
}
/**
* Initialize the dialect. Must be invoked post-mutation.
*/
void initialize() {
if (initialized) {
throw new IllegalStateException(
"Dialect already initialized; re-initialization not allowed");
}
this.hashCode = new HashCodeBuilder().append(geneNamespaces)
.append(bpNamespaces).append(chemNamespaces)
.append(displayLongForm).append(removeNamespacePrefix)
.toHashCode();
this.initialized = true;
}
/**
* {@inheritDoc}
*/
@Override
public String getLabel(KamNode kamNode) {
if (!initialized) {
throw new IllegalStateException("Dialect not initialized");
}
String label = labelCache.get(kamNode.getLabel());
if (label == null) {
try {
label = labelNode(kamNode);
} catch (Exception e) {
// TODO exception
}
labelCache.put(kamNode.getLabel(), label);
}
return label;
}
/**
* Internal method to obtain a label for a node. Grabs the first
* {@link BelTerm} that supports the node, parses it, and converts it to the
* preferred namespaces to obtain a label.
*
* @param node
* @return
* @throws KAMStoreException
*/
protected String labelNode(KamNode node) throws KAMStoreException {
List<BelTerm> terms = kAMStore.getSupportingTerms(node);
if (!terms.isEmpty()) {
BelTerm bt = terms.get(0);
Term parsed;
try {
parsed = BELParser.parseTerm(bt.getLabel());
} catch (Exception e) {
return null;
}
Term converted = convert(parsed);
return displayLongForm ? converted.toBELLongForm() : converted
.toBELShortForm();
}
return null;
}
/**
* Convert a {@link Term} to preferred namespaces
*
* @param orig
* @return
*/
protected Term convert(Term orig) {
Term t = new Term(orig.getFunctionEnum());
for (BELObject o : orig.getFunctionArguments()) {
t.addFunctionArgument(convert(o));
}
return t;
}
/**
* Convert a {@link BELObject}. Currently handles only {@link Term}s and
* {@link Parameter}s as these are the only objects supported by Term.
*
* @param o
* @return
* @see Term#addFunctionArgument(BELObject)
*/
protected BELObject convert(BELObject o) {
Class<?> clazz = o.getClass();
if (Term.class.isAssignableFrom(clazz)) {
return convert((Term) o);
} else if (Parameter.class.isAssignableFrom(clazz)) {
return convert((Parameter) o);
} else {
throw new UnsupportedOperationException("BEL object type "
+ o.getClass() + " is not supported");
}
}
/**
* Convert a parameter to the preferred namespaces.
*
* @param orig
* @return the converted {@link Parameter} or the original parameter if no
* conversion was possible
*/
protected Parameter convert(Parameter orig) {
if (orig.getNamespace() == null) {
return orig;
}
// determine domain of parameter namespace
NamespaceDomain domain = nsDomains.get(orig.getNamespace().getPrefix());
if (domain == null) {
return orig;
}
// iterate appropriate collection to find equiv
List<Namespace> coll;
switch (domain) {
case BiologicalProcess:
coll = bpNamespaces;
break;
case Chemical:
coll = chemNamespaces;
break;
case Gene:
coll = geneNamespaces;
break;
default:
throw new UnsupportedOperationException("Unknown domain: " + domain);
}
try {
SkinnyUUID uuid = equivalencer.getUUID(
getNamespace(orig.getNamespace()), orig.getValue());
if (uuid == null) {
// no equivalents anywhere
return orig;
}
// find first equivalent in list of desired
for (Namespace ns : coll) {
String v = equivalencer.equivalence(uuid, ns);
if (v != null) {
return removeNamespacePrefix ? new Parameter(null, v)
: new Parameter(ns, v);
}
}
} catch (EquivalencerException e) {
// TODO exception
return null;
}
// if no equiv, use param as-is
return orig;
}
/**
* Get a fully populated namespace object with valid resource location.
* (namespaces parsed by BELParser will not have a valid resource location)
*
* @param ns
* @return
*/
protected Namespace getNamespace(Namespace ns) {
return kamNamespaces.get(ns.getPrefix());
}
/**
* {@inheritDoc}
*/
@Override
public int hashCode() {
return hashCode;
}
/**
* {@inheritDoc}
*/
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (obj == this) {
return true;
}
if (!(obj instanceof CustomDialect)) {
return false;
}
CustomDialect rhs = (CustomDialect) obj;
return new EqualsBuilder().append(geneNamespaces, rhs.geneNamespaces)
.append(bpNamespaces, rhs.bpNamespaces)
.append(chemNamespaces, rhs.chemNamespaces)
.append(displayLongForm, rhs.displayLongForm)
.append(removeNamespacePrefix, rhs.removeNamespacePrefix)
.isEquals();
}
}