LookupService.java
/*
* Copyright (c) 2007-2026 MetaSolutions AB
*
* 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.entrystore.rest.springboot.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.solr.client.solrj.request.SolrQuery;
import org.apache.solr.client.solrj.util.ClientUtils;
import org.apache.solr.common.SolrException;
import org.eclipse.rdf4j.model.Model;
import org.eclipse.rdf4j.model.impl.LinkedHashModel;
import org.entrystore.Context;
import org.entrystore.Entry;
import org.entrystore.EntryType;
import org.entrystore.Metadata;
import org.entrystore.SearchIndex;
import org.entrystore.impl.RepositoryManagerImpl;
import org.entrystore.repository.util.QueryResult;
import org.entrystore.repository.util.SolrSearchIndex;
import org.entrystore.rest.springboot.model.api.LookupScope;
import org.entrystore.rest.springboot.model.exception.BadRequestException;
import org.entrystore.rest.springboot.model.exception.CustomResponseException;
import org.entrystore.rest.springboot.model.exception.EntityNotFoundException;
import org.entrystore.rest.springboot.util.GraphUtil;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import java.net.URI;
import java.util.Objects;
import java.util.Set;
@Slf4j
@Service
@RequiredArgsConstructor
public class LookupService {
private final RepositoryManagerImpl repositoryManager;
private final ContextService contextService;
public Entry lookupGlobal(URI resourceURI) {
SearchIndex index = repositoryManager.getIndex();
if (index == null) {
throw new CustomResponseException("Solr search is deactivated", HttpStatus.SERVICE_UNAVAILABLE);
}
if (!(index instanceof SolrSearchIndex solrIndex)) {
log.error("Lookup requires SolrSearchIndex but found: {}", index.getClass().getName());
throw new CustomResponseException("Lookup is not available: search index is not properly configured", HttpStatus.SERVICE_UNAVAILABLE);
}
String escapedURI = ClientUtils.escapeQueryChars(resourceURI.toString());
SolrQuery q = new SolrQuery("resource:" + escapedURI + " AND public:true");
q.setStart(0);
q.setRows(1);
QueryResult qResult;
try {
qResult = solrIndex.sendQuery(q);
} catch (SolrException se) {
log.warn("SolrException during lookup for URI '{}' (code: {}): {}", resourceURI, se.code(), se.getMessage());
if (se.code() >= 500) {
throw new CustomResponseException("Search service error during lookup", HttpStatus.SERVICE_UNAVAILABLE);
}
throw new BadRequestException("Lookup failed due to invalid parameters", se);
} catch (Exception e) {
log.error("Unexpected error during lookup for URI '{}': {}", resourceURI, e.getMessage(), e);
throw new CustomResponseException("Search service encountered an unexpected error", HttpStatus.SERVICE_UNAVAILABLE);
}
// sendQueryForEntryURIs sets hits = -1 when SolrServerException/IOException occurs
if (qResult.getHits() == -1) {
log.error("Solr query returned error sentinel (hits=-1) for URI '{}'; search backend may be unavailable", resourceURI);
throw new CustomResponseException("Search service encountered an error during lookup", HttpStatus.SERVICE_UNAVAILABLE);
}
return extractSingleEntry(qResult.getEntries(), resourceURI);
}
public Entry lookupInContext(String contextId, URI resourceURI) {
Context context = contextService.getContextOrThrow(contextId);
Set<Entry> entries = context.getByResourceURI(resourceURI);
return extractSingleEntry(entries, resourceURI);
}
public String getMetadataByScope(Entry entry, LookupScope scope, String mediaType) {
Model graph = new LinkedHashModel();
EntryType entryType = entry.getEntryType();
if (EntryType.Local.equals(entryType) || EntryType.Link.equals(entryType)) {
if (scope == LookupScope.ALL || scope == LookupScope.LOCAL) {
addMetadataGraph(graph, entry.getLocalMetadata(), "local", entry);
}
} else if (EntryType.Reference.equals(entryType)) {
if (scope == LookupScope.ALL || scope == LookupScope.EXTERNAL) {
addMetadataGraph(graph, entry.getCachedExternalMetadata(), "cached external", entry);
}
} else if (EntryType.LinkReference.equals(entryType)) {
if (scope == LookupScope.ALL || scope == LookupScope.LOCAL) {
addMetadataGraph(graph, entry.getLocalMetadata(), "local", entry);
}
if (scope == LookupScope.ALL || scope == LookupScope.EXTERNAL) {
addMetadataGraph(graph, entry.getCachedExternalMetadata(), "cached external", entry);
}
}
if (graph.isEmpty() && scope != LookupScope.ALL) {
log.info("Scope '{}' produced no metadata for entry type '{}' (resource URI: {})",
scope, entryType, entry.getResourceURI());
}
return GraphUtil.serializeGraph(graph, mediaType);
}
private Entry extractSingleEntry(Set<Entry> entries, URI resourceURI) {
if (entries == null || entries.isEmpty()) {
throw new EntityNotFoundException("No entry found for resource URI: " + resourceURI);
}
entries.removeIf(Objects::isNull);
if (entries.isEmpty()) {
log.warn("All entries resolved to null for resource URI: {}", resourceURI);
throw new EntityNotFoundException("No entry found for resource URI: " + resourceURI);
}
if (entries.size() > 1) {
log.warn("Multiple entries found for resource URI: {}", resourceURI);
}
return entries.iterator().next();
}
private void addMetadataGraph(Model target, Metadata metadata, String metadataKind, Entry entry) {
if (metadata == null) {
log.warn("Expected {} metadata is null for entry '{}' (type: {})",
metadataKind, entry.getEntryURI(), entry.getEntryType());
return;
}
Model mdGraph = metadata.getGraph();
if (mdGraph == null) {
log.warn("{} metadata graph is null for entry '{}' (type: {})",
metadataKind, entry.getEntryURI(), entry.getEntryType());
return;
}
target.addAll(mdGraph);
}
}