RepositoryFinderSupport.java
- /*
- * *********************************************************************************************************************
- *
- * blueMarine II: Semantic Media Centre
- * http://tidalwave.it/projects/bluemarine2
- *
- * Copyright (C) 2015 - 2021 by Tidalwave s.a.s. (http://tidalwave.it)
- *
- * *********************************************************************************************************************
- *
- * 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.
- *
- * *********************************************************************************************************************
- *
- * git clone https://bitbucket.org/tidalwave/bluemarine2-src
- * git clone https://github.com/tidalwave-it/bluemarine2-src
- *
- * *********************************************************************************************************************
- */
- package it.tidalwave.bluemarine2.model.impl.catalog.finder;
- import javax.annotation.Nonnegative;
- import javax.annotation.Nonnull;
- import javax.inject.Inject;
- import java.util.Arrays;
- import java.util.ArrayList;
- import java.util.List;
- import java.util.Optional;
- import java.util.concurrent.atomic.AtomicInteger;
- import java.util.function.Function;
- import java.util.regex.Matcher;
- import java.util.regex.Pattern;
- import java.util.stream.Stream;
- import java.io.IOException;
- import java.io.InputStream;
- import org.eclipse.rdf4j.model.IRI;
- import org.eclipse.rdf4j.model.Value;
- import org.eclipse.rdf4j.model.impl.SimpleValueFactory;
- import org.eclipse.rdf4j.query.QueryLanguage;
- import org.eclipse.rdf4j.query.TupleQuery;
- import org.eclipse.rdf4j.query.TupleQueryResult;
- import org.eclipse.rdf4j.repository.Repository;
- import org.eclipse.rdf4j.repository.RepositoryConnection;
- import org.springframework.util.StreamUtils;
- import org.springframework.beans.factory.annotation.Configurable;
- import it.tidalwave.util.Id;
- import it.tidalwave.util.Finder;
- import it.tidalwave.util.LoggingUtilities;
- import it.tidalwave.util.ReflectionUtils;
- import it.tidalwave.util.Task;
- import it.tidalwave.util.spi.FinderSupport;
- import it.tidalwave.role.ContextManager;
- import it.tidalwave.bluemarine2.util.ImmutableTupleQueryResult;
- import it.tidalwave.bluemarine2.model.spi.CacheManager;
- import it.tidalwave.bluemarine2.model.spi.CacheManager.Cache;
- import it.tidalwave.bluemarine2.model.spi.SourceAwareFinder;
- import it.tidalwave.bluemarine2.model.impl.catalog.factory.RepositoryEntityFactory;
- import lombok.extern.slf4j.Slf4j;
- import lombok.EqualsAndHashCode;
- import lombok.Getter;
- import lombok.RequiredArgsConstructor;
- import lombok.Setter;
- import lombok.ToString;
- import static java.util.stream.Collectors.*;
- import static java.nio.charset.StandardCharsets.UTF_8;
- import static it.tidalwave.bluemarine2.util.RdfUtilities.streamOf;
- import static it.tidalwave.bluemarine2.model.vocabulary.BMMO.*;
- /***********************************************************************************************************************
- *
- * A base class for creating {@link Finder}s.
- *
- * @param <ENTITY> the entity the {@code Finder} should find
- * @param <FINDER> the subclass
- *
- * @stereotype Finder
- *
- * @author Fabrizio Giudici
- *
- **********************************************************************************************************************/
- @Configurable @Slf4j
- public class RepositoryFinderSupport<ENTITY, FINDER extends Finder<ENTITY>>
- extends FinderSupport<ENTITY, FINDER>
- implements SourceAwareFinder<ENTITY, FINDER>
- {
- private static final String REGEX_BINDING_TAG = "^@([A-Za-z0-9]*)@";
- private static final String REGEX_BINDING_TAG_LINE = REGEX_BINDING_TAG + ".*$";
- private static final String REGEX_COMMENT = "^ *#.*";
- private static final String PREFIXES = "PREFIX foaf: <http://xmlns.com/foaf/0.1/>\n"
- + "PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>\n"
- + "PREFIX rel: <http://purl.org/vocab/relationship/>\n"
- + "PREFIX bmmo: <http://bluemarine.tidalwave.it/2015/04/mo/>\n"
- + "PREFIX mo: <http://purl.org/ontology/mo/>\n"
- + "PREFIX vocab: <http://dbtune.org/musicbrainz/resource/vocab/>\n"
- + "PREFIX xs: <http://www.w3.org/2001/XMLSchema#>\n";
- private static final String QUERY_COUNT_HOLDER = "queryCount";
- private static final long serialVersionUID = 1896412264314804227L;
- private static final SimpleValueFactory FACTORY = SimpleValueFactory.getInstance();
- @Nonnull
- protected final transient Repository repository;
- @Nonnull
- private final Class<ENTITY> entityClass;
- @Nonnull
- private final String idName;
- @Nonnull
- private final transient Optional<Id> id;
- @Nonnull
- private final transient Optional<Value> source;
- @Nonnull
- private final transient Optional<Value> sourceFallback;
- @Inject
- private transient ContextManager contextManager;
- @Inject
- private transient RepositoryEntityFactory entityFactory;
- @Inject
- private transient CacheManager cacheManager;
- // FIXME: move to a stats bean
- private static final AtomicInteger queryCount = new AtomicInteger();
- @Getter @Setter
- private static boolean dumpThreadOnQuery = false;
- /*******************************************************************************************************************
- *
- *
- *
- ******************************************************************************************************************/
- @RequiredArgsConstructor(staticName = "withSparql") @EqualsAndHashCode @ToString
- protected static class QueryAndParameters
- {
- @Getter @Nonnull
- private final String sparql;
- @Nonnull
- private final List<Object> parameters = new ArrayList<>();
- @Nonnull
- public QueryAndParameters withParameter (@Nonnull final String name, @Nonnull final Optional<? extends Value> value)
- {
- return value.map(v -> withParameter(name, v)).orElse(this);
- }
- @Nonnull
- public QueryAndParameters withParameter (@Nonnull final String name, @Nonnull final Value value)
- {
- parameters.addAll(List.of(name, value));
- return this;
- }
- @Nonnull
- public Object[] getParameters()
- {
- return parameters.toArray();
- }
- @Nonnull
- private String getCountSparql()
- {
- return String.format("SELECT (COUNT(*) AS ?%s)%n {%n%s%n }",
- QUERY_COUNT_HOLDER,
- sparql.replaceAll("ORDER BY[\\s\\S]*", ""));
- }
- }
- /*******************************************************************************************************************
- *
- *
- *
- ******************************************************************************************************************/
- protected RepositoryFinderSupport (@Nonnull final Repository repository, @Nonnull final String idName)
- {
- this.repository = repository;
- this.entityClass = (Class<ENTITY>)ReflectionUtils.getTypeArguments(RepositoryFinderSupport.class, getClass()).get(0);
- this.idName = idName;
- this.id = Optional.empty();
- this.source = Optional.of(O_SOURCE_EMBEDDED); // FIXME: resets
- this.sourceFallback = Optional.empty(); // FIXME: resets
- }
- /*******************************************************************************************************************
- *
- *
- *
- ******************************************************************************************************************/
- private RepositoryFinderSupport (@Nonnull final Repository repository,
- @Nonnull final Class<ENTITY> entityClass,
- @Nonnull final String idName,
- @Nonnull final Optional<Id> id,
- @Nonnull final Optional<Value> source,
- @Nonnull final Optional<Value> sourceFallback)
- {
- this.repository = repository;
- this.entityClass = entityClass;
- this.idName = idName;
- this.id = id;
- this.source = source;
- this.sourceFallback = sourceFallback;
- }
- /*******************************************************************************************************************
- *
- * Clone constructor.
- *
- ******************************************************************************************************************/
- public RepositoryFinderSupport (@Nonnull final RepositoryFinderSupport<ENTITY, FINDER> other,
- @Nonnull final Object override)
- {
- super(other, override);
- final RepositoryFinderSupport<ENTITY, FINDER> source = getSource(RepositoryFinderSupport.class, other, override);
- this.repository = source.repository;
- this.entityClass = source.entityClass;
- this.idName = source.idName;
- this.id = source.id;
- this.source = source.source;
- this.sourceFallback = source.sourceFallback;
- }
- /*******************************************************************************************************************
- *
- * {@inheritDoc}
- *
- ******************************************************************************************************************/
- @Override @Nonnull
- protected final List<? extends ENTITY> computeNeededResults()
- {
- return query(QueryAndParameters::getSparql,
- result -> createEntities(repository, entityClass, result),
- result -> String.format("%d entities", result.size()));
- }
- /*******************************************************************************************************************
- *
- * {@inheritDoc}
- *
- ******************************************************************************************************************/
- @Override @Nonnegative
- public int count()
- {
- return query(QueryAndParameters::getCountSparql,
- result -> Integer.parseInt(result.next().getValue(QUERY_COUNT_HOLDER).stringValue()),
- result -> String.format("%d", result));
- }
- /*******************************************************************************************************************
- *
- * {@inheritDoc}
- *
- ******************************************************************************************************************/
- @Override @Nonnull
- public FINDER withId (@Nonnull final Id id)
- {
- return clonedWith(new RepositoryFinderSupport(repository,
- entityClass,
- idName,
- Optional.of(id),
- source,
- sourceFallback));
- }
- /*******************************************************************************************************************
- *
- * {@inheritDoc}
- *
- ******************************************************************************************************************/
- @Override @Nonnull
- public FINDER importedFrom (@Nonnull final Optional<Id> optionalSource)
- {
- return optionalSource.map(this::importedFrom).orElse((FINDER)this);
- }
- /*******************************************************************************************************************
- *
- * {@inheritDoc}
- *
- ******************************************************************************************************************/
- @Override @Nonnull
- public FINDER importedFrom (@Nonnull final Id source)
- {
- return clonedWith(new RepositoryFinderSupport(repository,
- entityClass,
- idName,
- id,
- Optional.of(FACTORY.createLiteral(source.toString())),
- sourceFallback));
- }
- /*******************************************************************************************************************
- *
- * {@inheritDoc}
- *
- ******************************************************************************************************************/
- @Override @Nonnull
- public FINDER withFallback (@Nonnull final Optional<Id> sourceFallback)
- {
- return sourceFallback.map(this::withFallback).orElse((FINDER)this);
- }
- /*******************************************************************************************************************
- *
- * {@inheritDoc}
- *
- ******************************************************************************************************************/
- @Override @Nonnull
- public FINDER withFallback (@Nonnull final Id sourceFallback)
- {
- return clonedWith(new RepositoryFinderSupport(repository,
- entityClass,
- idName,
- id,
- source,
- Optional.of(FACTORY.createLiteral(sourceFallback.toString()))));
- }
- /*******************************************************************************************************************
- *
- * Returns the count of queries performed so far.
- *
- * @return the count of queries
- *
- ******************************************************************************************************************/
- @Nonnegative
- public static int getQueryCount()
- {
- return queryCount.intValue();
- }
- /*******************************************************************************************************************
- *
- * Resets the count of queries performed so far.
- *
- ******************************************************************************************************************/
- public static void resetQueryCount()
- {
- queryCount.set(0);
- }
- /*******************************************************************************************************************
- *
- * Prepares the SPARQL query and its parameters.
- *
- * @return the SPARQL query and its parameters
- *
- ******************************************************************************************************************/
- @Nonnull
- protected /* abstract */ QueryAndParameters prepareQuery()
- {
- throw new UnsupportedOperationException("Must be implemented by subclasses");
- }
- /*******************************************************************************************************************
- *
- * Performs a query, eventually using the cache.
- *
- * @param sparqlSelector a function that select the SPARQL statement to use
- * @param finalizer a function to transform the query raw result into the final result
- * @param resultToString a function that provide the logging string for the result
- * @return the found entities
- *
- ******************************************************************************************************************/
- @Nonnull
- private <E> E query (@Nonnull final Function<QueryAndParameters, String> sparqlSelector,
- @Nonnull final Function<TupleQueryResult, E> finalizer,
- @Nonnull final Function<E, String> resultToString)
- {
- log.info("query() - {}", entityClass);
- final long baseTime = System.nanoTime();
- final QueryAndParameters queryAndParameters = prepareQuery()
- .withParameter(idName, id.map(this::iriFor))
- .withParameter("source", source)
- .withParameter("fallback", sourceFallback.equals(source) ? Optional.empty() : sourceFallback);
- final Object[] parameters = queryAndParameters.getParameters();
- final String originalSparql = sparqlSelector.apply(queryAndParameters);
- final String sparql = PREFIXES + Stream.of(originalSparql.split("\n"))
- .filter(s -> matchesTag(s, parameters))
- .map(s -> s.replaceAll(REGEX_BINDING_TAG, ""))
- .collect(joining("\n"));
- log(originalSparql, sparql, parameters);
- final E result = query(sparql, finalizer, parameters);
- queryCount.incrementAndGet();
- final long elapsedTime = System.nanoTime() - baseTime;
- log.info(">>>> query returned {} in {} msec", resultToString.apply(result), elapsedTime / 1E6);
- LoggingUtilities.dumpStack(this, dumpThreadOnQuery);
- return result;
- }
- /*******************************************************************************************************************
- *
- * Performs a query.
- *
- * @param sparql the SPARQL of the query
- * @param finalizer a function to transform the query raw result into the final result
- * @param parameters an optional set of parameters of the query ("name", value, "name", value ,,,)
- * @return the found entities
- *
- ******************************************************************************************************************/
- @Nonnull
- private <R> R query (@Nonnull final String sparql,
- @Nonnull final Function<TupleQueryResult, R> finalizer,
- @Nonnull final Object ... parameters)
- {
- try (final RepositoryConnection connection = repository.getConnection())
- {
- final TupleQuery query = connection.prepareTupleQuery(QueryLanguage.SPARQL, sparql);
- for (int i = 0; i < parameters.length; i += 2)
- {
- query.setBinding((String)parameters[i], (Value)parameters[i + 1]);
- }
- //
- // Don't cache entities because they are injected with DCI roles in function of the context.
- // Caching tuples is safe.
- final Cache cache = cacheManager.getCache(RepositoryFinderSupport.class);
- final String key = String.format("%s # %s", compacted(sparql), Arrays.toString(parameters));
- try (final ImmutableTupleQueryResult result = cache.getCachedObject(key,
- () -> new ImmutableTupleQueryResult(query.evaluate())))
- {
- // ImmutableTupleQueryResult is not thread safe, so clone the cached instance
- return finalizer.apply(new ImmutableTupleQueryResult(result));
- }
- }
- }
- /*******************************************************************************************************************
- *
- * Facility method that creates an {@link IRI} given an {@link Id}.
- *
- * @param id the {@code Id}
- * @return the {@code IRI}
- *
- ******************************************************************************************************************/
- @Nonnull
- protected Value iriFor (@Nonnull final Id id)
- {
- return FACTORY.createIRI(id.stringValue());
- }
- /*******************************************************************************************************************
- *
- *
- ******************************************************************************************************************/
- @Nonnull
- protected Value literalFor (final boolean b)
- {
- return FACTORY.createLiteral(b);
- }
- /*******************************************************************************************************************
- *
- * Reads a SPARQL statement from a named resource
- *
- * @param clazz the reference class
- * @param name the resource name
- * @return the SPARQL statement
- *
- ******************************************************************************************************************/
- @Nonnull
- protected static String readSparql (@Nonnull final Class<?> clazz, @Nonnull final String name)
- {
- try (final InputStream is = clazz.getResourceAsStream(name))
- {
- return Stream.of(StreamUtils.copyToString(is, UTF_8)
- .split("\n"))
- .filter(s -> !s.matches(REGEX_COMMENT))
- .collect(joining("\n"));
- }
- catch (IOException e)
- {
- throw new RuntimeException(e);
- }
- }
- /*******************************************************************************************************************
- *
- * Instantiates an entity for each given {@link TupleQueryResult}. Entities are instantiated in the DCI contents
- * associated to this {@link Finder} - see {@link #getContexts()}.
- *
- * @param <E> the static type of the entities to instantiate
- * @param repository the repository we're querying
- * @param entityClass the dynamic type of the entities to instantiate
- * #param queryResult the {@code TupleQueryResult}
- * @return the instantiated entities
- *
- ******************************************************************************************************************/
- @Nonnull
- private <E> List<E> createEntities (@Nonnull final Repository repository,
- @Nonnull final Class<E> entityClass,
- @Nonnull final TupleQueryResult queryResult)
- {
- return contextManager.runWithContexts(getContexts(), new Task<>()
- {
- @Override @Nonnull
- public List<E> run()
- {
- return streamOf(queryResult)
- .map(bindingSet -> entityFactory.createEntity(repository, entityClass, bindingSet))
- .collect(toList());
- }
- });
- // TODO: requires TheseFoolishThings 3.1-ALPHA-3
- // return contextManager.runWithContexts(getContexts(), () -> streamOf(queryResult)
- // .map(bindingSet -> entityFactory.createEntity(repository, entityClass, bindingSet))
- // .collect(toList()));
- }
- /*******************************************************************************************************************
- *
- * Returns {@code true} if the given string contains a binding tag (in the form {@code @TAG@}) that matches one
- * of the bindings; or if there are no binding tags. This is used as a filter to eliminate portions of SPARQL
- * queries that don't match any binding.
- *
- * @param string the string
- * @param bindings the bindings
- * @return {@code true} if there is a match
- *
- ******************************************************************************************************************/
- private static boolean matchesTag (@Nonnull final String string, @Nonnull final Object[] bindings)
- {
- final Pattern patternBindingTagLine = Pattern.compile(REGEX_BINDING_TAG_LINE);
- final Matcher matcher = patternBindingTagLine.matcher(string);
- if (!matcher.matches())
- {
- return true;
- }
- final String tag = matcher.group(1);
- for (int i = 0; i < bindings.length; i+= 2)
- {
- if (tag.equals(bindings[i]))
- {
- return true;
- }
- }
- return false;
- }
- /*******************************************************************************************************************
- *
- * Logs the query at various detail levels.
- *
- * @param originalSparql the original SPARQL statement
- * @param sparql the SPARQL statement after binding tag filtering
- * @param bindings the bindings
- *
- ******************************************************************************************************************/
- private void log (@Nonnull final String originalSparql,
- @Nonnull final String sparql,
- @Nonnull final Object ... bindings)
- {
- if (log.isTraceEnabled())
- {
- Stream.of(originalSparql.split("\n")).forEach(s -> log.trace(">>>> original query: {}", s));
- }
- if (log.isDebugEnabled())
- {
- Stream.of(sparql.split("\n")).forEach(s -> log.debug(">>>> query: {}", s));
- }
- if (!log.isDebugEnabled() && log.isInfoEnabled())
- {
- log.info(">>>> query: {}", compacted(sparql));
- }
- if (log.isInfoEnabled())
- {
- log.info(">>>> query parameters: {}", Arrays.toString(bindings));
- }
- }
- /*******************************************************************************************************************
- *
- *
- ******************************************************************************************************************/
- @Nonnull
- private static String compacted (@Nonnull final String sparql)
- {
- return sparql.replace("\n", " ").replaceAll("\\s+", " ").trim();
- }
- }