package uk.ac.ebi.fg.myequivalents.utils; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; import javax.persistence.EntityManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import uk.ac.ebi.fg.myequivalents.dao.ServiceDAO; import uk.ac.ebi.fg.myequivalents.model.EntityId; import uk.ac.ebi.fg.myequivalents.model.Service; import uk.ac.ebi.fg.myequivalents.resources.Const; /** * <p>This is a DB-specific implementation of {@link EntityIdResolver}. Namely, it overrides * {@link #resolveUri(String, String, String)}, to search a service by name, when it's received in its parameters.</p> * * <p>Note that this class performs searches of {@link Service} over the DB backend, which is the reason it * needs an {@link EntityManager} in its constructor. Moreover, all found services are cached in {@link #serviceCache}, * which is a static class member (so is life spans the VM/class loader in which this class is used). The cache * is synchronized, further synchronization is ensured by transaction management into the myEquivalent managers.</p> * * @author brandizi * <dl><dt>Date:</dt><dd>2 Jun 2015</dd> * */ public class DbEntityIdResolver extends EntityIdResolver { private ServiceDAO serviceDao; private static Map<String, Object> serviceCache; private Logger log = LoggerFactory.getLogger ( this.getClass () ); static { long ttl = Long.valueOf ( System.getProperty ( Const.PROP_NAME_CACHE_TIMEOUT_MIN, "30" ) ); Cache<String, Object> cache = CacheBuilder.newBuilder (). maximumSize ( 100000 ) .expireAfterWrite ( ttl, TimeUnit.MINUTES ) .build (); serviceCache = cache.asMap (); } public DbEntityIdResolver ( EntityManager entityManager ) { serviceDao = new ServiceDAO ( entityManager ); } /** * If the service name is defined, lookup for the corresponding pattern in the DB. Else, it searches all * services having a {@link Service#getUriPattern() URI pattern} matching this URI. If exactly one service is found, * builds up the URI using the accession, or verify the given URI (depending on parameter values). If no service * is found, or too many, returns an exception. * */ @Override public EntityId resolveUri ( String serviceName, String acc, String uri ) { Service service = null; boolean isUriAccVerified = false; if ( serviceName != null ) { // If it was specified, just verify it exists service = findServiceByName ( serviceName ); if ( service == null ) throw new RuntimeException ( String.format ( "Error: cannot find service '%s'", serviceName )); } else { // We aren't given any serviceName, first try with breakUri () and possibly get the URI pattern String uriPattern = EntityIdResolver.breakUri ( acc, uri ); // Now search based on uriPattern service = findServiceByUriPattern ( uriPattern ); if ( service == null ) { // No service found, so let's see if we can match more than one by means of URI domain-based search String uriDom = getDomain ( uri ); List<Service> services = findServicesByUriPatternLike ( uriDom + "%" ); // For each of the retrieved services, see if there is any that has a URI pattern compatible with the // parameter URI for ( Service servi: services ) { if ( acc != null ) { // Rebuild it when you have an accession String urii = buildUriFromAcc ( acc, servi.getUriPattern () ); if ( uri.equals ( urii ) ) { service = servi; isUriAccVerified = true; } } else { // Else try to extract the accession String acci = extractAccession ( uri, servi.getUriPattern () ); if ( acci != null ) { acc = acci; service = servi; isUriAccVerified = true; } } // If you found something, we're done. Ambiguity is left to the user. if ( service != null ) break; } if ( service == null ) // If you didn't find any service, then fall back to unspecified service. service = Service.UNSPECIFIED_SERVICE; } // if ( service == null ) } // if serviceName // We have an already URI-verified service, congratulations! Return the result if ( isUriAccVerified ) return new EntityId ( service, acc, uri ); // Else, let's see if the URI matches the pattern String uriPattern = service.getUriPattern (); if ( "$id".equals ( uriPattern ) ) acc = uri; else { if ( acc != null ) { // As above, rebuild the URI if you have an accession to do so String builtUri = EntityIdResolver.buildUriFromAcc ( acc, uriPattern ); if ( !builtUri.equals ( uri ) ) throw new RuntimeException ( String.format ( "Entity ID error the URI <%s> is incompatible with the service '%s' having URI pattern '%s'", uri, serviceName, uriPattern )); } else { // Else, try to rebuild the pattern from the URI and see if it matches the service's pattern. String rebuiltUriPattern = EntityIdResolver.breakUri ( uri ); if ( !uriPattern.equals ( rebuiltUriPattern ) ) throw new RuntimeException ( String.format ( "Entity ID error the URI <%s> seems incompatible with the service '%s' having URI pattern '%s'", uri, serviceName, uriPattern )); // If yes, user the pattern to extract the still-missing accession. acc = EntityIdResolver.extractAccession ( uri, uriPattern ); } } // if uriPattern != $id // Now the result should contains all of service, accession and URI. return new EntityId ( service, acc, uri ); } /** * Uses {@link #findServices(String, Callable)}, which adds up caching. */ private synchronized Service findServiceByName ( final String name ) { return findServices ( name, new Callable<Service>() { @Override public Service call () throws Exception { return serviceDao.findByName ( name, false ); } }); } /** * Uses {@link #findServices(String, Callable)}, which adds up caching. */ private synchronized Service findServiceByUriPattern ( final String uriPattern ) { return findServices ( uriPattern, new Callable<Service>() { @Override public Service call () throws Exception { List<Service> services = serviceDao.findByUriPattern ( uriPattern, false ); Iterator<Service> servicesItr = services.iterator (); if ( !servicesItr.hasNext () ) return null; Service result = servicesItr.next (); if ( servicesItr.hasNext () ) { // It does not make much sense to have different services mapped to the same pattern, we accept to store // such a case (because, for instance, you're interested only in building URLs from service+accession), but // we don't support URI resolution. log.warn ( "More than one service found for the URI pattern '{}', cannot resolve URIs for such cases", uriPattern ); return null; } return result; } }); } /** * Uses {@link #findServices(String, Callable)}, which adds up caching. */ private synchronized List<Service> findServicesByUriPatternLike ( final String uriPatternLike ) { return findServices ( uriPatternLike, new Callable<List<Service>>() { @Override public List<Service> call () throws Exception { return serviceDao.findByUriPatternLike ( uriPatternLike, false ); } }); } /** * This is a wrapper that caches DB results fetched by {@link #serviceDao}. * * In practice, it first lookup a T (which is supposed to be {@link Service} or a collection of services) * into {@link #serviceCache} and returns any non-null result, or, if the cache doesn't contain such key, * it searches it via finder, populate the case with non-null results, which is also returned. * */ private <T> T findServices ( String key, Callable<T> finder ) { try { synchronized ( key.intern () ) { @SuppressWarnings ( "unchecked" ) T result = (T) serviceCache.get ( key ); if ( result != null ) return result; result = finder.call (); if ( result != null ) serviceCache.put ( key, result ); return result; } } catch ( Exception ex ) { throw new RuntimeException ( String.format ( "Internal error while searching service '%s': %s", key, ex.getMessage ()), ex ); } } public void setEntityManager ( EntityManager entityManager ) { this.serviceDao.setEntityManager ( entityManager ); } }