public static List<String> objectLiteralsOf(final String s, final String p, final Model model) {
    return model.listStatements(
            s != null ? model.getResource(s) : null,
            model.getProperty(p),
            (RDFNode) null)
            .mapWith(Statement::getObject)
            .filterKeep(RDFNode::isLiteral)
            .mapWith(RDFNode::asLiteral)
            .mapWith(Literal::getString)
            .toList();
} public static URI objectResourceOf(final String s, final String p, final Model model) {
    return one(s, p, objectResourcesOf(s, p, model));
} public static List<URI> objectResourcesOf(final String s, final String p, final Model model) {
    return model.listStatements(
            s != null ? model.getResource(s) : null,
            model.getProperty(p),
            (RDFNode) null)
            .mapWith(Statement::getObject)
            .filterKeep(RDFNode::isURIResource)
            .mapWith(RDFNode::asResource)
            .mapWith(Resource::getURI)
            .mapWith(URI::create)
            .toList();
} public static Model parse(final WebResource r, final String base) {

    if (r instanceof JenaResource && ((JenaResource) r).model() != null) {
        return ((JenaResource) r).model();
    }

    final Model model =
            ModelFactory.createDefaultModel();

    final Lang lang = rdfLanguage(r.contentType());

    try (WebResource toParse = r;
            InputStream representation = toParse.representation()) {
        RDFDataMgr.read(model, representation, base, lang);
    } catch (final Exception e) {
        throw new RuntimeException(e);
    }

    return model;
} public static URI subjectOf(final String p, final String o, final Model model) {
    final List<URI> subjects = subjectsOf(p, o, model);

    if (subjects.size() != 1) {
        throw new ResourceNotFoundException(
                String.format("Expecting to find exactly one subject of ? <%s> <%s>", p, o));
    }

    return subjects.get(0);
} public static Set<URI> subjectsOf(final Stream<Triple> triples) {
    return triples
            .map(Triple::getSubject)
            .filter(n -> !n.isBlank())
            .map(Node::getURI)
            .map(URI::create)
            .collect(Collectors.toSet());
} public interface Extension {

    /**
     * RDF type this extension binds to.
     *
     * @return URI of the <code>rdf:type</code> this extension binds to.
     */
    public URI bindingClass();

    /**
     * Determine if this extension exposes services.
     *
     * @return true if the extension exposes services
     */
    public boolean isExposing();

    /**
     * Determine if this extension intercepts requests.
     *
     * @return true if the extension intercepts requests.
     */
    public boolean isIntercepting();

    /**
     * Get specification for exposed services.
     *
     * @return Service exposure specification.
     */
    public ServiceExposureSpec exposed();

    /**
     * Get specification for intercepting modality.
     *
     * @return Specification.
     */
    public Spec intercepted();

    /**
     * The URI (location) of the extension.
     * <p>
     * This is a potentially more lightweight operation than <code>getResource().uri()</code>
     * </p>
     *
     * @return its resolvable URI.
     */
    public URI uri();

    /**
     * Underlying RDF representation of an extension.
     *
     * @return A serialization of the extension resource.
     */
    public WebResource getResource();

    /**
     * Implementation specification for an extension.
     */
    public interface Spec {

        /**
         * Set of consumed services.
         *
         * @return Non-null set.
         */
        public Set<URI> consumed();

    }

    /**
     * Implementation specification for a service-exposing specification.
     */
    public interface ServiceExposureSpec extends Spec {

        /**
         * The URI naming the exposed service.
         * <p>
         * This is not an endpoint URI, it is the URI which names the service in the abstract sense. A lookup in a
         * {@link org.fcrepo.apix.model.components.ServiceRegistry} by URI can be expected to provide more information
         * </p>
         *
         * @return the exposed service URI.
         */
        public URI exposedService();

        /**
         * URI the service is exposed at.
         * <p>
         * May be absolute or relative. If relative, the {@link org.fcrepo.apix.model.components.Routing} component
         * will use this to mint the resolvable URI a service is exposed at.
         * </p>
         *
         * @return service exposure URI.
         */
        public URI exposedAt();

        /**
         * The scope of the exposed service.
         *
         * @return scope (external, repository, resource)
         */
        public Scope scope();
    }

    public enum Scope {
        RESOURCE,
        REPOSITORY,
        EXTERNAL
    }
} public interface Service extends WebResource {

    /**
     * The canonical URI for a service.
     *
     * @return Canonical URI. If none defined, then this is implicitly the URI of the service resource itself.
     */
    URI canonicalURI();
} public interface ExtensionRegistry extends Registry {

    /**
     * Retrieve the abstract representation of a specific extension by URI.
     * <p>
     * Transforms the resource retrievable by {@link #get(URI)} into an abstract notion of an extension.
     * </p>
     *
     * @param uri URI of the desired extension.
     * @return Abstract representation of an extension.
     */
    public Extension getExtension(URI uri);

    /**
     * Retrieve all known extension. Contains a transformation of all resources accessible via
     * {@link ExtensionRegistry#list()}.
     *
     * @return A collection of all known extensions, or null if none.
     */
    public Collection<Extension> getExtensions();
} public interface ServiceRegistry extends Registry {

    /**
     * Get a registry of all instances of a given service.
     *
     * @param service the service.
     * @return registry of service instances.
     */
    ServiceInstanceRegistry instancesOf(Service service);

    /**
     * Create a new service instance registry.
     *
     * @param service The service for which to register instances.
     * @return new service instance registry
     */
    ServiceInstanceRegistry createInstanceRegistry(Service service);

    /**
     * Register the given uri as a service.
     * <p>
     * This does not deposit content in the registry, only references a web resource as a service.
     * </p>
     *
     * @param uri URI to the service resource.
     */
    public void register(URI uri);

    /**
     * Retrieve an abstract representation of a service.
     *
     * @param uri URI of the service
     * @return Service instance
     * @throws ResourceNotFoundException if the service is not in the registry.
     */
    Service getService(URI uri) throws ResourceNotFoundException;
}
import static org.fcrepo.apix.jena.Util.objectLiteralsOf;
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.jena.Util.subjectsOf;
import static org.fcrepo.apix.model.Ontologies.RDF_TYPE;
import static org.fcrepo.apix.model.Ontologies.Apix.CLASS_EXTENSION;
import static org.fcrepo.apix.model.Ontologies.Apix.PROP_CONSUMES_SERVICE;
import static org.fcrepo.apix.model.Ontologies.Apix.PROP_EXPOSES_SERVICE;
import static org.fcrepo.apix.model.Ontologies.Apix.PROP_EXPOSES_SERVICE_AT;
import static org.fcrepo.apix.model.Ontologies.Service.CLASS_SERVICE;
import static org.fcrepo.apix.model.Ontologies.Service.PROP_CANONICAL;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.net.URI;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;
import org.fcrepo.apix.model.Extension;
import org.fcrepo.apix.model.Service;
import org.fcrepo.apix.model.WebResource;
import org.fcrepo.apix.model.components.ExtensionRegistry;
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.apache.commons.io.IOUtils;
import org.apache.jena.rdf.model.Model;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
        }

        return depositedResource;
    }

    // Load an extension resource, then load any services defined or referenced by it if appropriate.
    private Set<URI> loadServices(final URI uri) {

        final Model persistedModel = parse(generalRegistry.get(uri));

        final Set<URI> defined = new HashSet<>(subjectsOf(RDF_TYPE, CLASS_SERVICE, persistedModel));

        final Set<URI> definedWithExplicitCanonical = new HashSet<>(subjectsOf(PROP_CANONICAL, null, persistedModel));

        final Set<URI> referenced = allReferencedServices(persistedModel);

        final Set<URI> resolvableservices = new HashSet<>();

        final Set<URI> canonicalServices = new HashSet<>();

        LOG.debug("Defined services are: {}", defined);
        LOG.debug("Defined with canonical: {}", definedWithExplicitCanonical);
        LOG.debug("Referenced: {}", referenced);

        // All referenced services that are not otherwise given a canonical URI are canonical.
        canonicalServices.addAll(
                referenced.stream().filter(u -> !definedWithExplicitCanonical.contains(u))
                        .collect(Collectors.toSet()));

        // All explicitly canonical URIs are canonical
        canonicalServices.addAll(
                definedWithExplicitCanonical.stream()
                        .map(URI::toString)
                        .map(s -> objectResourceOf(s, PROP_CANONICAL, persistedModel))
                        .collect(Collectors.toSet()));

        // Defined services without an explicitly canonical URI are canonical
        canonicalServices.addAll(defined.stream()
                .filter(u -> !definedWithExplicitCanonical.contains(u))
                .collect(Collectors.toSet()));

        // Finally, add any canonical services that aren't in our registry.
        for (final URI canonical : canonicalServices) {
            if (serviceRegistry.contains(canonical)) {
                LOG.debug("Service registry contains <{}>, NOT adding", canonical);
                resolvableservices.add(canonical);
            } else {
                LOG.info("Service registry does not contain <{}>, adding!", canonical);
                resolvableservices.add(putService(canonical));
            }
        }

        // Return a set of all resolvable URIs to services mentioned in this document.
        return resolvableservices;

    }

    private URI addInstance(final URI instanceURI, final Service service) {
        ServiceInstanceRegistry instances = null;
        try {
            instances = serviceRegistry.instancesOf(service);
        } catch (final ResourceNotFoundException e) {
            instances = serviceRegistry.createInstanceRegistry(service);
        }

        LOG.info("Adding endpoint <{}> as instance of <{}>", instanceURI, service.canonicalURI());
        return instances.addEndpoint(instanceURI);
    }

    private URI putService(final URI serviceURI) {

        try (InputStream template = getClass().getResourceAsStream("/objects/service.ttl")) {
            final String rdf = IOUtils.toString(template, "utf8")
                    .replace("CANONICAL_SERVICE_URI", serviceURI.toString());

            return serviceRegistry.put(WebResource.of(new ByteArrayInputStream(rdf.getBytes()), "text/turtle", null,
                    toName(serviceURI)));
        } catch (final Exception e) {
            throw new RuntimeException(e);
        }

    }

    private static Set<URI> allReferencedServices(final Model model) {
        final Set<URI> referencedServices = new HashSet<>();
        referencedServices.addAll(objectResourcesOf(null, PROP_CONSUMES_SERVICE, model));
        referencedServices.addAll(objectResourcesOf(null, PROP_EXPOSES_SERVICE, model));

        return referencedServices;
    }

    private static Set<URI> allDefinedServices(final Model model) {
        return new HashSet<>(subjectsOf(RDF_TYPE, CLASS_SERVICE, model));
    }

    // Find an extension in the registry that matches (i.e. "is") the given one
    private URI findMatchingExtension(final Model model) {
        final Collection<URI> exposesServices = objectResourcesOf(null, PROP_EXPOSES_SERVICE, model);
        final Collection<URI> consumesServices = objectResourcesOf(null, PROP_CONSUMES_SERVICE, model);
        final Collection<String> exposesAt = objectLiteralsOf(null, PROP_EXPOSES_SERVICE_AT, model);
        final boolean isExposing = exposesServices.size() > 0;

        for (final Extension e : extensionRegistry.getExtensions()) {
            if (isExposing && e.isExposing() && exposesAt.contains(e.exposed().exposedAt().toString())) {
                return e.uri();
            } else if (!isExposing && consumesServices.equals(e.intercepted().consumed())) {
                return e.uri();
            }
        }

        return null;
    }

    // Return true if any blank nodes are present
    private boolean useBinary(final Model model) {
        return !model.listSubjects().filterKeep(r -> r.isAnon()).toList().isEmpty();
    }

    // Come up with a plausible name for the extension
    private String extensionName(final Model model) {
        return toName(subjectOf(RDF_TYPE, CLASS_EXTENSION, model));