// 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.impl;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
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 javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import org.bridgedb.DataSource;
import org.bridgedb.IDMapper;
import org.bridgedb.IDMapperException;
import org.bridgedb.Xref;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;
/**
* To prevent duplication and redundancy, functions that are common to
* multiple IDMapper implementations
* can be placed here.
* <p>
* <b>Warning!</b> This class is not part of the public API of BridgeDb. Methods in this class
* may disappear or change in backwards-incompatible ways. <b>This class should not be used by applications!</b>
*/
public final class InternalUtils
{
/** private constructor to prevent instantiation. */
private InternalUtils() {}
/**
* call the "single" mapID (Xref, ...) multiple times
* to perform mapping of a Set.
* <p>
* This is intended for IDMappers that don't gain any advantage of mapping
* multiple ID's at a time. They can implement mapID(Xref, ...), and
* use mapMultiFromSingle to simulate mapID(Set, ...)
* @param mapper used for performing a single mapping
* @param srcXrefs xrefs to translate
* @param tgt DataSource(s) to map to, optional.
* @return mappings with the translated result for each input Xref. Never returns null, but
* not each input is a key in the output map.
* @throws IDMapperException when mapper.mapID throws IDMapperException
*/
public static Map<Xref, Set<Xref>> mapMultiFromSingle(IDMapper mapper, Collection<Xref> srcXrefs, DataSource... tgt)
throws IDMapperException
{
final Map<Xref, Set<Xref>> result = new HashMap<Xref, Set<Xref>>();
for (Xref src : srcXrefs)
{
final Set<Xref> refs = mapper.mapID(src, tgt);
if (refs.size() > 0)
result.put (src, refs);
}
return result;
}
/**
* call the "multi" mapID (Set, ...) using a Set with one item
* to perform mapping of a single ID.
* <p>
* This is intended for IDMappers that have a performance advantage of mapping
* multiple ID's at a time. They can implement mapID(Set, ...), and
* use mapSingleFromMulti to simulate mapID(Xref, ...)
* @param mapper used for performing a multi-mapping
* @param src xref to translate
* @param tgt DataSource(s) to map to, optional.
* @return Set of translated Xrefs, or an empty set if none were found.
* @throws IDMapperException when mapper.mapID throws IDMapperException
*/
public static Set<Xref> mapSingleFromMulti(IDMapper mapper, Xref src, DataSource... tgt)
throws IDMapperException
{
Set<Xref> srcXrefs = new HashSet<Xref>();
srcXrefs.add(src);
Map<Xref, Set<Xref>> mapXrefs = mapper.mapID(srcXrefs, tgt);
if (mapXrefs.containsKey(src))
return mapXrefs.get(src);
else
return Collections.emptySet();
}
/**
* parse configuration params of the connection string. Connection strings are
* expected to have a formatting like @code{base?arg1=val&arg2=val}.
* @param location configuration string to parse.
* @param allowedParams allowed argument names to appear before =
* @return key / value Map of configuration arguments. The base (the part before the ?)
* is returned in the special key "BASE". If the part before ? is empty,
* the "BASE" key is not created.
* @throws IllegalArgumentException if arguments do not follow the key=val structure, or
* if the key is not in allowedParams
*/
public static Map<String, String> parseLocation (String location, String... allowedParams)
{
Map<String, String> result = new HashMap<String, String>();
Set<String> allowedSet = new HashSet<String>(Arrays.asList(allowedParams));
String param = location;
int idx = location.indexOf('?');
if (idx > -1)
{
// do not add empty string.
if (idx > 0)
result.put ("BASE", location.substring(0,idx));
param = location.substring(idx+1);
}
if ("".equals(param)) return result;
String[] args = param.split ("&");
for (String arg : args)
{
idx = arg.indexOf("=");
if (idx > -1)
{
String key = arg.substring (0, idx);
if (!allowedSet.contains(key))
{
throw new IllegalArgumentException("Unexpected property '" + key + "'");
}
String val = arg.substring (idx + 1);
result.put (key, val);
}
else
{
throw new IllegalArgumentException("Could not parse argument " + arg +
". Expected key=val format");
}
}
return result;
}
public static InputStream getInputStream(String source) throws IOException {
URL url = new URL(source);
return getInputStream(url);
}
private static final int MS_CONNECTION_TIMEOUT = 2000;
//TODO: test when IOException is thrown
/**
* Start downloading a file from the web and open an InputStream to it.
* @param source location of file to download.
* @return InputStream
* @throws IOException after a number of attempts to connect to the remote server have failed.
*/
public static InputStream getInputStream(URL source) throws IOException {
InputStream stream = null;
int expCount = 0;
int timeOut = MS_CONNECTION_TIMEOUT;
while (true) { // multiple chances
try {
URLConnection uc = source.openConnection();
uc.setUseCaches(false); // don't use a cached page
uc.setConnectTimeout(timeOut); // set timeout for connection
stream = uc.getInputStream();
break;
} catch (IOException e) {
if (expCount++==4) {
throw(e);
} else {
timeOut *= 2;
}
}
}
return stream;
}
/**
* Generic method for multimaps, a map that can contain multiple values per key.
* Unlike a regular map, if you insert a value for a key that already exists, the previous value
* will not be discared.
* @param <T> key type of multimap
* @param <U> value type of multimap
* @param map multimap to work on
* @param key key of the value to insert
* @param val value to insert.
*/
public static <T, U> void multiMapPut(Map<T, Set<U>> map, T key, U val)
{
Set<U> set;
if (map.containsKey (key))
{
set = map.get(key);
}
else
{
set = new HashSet<U>();
map.put(key, set);
}
set.add (val);
}
/**
* Generic method for multimaps, a map that can contain multiple values per key.
* Unlike a regular map, if you insert a value for a key that already exists, the previous value
* will not be discarded.
* <p>
* This is like multiMapPut, but uses a list instead of a set for each value of the map.
* @param <T> key type of multimap
* @param <U> value type of multimap
* @param map multimap to work on
* @param key key of the value to insert
* @param val value to insert.
*/
public static <T, U> void multiMapAdd(Map<T, List<U>> map, T key, U val)
{
List<U> list;
if (map.containsKey (key))
{
list = map.get(key);
}
else
{
list = new ArrayList<U>();
map.put(key, list);
}
list.add (val);
}
/**
* Generic method for multimaps, a map that can contain multiple values per key.
* Unlike a regular map, if you insert a value for a key that already exists, the previous value
* will not be discarded.
* <p>
* multiMapPutAll let's you insert a collection of items at once.
* @param <T> key type of multimap
* @param <U> value type of multimap
* @param map multimap to work on
* @param key key of the value to insert
* @param vals values to insert.
*/
public static <T, U> void multiMapPutAll(Map<T, Set<U>> map, T key, Collection<U> vals)
{
Set<U> set;
if (map.containsKey (key))
{
set = map.get(key);
}
else
{
set = new HashSet<U>();
map.put(key, set);
}
set.addAll (vals);
}
/**
* Split a heterogeneous {@link Xref} set into multiple homogeneous Xref sets.
* <p>
* If the input contains {L:3643, L:1234, X:1004_at, X:1234_at},
* then the output will contain
* { L=> {L:3643, L:1234}, X=> {X:1004_at, X:1234_at} }.
* @param srcXrefs the set to split
* @return map with datasources as keys and homogeneous sets as values.
*/
public static Map<DataSource, Set<Xref>> groupByDataSource(Collection<Xref> srcXrefs)
{
Map<DataSource, Set<Xref>> result = new HashMap<DataSource, Set<Xref>>();
for (Xref ref : srcXrefs)
{
multiMapPut(result, ref.getDataSource(), ref);
}
return result;
}
/**
* Join the ID part of a collection of Xrefs with a custom separator.
* @param refs Xrefs from which the ids will be concatenated
* @param sep separator string.
* @return concatenation of the ids.
*/
public static String joinIds (Collection<Xref> refs, String sep)
{
boolean first = true;
StringBuilder builder = new StringBuilder();
for (Xref ref : refs)
{
if (!first) builder.append (sep);
builder.append (ref.getId());
first = false;
}
return builder.toString();
}
/** read a configuration file in the bridgedb xml format */
public static void readXmlConfig(InputSource is) throws ParserConfigurationException, SAXException, IOException
{
SAXParserFactory spf = SAXParserFactory.newInstance();
spf.setNamespaceAware(true);
SAXParser saxParser = spf.newSAXParser();
XMLReader xmlReader = saxParser.getXMLReader();
xmlReader.setContentHandler(new ConfigXmlHandler());
xmlReader.parse(is);
}
private static class ConfigXmlHandler extends DefaultHandler
{
DataSource current = null;
@Override
public void startElement(String namespaceURI, String localName,
String qName, Attributes atts)
throws SAXException
{
if ("datasource".equals (localName))
{
String fullname = atts.getValue("fullname");
if (fullname == null) throw new SAXException ("missing attribute fullname");
current = DataSource.getByFullName(fullname);
}
if ("alias".equals (localName))
{
String alias = atts.getValue ("name");
if (alias != null && current != null)
current.registerAlias(alias);
}
}
@Override
public void endElement(String namespaceURI, String localName, String qName) throws SAXException
{
if ("datasource".equals (localName))
{
current = null;
}
}
}
}