org.fcrepo.apix.jena.impl.JenaServiceRegistry.java Source code

Java tutorial

Introduction

Here is the source code for org.fcrepo.apix.jena.impl.JenaServiceRegistry.java

Source

/*
 * Licensed to DuraSpace under one or more contributor license agreements.
 * See the NOTICE file distributed with this work for additional information
 * regarding copyright ownership.
 *
 * DuraSpace licenses this file to you 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.fcrepo.apix.jena.impl;

import static java.nio.charset.StandardCharsets.UTF_8;
import static org.fcrepo.apix.jena.Util.objectResourceOf;
import static org.fcrepo.apix.jena.Util.objectResourcesOf;
import static org.fcrepo.apix.jena.Util.parse;
import static org.fcrepo.apix.jena.Util.subjectOf;
import static org.fcrepo.apix.model.Ontologies.RDF_TYPE;
import static org.fcrepo.apix.model.Ontologies.Service.CLASS_SERVICE;
import static org.fcrepo.apix.model.Ontologies.Service.CLASS_SERVICE_INSTANCE;
import static org.fcrepo.apix.model.Ontologies.Service.PROP_CANONICAL;
import static org.fcrepo.apix.model.Ontologies.Service.PROP_CONTAINS_SERVICE;
import static org.fcrepo.apix.model.Ontologies.Service.PROP_HAS_ENDPOINT;
import static org.fcrepo.apix.model.Ontologies.Service.PROP_HAS_SERVICE_INSTANCE;
import static org.fcrepo.apix.model.Ontologies.Service.PROP_HAS_SERVICE_INSTANCE_REGISTRY;
import static org.fcrepo.apix.model.Ontologies.Service.PROP_IS_SERVICE_INSTANCE_OF;

import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.fcrepo.apix.jena.JenaResource;
import org.fcrepo.apix.jena.Util;
import org.fcrepo.apix.model.Service;
import org.fcrepo.apix.model.ServiceInstance;
import org.fcrepo.apix.model.components.Initializer;
import org.fcrepo.apix.model.components.Initializer.Initialization;
import org.fcrepo.apix.model.components.Registry;
import org.fcrepo.apix.model.components.ResourceNotFoundException;
import org.fcrepo.apix.model.components.ServiceInstanceRegistry;
import org.fcrepo.apix.model.components.ServiceRegistry;
import org.fcrepo.apix.model.components.Updateable;

import org.apache.commons.io.IOUtils;
import org.apache.http.HttpHeaders;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPatch;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.entity.InputStreamEntity;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.apache.jena.rdf.model.Model;
import org.apache.jena.rdf.model.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Jena-based service registry implementation,
 *
 * @author apb@jhu.edu
 */
public class JenaServiceRegistry extends WrappingRegistry implements ServiceRegistry, Updateable {

    private URI registryContainer;

    private CloseableHttpClient client;

    private Initializer initializer;

    private Initialization init = Initialization.NONE;

    private static final Logger LOG = LoggerFactory.getLogger(JenaServiceRegistry.class);

    private static final String SPARQL_UPDATE = "application/sparql-update";

    /**
     * Set the Fedora client.
     *
     * @param client the client
     */
    public void setHttpClient(final CloseableHttpClient client) {
        this.client = client;
    }

    /**
     * Set the LDP container corresponding to this service registry.
     *
     * @param container the container;
     */
    public void setRegistryContainer(final URI container) {
        registryContainer = container;
    }

    /** Set the initializer. */
    public void setInitializer(final Initializer initializer) {
        this.initializer = initializer;
    }

    @Override
    public void setRegistryDelegate(final Registry delegate) {
        super.setRegistryDelegate(delegate);
    }

    // Index mapping canonical URI to resource URI
    private final ConcurrentHashMap<URI, URI> canonicalUriMap = new ConcurrentHashMap<>();

    /** Initial update and re-indexing. */
    public void init() {
        init = initializer.initialize(() -> {
            update();
        });
    }

    /** Shutdown */
    public void shutdown() {
        init.cancel();
    }

