// BridgeDb, // An abstraction layer for identifier mapping services, both local and online. // Copyright 2006-2009 BridgeDb developers // // 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 org.bridgedb; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; import org.bridgedb.impl.TransitiveGraph; /** * combines multiple {@link IDMapper}'s in a stack. * <p> * The behavior of the {@link IDMapper} interface implementations * differs per method: * if the method returns a single result, usually it is * from the first child database that has a sensible result. * This also means that the child databases have a definitive * ordering: the first one shadows the second one for some results. * <p> * If the method returns a list, IDMapperStack joins * the result from all connected child databases together. * <p> * Transitive maps are deduced from the IDMappers, added to this * IDMapperStack. Transitive maps are enabled as soon as the transitivity * is set to be true. * <p> * In order to calculate the deduced transitive mappings, we build * a graph that represents the possible mappings supported by * the IDMappers in this IDMapperStack. The nodes of this graph are * DataSources, the Edges are IDMappers. Possible mappings are * loop free paths in this graph. We consider a path p to be loop free * if no data source in p is reached twice by the same IDMapper. * <p> * The mapping graph for transitive maps is retained and re-calculated * whenever an IDMapper is added or removed from this IDMapperStack. * */ public class IDMapperStack implements IDMapper, AttributeMapper { // reference shared with TransitiveGraph private final List<IDMapper> gdbs = new CopyOnWriteArrayList<IDMapper>(); /** Helper class for calculating transitive paths */ private TransitiveGraph transitiveGraph = null; private TransitiveGraph getTransitiveGraph() throws IDMapperException { if (transitiveGraph == null) transitiveGraph = new TransitiveGraph(gdbs); return transitiveGraph; } /** * Create a fresh IDMapper from a connectionString and add it to the stack. * @param connectionString connectionString for configuring the new IDMapper * @return the newly created IDMapper * @throws IDMapperException when the connection failed. */ public IDMapper addIDMapper(String connectionString) throws IDMapperException { IDMapper idMapper = BridgeDb.connect(connectionString); addIDMapper(idMapper); return idMapper; } /** * Add an existing IDMapper to the stack. * @param idMapper IDMapper to be added. */ public void addIDMapper(IDMapper idMapper) { if (idMapper!=null) { gdbs.add(idMapper); transitiveGraph = null; // trigger rebuild. } } private boolean isTransitive = false; /** * Set Transitivity mode, where all mappings are combined to infer * second degree mappings. * @param value true or false */ public void setTransitive(boolean value) { isTransitive = value; } /** * @return true if the stack is in transitive mode */ public boolean getTransitive() { return isTransitive; } /** * Remove an idMapper from the stack. * Automatically rebuilds the mapping graph. * * @param idMapper IDMapper to be removed. */ public void removeIDMapper(IDMapper idMapper) { gdbs.remove(idMapper); transitiveGraph = null; // trigger rebuild } /** * closes all child databases. * @throws IDMapperException when closing failed for one of the databases. It will still try to * close all child databases even if one throws an exception. However, only the last exception will be thrown. */ public void close() throws IDMapperException { IDMapperException postponed = null; for (IDMapper child : gdbs) { if (child != null) { try { child.close(); child = null; // garbage collect } catch (IDMapperException ex) { postponed = ex; } } } if (postponed != null) { throw postponed; } } /** {@inheritDoc} */ public boolean xrefExists(Xref xref) throws IDMapperException { for (IDMapper child : gdbs) { if (child != null && child.isConnected()) { if(child.xrefExists(xref)) { return true; } } } return false; } /** * @return true if at least one of the child services * are connected. */ public boolean isConnected() { for (IDMapper child : gdbs) { if (child != null && child.isConnected()) { return true; } } return false; } private final IDMapperCapabilities caps = new IDMapperStackCapabilities(); private class IDMapperStackCapabilities implements IDMapperCapabilities { /** * @return union of DataSources supported by child services * @throws IDMapperException when one of the services was unavailable */ public Set<DataSource> getSupportedSrcDataSources() throws IDMapperException { final Set<DataSource> result = new HashSet<DataSource>(); for (IDMapper idm : IDMapperStack.this.gdbs) { Set<DataSource> dss = null; dss = idm.getCapabilities().getSupportedSrcDataSources(); if (dss!=null) { result.addAll (dss); } } return result; } /** * @return union of DataSources supported by child services * @throws IDMapperException when one of the services was unavailable */ public Set<DataSource> getSupportedTgtDataSources() throws IDMapperException { final Set<DataSource> result = new HashSet<DataSource>(); for (IDMapper idm : IDMapperStack.this.gdbs) { Set<DataSource> dss = null; dss = idm.getCapabilities().getSupportedTgtDataSources(); if (dss!=null) { result.addAll (dss); } } return result; } /** {@inheritDoc} */ public boolean isMappingSupported(DataSource src, DataSource tgt) throws IDMapperException { if(isTransitive) { return getTransitiveGraph().isTransitiveMappingSupported(src, tgt); } else { for (IDMapper idm : IDMapperStack.this.gdbs) { if (idm.getCapabilities().isMappingSupported(src, tgt)) { return true; } } } return false; } /** * @return true if free search is supported by one of the children */ public boolean isFreeSearchSupported() { // returns true if any returns true // TODO: not sure if this is the right logic? for (IDMapper idm : IDMapperStack.this.gdbs) { if (idm.getCapabilities().isFreeSearchSupported()) return true; } return false; } /** {@inheritDoc} */ public Set<String> getKeys() { return Collections.emptySet(); } /** {@inheritDoc} */ public String getProperty(String key) { return null; } }; /** * @return an object describing the capabilities of the combined stack of services. */ public IDMapperCapabilities getCapabilities() { return caps; } /** {@inheritDoc} */ public Set<Xref> freeSearch(String text, int limit) throws IDMapperException { Set<Xref> result = new HashSet<Xref>(); for (IDMapper child : gdbs) { if (child != null && child.isConnected()) { result.addAll (child.freeSearch(text, limit)); } } return result; } /** {@inheritDoc} */ public Map<Xref, Set<Xref>> mapID(Collection<Xref> srcXrefs, DataSource... tgtDataSources) throws IDMapperException { if (isTransitive) { return mapIDtransitive(srcXrefs, tgtDataSources); } else { return mapIDnormal(srcXrefs, tgtDataSources); } } /** * helper method to map Id's in non-transitive mode. * @param srcXrefs mapping source * @param tgtDataSources target data sources * @return mapping result * @throws IDMapperException if one of the children fail */ private Map<Xref, Set<Xref>> mapIDnormal(Collection<Xref> srcXrefs, DataSource... tgtDataSources) throws IDMapperException { Map<Xref, Set<Xref>> result = new HashMap<Xref, Set<Xref>>(); for (IDMapper child : gdbs) { if (child != null && child.isConnected()) { for (Map.Entry<Xref, Set<Xref>> entry : child.mapID(srcXrefs, tgtDataSources).entrySet()) { Set<Xref> resultSet = result.get (entry.getKey()); if (resultSet == null) { resultSet = new HashSet<Xref>(); result.put (entry.getKey(), resultSet); } resultSet.addAll (entry.getValue()); } } } return result; } /** * helper method to map Id's in transitive mode. * @param srcXrefs mapping source * @param tgtDataSources target data sources * @return mapping result * @throws IDMapperException if one of the children fail */ private Map<Xref, Set<Xref>> mapIDtransitive(Collection<Xref> srcXrefs, DataSource... tgtDataSources) throws IDMapperException { // Current implementation just repeatedly calls mapIDTransitive (Xref, Set<Ds>) // It may be possible to rearrange loops to optimize for fewer database calls. Map <Xref, Set<Xref>> result = new HashMap<Xref, Set<Xref>>(); for (Xref ref: srcXrefs) { result.put (ref, mapIDtransitive(ref, tgtDataSources)); } return result; } /** {@inheritDoc} */ public Set<String> getAttributes(Xref ref, String attrname) throws IDMapperException { Set<String> result = new HashSet<String>(); for (IDMapper child : gdbs) { if (child != null && child instanceof AttributeMapper && child.isConnected()) { result.addAll (((AttributeMapper)child).getAttributes(ref, attrname)); } } return result; } /** {@inheritDoc} */ public Set<String> getAttributesForAllMappings(Xref ref, String attrname, DataSource... dataSources) throws IDMapperException { Set<String> result = new HashSet<String>(); Set<Xref> mappings = mapID(ref, dataSources); mappings.add(ref); // TODO: check if this is really needed for (Xref mappedRef : mappings) { result.addAll(getAttributes(mappedRef, attrname)); } return result; } /** * @return true if free attribute search is supported by one of the children */ public boolean isFreeAttributeSearchSupported() { // returns true if any returns true // TODO: not sure if this is the right logic? for (IDMapper child : IDMapperStack.this.gdbs) { if (child != null && child instanceof AttributeMapper) { if (((AttributeMapper)child).isFreeAttributeSearchSupported()) return true; } } return false; } /** {@inheritDoc} */ public Map<Xref, String> freeAttributeSearch (String query, String attrType, int limit) throws IDMapperException { Map<Xref, String> result = null; for (IDMapper child : gdbs) { if (child != null && child instanceof AttributeMapper && child.isConnected() && ((AttributeMapper)child).isFreeAttributeSearchSupported()) { Map<Xref, String> childResult = ((AttributeMapper)child).freeAttributeSearch(query, attrType, limit); if (result == null) result = childResult; else { for (Xref ref : childResult.keySet()) { if (!result.containsKey(ref)) result.put (ref, childResult.get(ref)); } } } } return result; } public Map<Xref, Set<String>> freeAttributeSearchEx (String query, String attrType, int limit) throws IDMapperException { Map<Xref, Set<String>> result = new HashMap<Xref, Set<String>>(); for (IDMapper child : gdbs) { if (child != null && child instanceof AttributeMapper && child.isConnected() && ((AttributeMapper)child).isFreeAttributeSearchSupported()) { Map<Xref, Set<String>> childResult = ((AttributeMapper)child).freeAttributeSearchEx(query, attrType, limit); if (result == null) result = childResult; else { for (Xref ref : childResult.keySet()) { if (!result.containsKey(ref)) result.put (ref, childResult.get(ref)); } } } } return result; } /** @return concatenation of toString of each child */ @Override public String toString() { String result = ""; boolean first = true; for (IDMapper child : gdbs) { if (!first) result += ", "; first = false; result += child.toString(); } return result; } /** @return number of child databases */ public int getSize() { return gdbs.size(); } /** * @param index in the range 0 <= index < getSize() * @return the IDMapper at the given position */ public IDMapper getIDMapperAt(int index) { return gdbs.get(index); } /** {@inheritDoc} */ public Set<Xref> mapID(Xref ref, DataSource... resultDs) throws IDMapperException { if (isTransitive) { return mapIDtransitive (ref, resultDs); } else { return mapIDnormal (ref, resultDs); } } /** * Helper method to map Id's in transitive mode. * Relies on pre-calculated mapping graph * in order to deduce transitively mappings. * * @param ref Xref to map * @param resultDs target data sources * @return mapping result * @throws IDMapperException if one of the children fail */ private Set<Xref> mapIDtransitive(Xref ref, DataSource... resultDs) throws IDMapperException { if( resultDs.length == 0 ) { return getTransitiveGraph().mapIDtransitiveUntargetted(ref); } else { Set<DataSource> dsFilter = new HashSet<DataSource>(Arrays .asList(resultDs)); Set<Xref> result = getTransitiveGraph().mapIDtransitiveTargetted(ref, dsFilter); return result; } } /** * helper method to map Id's in transitive mode. * @param ref Xref to map * @param resultDs target data sources * @return mapping result * @throws IDMapperException if one of the children fail */ private Set<Xref> mapIDnormal(Xref ref, DataSource... resultDs) throws IDMapperException { Set<Xref> result = new HashSet<Xref>(); for (IDMapper child : gdbs) { if (child != null && child.isConnected()) { result.addAll (child.mapID(ref, resultDs)); } } return result; } /** {@inheritDoc} */ public Set<String> getAttributeSet() throws IDMapperException { Set<String> result = new HashSet<String>(); for (IDMapper child : gdbs) { if (child != null && child instanceof AttributeMapper && child.isConnected()) { result.addAll (((AttributeMapper)child).getAttributeSet()); } } return result; } /** {@inheritDoc} */ public Map<String, Set<String>> getAttributes(Xref ref) throws IDMapperException { Map<String, Set<String>> result = new HashMap<String, Set<String>>(); for (IDMapper child : gdbs) { if (child != null && child instanceof AttributeMapper && child.isConnected()) { for (Map.Entry<String, Set<String>> entry : ((AttributeMapper)child).getAttributes(ref).entrySet()) { Set<String> thisSet; if (!result.containsKey(entry.getKey())) { thisSet = new HashSet<String>(); result.put (entry.getKey(), thisSet); } else { thisSet = result.get(entry.getKey()); } thisSet.addAll(entry.getValue()); } } } return result; } /** {@inheritDoc} */ public Map<String, Set<String>> getAttributesForAllMappings(Xref ref, DataSource... dataSources) throws IDMapperException { Map<String, Set<String>> result = new HashMap<String, Set<String>>(); Set<Xref> mappings = mapID(ref, dataSources); mappings.add(ref); // TODO: check if this is really needed for (Xref mappedRef : mappings) { result.putAll(getAttributes(mappedRef)); } return result; } /** get all mappers */ public List<IDMapper> getMappers() { return gdbs; } }