/** Copyright (C) SYSTAP, LLC DBA Blazegraph 2006-2016. All rights reserved. Contact: SYSTAP, LLC DBA Blazegraph 2501 Calvert ST NW #106 Washington, DC 20008 licenses@blazegraph.com 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; version 2 of the License. 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, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package com.bigdata.rdf.sparql.ast.service; import java.util.Collections; import java.util.Iterator; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.atomic.AtomicReference; import org.apache.http.conn.ClientConnectionManager; import org.eclipse.jetty.client.HttpClient; import org.openrdf.model.URI; import org.openrdf.model.impl.URIImpl; import com.bigdata.bop.join.BaseJoinStats; import com.bigdata.rdf.graph.impl.bd.GASService; import com.bigdata.rdf.sail.RDRHistoryServiceFactory; import com.bigdata.rdf.sparql.ast.QueryHints; import com.bigdata.rdf.sparql.ast.cache.DescribeServiceFactory; import com.bigdata.rdf.sparql.ast.eval.GeoSpatialServiceFactory; import com.bigdata.rdf.sparql.ast.eval.SampleServiceFactory; import com.bigdata.rdf.sparql.ast.eval.SearchInSearchServiceFactory; import com.bigdata.rdf.sparql.ast.eval.SearchServiceFactory; import com.bigdata.rdf.sparql.ast.eval.SliceServiceFactory; import com.bigdata.rdf.sparql.ast.eval.ValuesServiceFactory; import com.bigdata.rdf.sparql.ast.service.history.HistoryServiceFactory; import com.bigdata.rdf.store.AbstractTripleStore; import com.bigdata.rdf.store.BD; import com.bigdata.rdf.store.BDS; import com.bigdata.service.fts.FTS; import com.bigdata.service.fts.FulltextSearchServiceFactory; import com.bigdata.service.geospatial.GeoSpatial; import cutthecrap.utils.striterators.ReadOnlyIterator; /** * Registry for service calls. * * @see <a * href="https://sourceforge.net/apps/mediawiki/bigdata/index.php?title=FederatedQuery"> * Federated Query and Custom Services</a> */ public class ServiceRegistry { /** * TODO Allow SPI pattern for override? */ private static final ServiceRegistry DEFAULT = new ServiceRegistry(); static public ServiceRegistry getInstance() { return DEFAULT; } /** * Primary {@link ServiceFactory} registration. */ private final ConcurrentMap<URI, ServiceFactory> services; /** * Aliases for registered {@link ServiceFactory}s. */ private final ConcurrentMap<URI/* from */, URI/* to */> aliases; /** * The set of registered {@link ServiceFactory}s is also maintained here for * fast, safe iteration by {@link #services()}. */ private final CopyOnWriteArrayList<CustomServiceFactory> customServices; /** * The default {@link ServiceFactory} used for REMOTE SPARQL SERVICE end * points which are not otherwise registered. */ private AtomicReference<ServiceFactory> defaultServiceFactoryRef; /** * Allowed service whitelist. */ private final Set<String> serviceWhitelist = Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>()); /** * Whether service whitelisting is enabled. */ private boolean whitelistEnabled = false; protected ServiceRegistry() { services = new ConcurrentHashMap<URI, ServiceFactory>(); customServices = new CopyOnWriteArrayList<CustomServiceFactory>(); aliases = new ConcurrentHashMap<URI, URI>(); defaultServiceFactoryRef = new AtomicReference<ServiceFactory>( new RemoteServiceFactoryImpl(SPARQLVersion.SPARQL_11)); // Add the Bigdata search service. add(BDS.SEARCH, new SearchServiceFactory()); // Add the Geospatial search service. add(GeoSpatial.SEARCH, new GeoSpatialServiceFactory()); // Add the external Solr search service add(FTS.SEARCH, new FulltextSearchServiceFactory()); // Add the Bigdata search in search service. add(BDS.SEARCH_IN_SEARCH, new SearchInSearchServiceFactory()); // Add the sample index service. add(SampleServiceFactory.SERVICE_KEY, new SampleServiceFactory()); // Add the slice index service. add(SliceServiceFactory.SERVICE_KEY, new SliceServiceFactory()); // Add the values service. add(ValuesServiceFactory.SERVICE_KEY, new ValuesServiceFactory()); if (QueryHints.DEFAULT_DESCRIBE_CACHE) { add(new URIImpl(BD.NAMESPACE + "describe"), new DescribeServiceFactory()); } if (true) { /** * @see <a * href="https://sourceforge.net/apps/trac/bigdata/ticket/607"> * HISTORY SERVICE </a> */ add(new URIImpl(BD.NAMESPACE + "history"), new HistoryServiceFactory()); /** * Replacing with a history service using RDR instead of a custom * index. */ add(new URIImpl(BD.NAMESPACE + "rdrhistory"), new RDRHistoryServiceFactory()); } // The Gather-Apply-Scatter RDF Graph Mining service. add(GASService.Options.SERVICE_KEY, new GASService()); } /** * Set the default {@link ServiceFactory}. This will be used when the * serviceURI is not associated with an explicitly registered service. For * example, you can use this to control whether or not the service end point * is assumed to support <code>SPARQL 1.0</code> or <code>SPARQL 1.1</code>. * * @param defaultServiceFactory * The default {@link ServiceFactory}. * * @throws IllegalArgumentException * if the argument is <code>null</code>. */ public void setDefaultServiceFactory( final ServiceFactory defaultServiceFactory) { if (defaultServiceFactory == null) throw new IllegalArgumentException(); this.defaultServiceFactoryRef.set(defaultServiceFactory); } public ServiceFactory getDefaultServiceFactory() { return defaultServiceFactoryRef.get(); } /** * Register a service. * * @param serviceURI * The service URI. * @param factory * The factory to execute calls against that service. */ public final void add(final URI serviceURI, final ServiceFactory factory) { synchronized (this) { if (aliases.containsKey(serviceURI)) { throw new UnsupportedOperationException("Already declared."); } if (services.putIfAbsent(serviceURI, factory) != null) { throw new UnsupportedOperationException("Already declared."); } if (factory instanceof CustomServiceFactory) { customServices.add((CustomServiceFactory) factory); } } } /** * Remove a service from the registry and/or set of known aliases. * * @param serviceURI * The URI of the service -or- the URI of an alias registered * using {@link #addAlias(URI, URI)}. * * @return <code>true</code> iff a service for that URI was removed. */ public final boolean remove(final URI serviceURI) { boolean modified = false; synchronized (this) { if (aliases.remove(serviceURI) != null) { // removed an alias. modified = true; } // Remove the factory. final ServiceFactory factory = services.remove(serviceURI); if (factory != null) { modified = true; if(factory instanceof CustomServiceFactory) { customServices.remove(factory); } } } return modified; } /** * Register one URI as an alias for another. * * @param serviceURI * The URI of a service. It is expressly permitted to register an * alias for a URI which does not have a registered * {@link ServiceFactory}. This may be used to alias a remote URI * which you want to intercept locally. * @param aliasURI * The URI of an alias under which that service may be accessed. * @throws IllegalStateException * if the <i>serviceURI</i> has already been registered as a * alias (you must {@link #remove(URI)} the old alias before you * can map it against a different <i>serviceURI</i>). * @throws IllegalStateException * if the <i>aliasURI</i> has already been registered as a * service (you can not mask an existing service registration). */ public final void addAlias(final URI serviceURI, final URI aliasURI) { if (serviceURI == null) throw new IllegalArgumentException(); if (aliasURI == null) throw new IllegalArgumentException(); synchronized (this) { /* * Note: it is expressly permitted to register an alias for a URI * which does not have a registered ServiceFactory. This may be used * to alias a remote URI which you want to intercept locally. */ // // Lookup the service. // final ServiceFactory service = services.get(serviceURI); // // if (service == null) { // // throw new IllegalStateException("No such service: uri=" // + serviceURI); // // } if (services.containsKey(aliasURI)) { throw new IllegalStateException( "Alias already registered as service: uri=" + aliasURI); } if (aliases.containsKey(aliasURI)) { throw new IllegalStateException( "Alias already registered: uri=" + aliasURI); } aliases.put(aliasURI, serviceURI); } } /** * Add URL to service whitelist * @param URL the URL to add */ public void addWhitelistURL(String URL) { serviceWhitelist.add(URL); } /** * Remove URL to service whitelist * @param URL the URL to remove */ public void removeWhitelistURL(String URL) { serviceWhitelist.remove(URL); } /** * Set whitelist status. * @param enable true if enabled, false if disabled */ public void setWhitelistEnabled(boolean enable) { whitelistEnabled = enable; } /** * Check if whitelisting is enabled */ public boolean isWhitelistEnabled() { return whitelistEnabled; } /** * Return an {@link Iterator} providing a read-only view of the registered * {@link CustomServiceFactory}s. */ public Iterator<CustomServiceFactory> customServices() { /* * Note: This relies on the copy-on-write array list for fast and * efficient traversal with snapshot isolation. */ return new ReadOnlyIterator<CustomServiceFactory>( customServices.iterator()); } /** * Return the {@link ServiceFactory} for that URI. If the {@link URI} is a * known alias, then it is resolved before looking up the * {@link ServiceFactory}. * * @param serviceURI * The {@link URI}. * * @return The {@link ServiceFactory} if one is registered for that * {@link URI}. */ public ServiceFactory get(final URI serviceURI) { if (serviceURI == null) throw new IllegalArgumentException(); if (isWhitelistEnabled() && !serviceWhitelist.contains(serviceURI.stringValue())) { throw new IllegalArgumentException("Service URI " + serviceURI + " is not allowed"); } final URI alias = aliases.get(serviceURI); if (alias != null) { return services.get(alias); } return services.get(serviceURI); } /** * Resolve a {@link ServiceCall} for a service {@link URI}. If a * {@link ServiceFactory} was registered for that <i>serviceURI</i>, then it * will be returned. Otherwise {@link #getDefaultServiceFactory()} is used * to obtain the {@link ServiceFactory} that will be used to create the * {@link ServiceCall} object for that end point. * * @param store * The {@link AbstractTripleStore}. * @param cm * The {@link ClientConnectionManager} will be used to make * remote HTTP connections. * @param serviceURI * The as-bound {@link URI} of the service end point. * @param serviceNode * The AST model of the SERVICE clause. * * @return A {@link ServiceCall} for that service. */ public final ServiceCall<? extends Object> toServiceCall( final AbstractTripleStore store, final HttpClient cm, URI serviceURI, final ServiceNode serviceNode, final BaseJoinStats stats) { if (serviceURI == null) throw new IllegalArgumentException(); // Resolve URI in case it was an alias. final URI dealiasedServiceURI = aliases.get(serviceURI); if (dealiasedServiceURI != null) { // Use the de-aliased URI. serviceURI = dealiasedServiceURI; } if (isWhitelistEnabled() && !serviceWhitelist.contains(serviceURI.stringValue())) { throw new IllegalArgumentException("Service URI " + serviceURI + " is not allowed"); } ServiceFactory f = services.get(serviceURI); if (f == null) { f = getDefaultServiceFactory(); if (f == null) { // Should never be null at this point. throw new AssertionError(); } } final ServiceCallCreateParams params = new ServiceCallCreateParamsImpl( serviceURI, store, serviceNode, cm, f.getServiceOptions(), stats); return f.create(params); // return f.create(store, serviceURI, serviceNode); } /** * Maps a URI to a service factory. If the URI is null or there is no custom * service, the default service factory (SPARQL 1.1 service) is returned. * * @param serviceUri the URI for which we look up the service * @return the associated service factory or the default service factory * as fallback */ public ServiceFactory getServiceFactoryByServiceURI(URI serviceUri) { if (isWhitelistEnabled() && !serviceWhitelist.contains(serviceUri.stringValue())) { throw new IllegalArgumentException("Service URI " + serviceUri + " is not allowed"); } final ServiceFactory serviceFactory = serviceUri==null ? getDefaultServiceFactory() : services.get(serviceUri); return serviceFactory==null ? getDefaultServiceFactory() : serviceFactory; } private static class ServiceCallCreateParamsImpl implements ServiceCallCreateParams { private final URI serviceURI; private final AbstractTripleStore store; private final ServiceNode serviceNode; private final HttpClient cm; private final IServiceOptions serviceOptions; private final BaseJoinStats stats; public ServiceCallCreateParamsImpl(final URI serviceURI, final AbstractTripleStore store, final ServiceNode serviceNode, final HttpClient cm, final IServiceOptions serviceOptions, final BaseJoinStats stats) { this.serviceURI = serviceURI; this.store = store; this.serviceNode = serviceNode; this.cm = cm; this.serviceOptions = serviceOptions; this.stats = stats; } @Override public URI getServiceURI() { return serviceURI; } @Override public AbstractTripleStore getTripleStore() { return store; } @Override public ServiceNode getServiceNode() { return serviceNode; } @Override public HttpClient getClientConnectionManager() { return cm; } @Override public IServiceOptions getServiceOptions() { return serviceOptions; } @Override public BaseJoinStats getStats() { return stats; } @Override public String toString() { final StringBuilder sb = new StringBuilder(); sb.append(getClass().getName()); sb.append("{serviceURI=" + getServiceURI()); sb.append(",serviceNode=" + getServiceNode()); sb.append(",serviceOptions=" + getServiceOptions()); sb.append(",tripleStore=" + getTripleStore()); sb.append(",clientConnectionManager=" + getClientConnectionManager()); sb.append("}"); return sb.toString(); } } }