    @Override
    public void update() {

        // For all resources in the registry, get the URIs of everything that calls itself a Service, or is explicitly
        // registered as a service

        final Set<URI> serviceURIs = Stream.concat(super.list().stream().map(this::get).map(Util::parse)
                .flatMap(m -> m.listSubjectsWithProperty(m.getProperty(RDF_TYPE), m.getResource(CLASS_SERVICE))
                        .mapWith(Resource::getURI).toSet().stream().map(URI::create)),
                objectResourcesOf(null, PROP_CONTAINS_SERVICE, parse(this.get(registryContainer))).stream())
                .collect(Collectors.toSet());

        // Map canonical URI to service resource. If multiple service resources
        // indicate the same canonical URI, pick one arbitrarily.
        final Map<URI, URI> canonical = serviceURIs.stream().flatMap(this::attemptLookupService)
                .collect(Collectors.toMap(s -> s.canonicalURI(), s -> s.uri(), (a, b) -> a));

        canonicalUriMap.putAll(canonical);

        canonicalUriMap.keySet().removeIf(k -> !canonical.containsKey(k));
    }

    @Override
    public void update(final URI uri) {
        if (hasInDomain(uri) && uri.getFragment() == null) {
            // TODO: This can be optimized more. Right now, it re-scans all services,
            // but at least filters out obvious redundancies (hash URIs) or inapplicable resources
            update();
        }
    }

    @Override
    public void register(final URI uri) {
        init.await();
        try {
            LOG.debug("Registering service {} ", uri);

            final HttpPatch patch = new HttpPatch(registryContainer);
            patch.setHeader(HttpHeaders.CONTENT_TYPE, SPARQL_UPDATE);
            patch.setEntity(new InputStreamEntity(patchAddService(uri)));

            try (CloseableHttpResponse resp = execute(patch)) {
                LOG.info("Adding service {} to registry {}", uri, registryContainer);
            }
        } catch (final Exception e) {
            throw new RuntimeException(
                    String.format("Could not add <%s> to service registry <%s>", uri, registryContainer), e);
        }

        update(uri);
    }

    private InputStream patchAddService(final URI service) {
        init.await();
        try {
            return IOUtils.toInputStream(
                    String.format("INSERT {<> <%s> <%s> .} WHERE {}", PROP_CONTAINS_SERVICE, service), "utf8");
        } catch (final IOException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public ServiceInstanceRegistry createInstanceRegistry(final Service service) {
        init.await();
        LOG.debug("POST: Creating service instance registry");

        final HttpPost post = new HttpPost(service.uri());
        post.setHeader(HttpHeaders.CONTENT_TYPE, "text/turtle");
        post.setEntity(new InputStreamEntity(
                this.getClass().getResourceAsStream("objects/service-instance-registry.ttl")));

        final URI uri;
        try (CloseableHttpResponse resp = execute(post)) {
            uri = URI.create(resp.getFirstHeader(HttpHeaders.LOCATION).getValue());
        } catch (final Exception e) {
            throw new RuntimeException("Could not create service instance registry", e);
        }

        final HttpPatch patch = new HttpPatch(uri);
        patch.setHeader(HttpHeaders.CONTENT_TYPE, SPARQL_UPDATE);
        patch.setEntity(new StringEntity(String.format("INSERT {?instance <%s> <%s> .} WHERE {?instance a <%s> .}",
                PROP_IS_SERVICE_INSTANCE_OF, service.uri(), CLASS_SERVICE_INSTANCE), UTF_8));

        try (CloseableHttpResponse resp = execute(patch)) {
            LOG.info("Updating instance registry for {}", service.uri());
        } catch (final Exception e) {
            throw new RuntimeException("Could not update service instance registry", e);
        }

        return instancesOf(getService(service.uri()));
    }

    @Override
    public ServiceInstanceRegistry instancesOf(final Service service) {
        init.await();
        final URI registryURI = objectResourceOf(service.uri().toString(), PROP_HAS_SERVICE_INSTANCE_REGISTRY,
                parse(service));

        if (registryURI == null) {
            throw new ResourceNotFoundException("No service instance registry found for service " + service.uri());
        }

        // TODO: Allow this to be pluggable to different service instance registry implementations

        final Model registry = getRegistry(registryURI, service);

        return new ServiceInstanceRegistry() {

            @Override
            public List<ServiceInstance> instances() {
                return objectResourcesOf(registryURI.toString(), PROP_HAS_SERVICE_INSTANCE, registry).stream()
                        .map(uri -> new LdpServiceInstanceImpl(uri, service)).collect(Collectors.toList());
            }

            @Override
            public URI addEndpoint(final URI endpoint) {
                LOG.debug("PATCH: Adding endpoint <{}> to <{}>", endpoint, registryURI);

                final HttpPatch patch = new HttpPatch(registryURI);
                patch.setHeader(HttpHeaders.CONTENT_TYPE, SPARQL_UPDATE);
                patch.setEntity(
                        new StringEntity(String.format("INSERT {?instance <%s> <%s> .} WHERE {?instance a <%s> .}",
                                PROP_HAS_ENDPOINT, endpoint, CLASS_SERVICE_INSTANCE), UTF_8));

                try (CloseableHttpResponse resp = execute(patch)) {
                    LOG.info("Adding endpoint <{}> to <{}>", endpoint, registryURI);
                } catch (final Exception e) {
                    throw new RuntimeException(
                            String.format("Failed adding endpoint <%s> to <%s>", endpoint, registryURI), e);
                }

                return registryURI;
            }
        };
    }

    class LdpServiceInstanceImpl implements ServiceInstance {

        final Model model;

        final String uri;

        final Service service;

        public LdpServiceInstanceImpl(final URI uri, final Service service) {
            model = parse(get(uri));
            this.uri = uri.toString();
            this.service = service;
        }

        @Override
        public List<URI> endpoints() {
            return objectResourcesOf(uri, PROP_HAS_ENDPOINT, model);
        }

        @Override
        public Service instanceOf() {
            return service;
        }
    }

    @Override
    public Service getService(final URI uri) {
        init.await();
        return new ServiceImpl(uri);
    }

    @Override
    public Collection<URI> list() {
        init.await();
        return new HashSet<>(canonicalUriMap.values());
    }

    @Override
    public boolean contains(final URI uri) {
        init.await();
        return canonicalUriMap.containsKey(uri) || canonicalUriMap.containsValue(uri);
    }

    class ServiceImpl extends WrappingResource implements Service, JenaResource {

        final Model model;

        final URI uri;

        ServiceImpl(final URI uri) {
            super(get(resourceURI(uri)));
            this.model = parse(this);

            // Sanity check - verify that the uri is a service. If not, and if there is exactly one service, use that.
            if (!model.contains(model.getResource(uri.toString()), model.getProperty(RDF_TYPE),
                    model.getResource(CLASS_SERVICE))) {
                try {
                    this.uri = subjectOf(RDF_TYPE, CLASS_SERVICE, model);
                } catch (final ResourceNotFoundException e) {
                    throw new RuntimeException(
                            String.format("Error performing sanity check on service impl <%s>", uri), e);
                }
            } else {
                this.uri = uri;
            }
        }

        @Override
        public URI canonicalURI() {

            final List<URI> canonical = objectResourcesOf(uri().toString(), PROP_CANONICAL, model);

            return canonical.isEmpty() ? uri() : canonical.get(0);
        }

        @Override
        public Model model() {
            return model;
        }

        @Override
        public URI uri() {
            return uri;
        }

    }

    private Model getRegistry(final URI registryURI, final Service svc) {
        if (hasSameRepresentation(registryURI, svc.uri())) {
            return parse(svc);
        } else {
            return parse(get(registryURI));
        }
    }

    private static boolean hasSameRepresentation(final URI a, final URI b) {
        try {
            return new URI(a.getScheme(), a.getAuthority(), a.getPath(), null, null)
                    .equals(new URI(b.getScheme(), b.getAuthority(), b.getPath(), null, null));
        } catch (final URISyntaxException e) {
            throw new RuntimeException("Shoud never happen", e);
        }
    }

    // Try looking in canonical map first
    private URI resourceURI(final URI uri) {
        return Optional.ofNullable(canonicalUriMap.get(uri)).orElse(uri);
    }

    @Override
    public boolean hasInDomain(final URI uri) {
        init.await();
        return delegate.hasInDomain(uri) || canonicalUriMap.values().contains(uri);
    }

    private Stream<Service> attemptLookupService(final URI uri) {
        try {
            return Stream.of(this.getService(uri));
        } catch (final Exception e) {
            LOG.warn("Could not resolve service for " + uri, e);
            return Stream.of();
        }
    }

    private CloseableHttpResponse execute(final HttpUriRequest req) throws Exception {

        final CloseableHttpResponse resp = client.execute(req);

        final int statusCode = resp.getStatusLine().getStatusCode();
        if (statusCode < 200 || statusCode >= 300) {
            String body;
            try {
                body = EntityUtils.toString(resp.getEntity());
            } catch (final IOException e) {
                body = e.getMessage();
            }
            throw new RuntimeException(String.format("Unexpected status code %s when interacting wirth <%s>: %s",
                    statusCode, req.getURI(), body));
        }

        return resp;
    }

}