All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.exist.extensions.exquery.restxq.impl.ExistXqueryRegistry Maven / Gradle / Ivy

/*
 * Copyright © 2001, Adam Retter
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of the  nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL  BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package org.exist.extensions.exquery.restxq.impl;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.exist.dom.persistent.BinaryDocument;
import org.exist.dom.persistent.DocumentImpl;
import org.exist.security.Permission;
import org.exist.security.PermissionDeniedException;
import org.exist.storage.DBBroker;
import org.exist.xmldb.XmldbURI;
import org.exist.xquery.CompiledXQuery;
import org.exist.xquery.ErrorCodes;
import org.exist.xquery.XPathException;
import org.exist.xquery.value.Item;
import org.exist.xquery.value.Sequence;
import org.exist.xquery.value.StringValue;
import org.exquery.ExQueryException;
import org.exquery.restxq.RestXqService;
import org.exquery.restxq.RestXqServiceRegistry;

/**
 *
 * @author Adam Retter
 */
public class ExistXqueryRegistry {

    //singleton
    private final static ExistXqueryRegistry instance = new ExistXqueryRegistry();
    private ExistXqueryRegistry() {
    }
    
    public static ExistXqueryRegistry getInstance() {
        return instance;
    }
    
    
    private final static Logger LOG = LogManager.getLogger(ExistXqueryRegistry.class);
    
    /**
     * Key is XQuery Module URI
     * Value is set of XQuery Module URIs on which the Module indicated by the Key depends on
     */
    private final static Map> dependenciesTree = new HashMap<>();

    /**
     * Returns a copy of the known dependency tree
     * @return copy of the known dependency tree
     */
    public Map> getDependenciesTree() {
        final Map> copy = new HashMap<>();
        for(final Map.Entry> dependencyTree : dependenciesTree.entrySet()) {
            copy.put(dependencyTree.getKey(), new HashSet<>(dependencyTree.getValue()));
        }
        return copy;
    }

    /**
     * Key is the missing Module URI
     * Value is the Set of XQuery Module URIs that require the missing Module indicated by the Key
     */
    private final static Map> missingDependencies = new HashMap<>();

    /**
     * Returns a copy of the current known missing dependencies
     * @return copy of the current known missing dependencies
     */
    public Map> getMissingDependencies() {
        final Map> copy = new HashMap<>();
        for(final Map.Entry> missingDependency : missingDependencies.entrySet()) {
            copy.put(missingDependency.getKey(), new HashSet<>(missingDependency.getValue()));
        }
        return copy;
    }

    /**
     * The list of XQuerys that could not be compiled
     * for reasons other than missing dependencies
     */
    private final static Set invalidQueries = new HashSet<>();

    /**
     * Returns a copy of the current known invalid queries
     * @return a copy of the current known invalid queries
     */
    public Set getInvalidQueries() {
        return new HashSet<>(invalidQueries);
    }

    public boolean isXquery(final DocumentImpl document) {
         return document instanceof BinaryDocument && document.getMimeType().equals(XQueryCompiler.XQUERY_MIME_TYPE);
    }
    
    public void registerServices(final DBBroker broker, final List services) {
        getRegistry(broker).register(services);
    }
    
    public void deregisterServices(final DBBroker broker, final XmldbURI xqueryLocation) {
        
        getRegistry(broker).deregister(xqueryLocation.getURI());
        
        //find and remove services from modules that depend on this one
        for(final String dependant : getDependants(xqueryLocation)) {
            try {
                
                //TODO This null check is a temporary workaround
                //as a NPE in the URI class was reported by Wolf
                //where dependant was null. I can only imagine
                //that another thread interrupted and removed it
                //from the hashmap that it comes from.
                //its quite possible the use of synchronized around
                //the various maps in this class is not sufficient in scope
                //and we should move to some locks and operating over closures
                //on the maps.
                if(dependant != null) {
                    getRegistry(broker).deregister(new URI(dependant));

                    //record the now missing dependency
                    recordMissingDependency(xqueryLocation.toString(), XmldbURI.create(dependant));
                }
            } catch(final URISyntaxException e) {
                LOG.error(e.getMessage(), e);
            }
        }
        
        /*
         * update the missingDependencies??
         * Probably not needed as this will be done in find services
         */
    }
    
    public void deregisterService(final DBBroker broker, final RestXqService service) {
        
        getRegistry(broker).deregister(service);
        
        //TODO below is not needed as we are not removing the module just a single service
        //find and remove services from modules that depend on this one
        /*for(final String dependant : getDependants(xqueryLocation)) {
            try {
                
                //TODO This null check is a temporary workaround
                //as a NPE in the URI class was reported by Wolf
                //where dependant was null. I can only imagine
                //that another thread interrupted and removed it
                //from the hashmap that it comes from.
                //its quite possible the use of synchronized around
                //the various maps in this class is not sufficient in scope
                //and we should move to some locks and operating over closures
                //on the maps.
                if(dependant != null) {
                    getRegistry(broker).deregister(new URI(dependant));

                    //record the now missing dependency
                    recordMissingDependency(xqueryLocation.toString(), XmldbURI.create(dependant));
                }
            } catch(final URISyntaxException e) {
                LOG.error(e.getMessage(), e);
            }
        }*/
        
        /*
         * update the missingDependencies??
         * Probably not needed as this will be done in find services
         */
    }
    
    private Set getDependants(final XmldbURI xqueryLocation) {
        final Set dependants = new HashSet<>();
        
        //make a copy of the dependenciesTree into depTree
        final Map> depTree;
        synchronized(dependenciesTree) {
            depTree = new HashMap<>(dependenciesTree);
        }
        
        //find all modules that have a dependency on this one
        for(final Map.Entry> depTreeEntry : depTree.entrySet()) {
            for(final String dependency : depTreeEntry.getValue()) {
                if(dependency.equals(xqueryLocation.toString())) {
                    dependants.add(depTreeEntry.getKey());
                    continue; //TODO(AR) do we mean continue or break?
                }
            }
        }
        return dependants;
    }
    
    public Iterator registered(final DBBroker broker) {
        return getRegistry(broker).iterator();
    }
    
    public List findServices(final DBBroker broker, final DocumentImpl document) throws ExQueryException {
        
        try {
            final CompiledXQuery compiled = XQueryCompiler.compile(broker, document);

            /*
             * examine the compiled query, record all modules and modules of modules.
             * Keep a dependencies list so that we can act on it if a module is deleted.
             */ 
            final Map> queryDependenciesTree = XQueryInspector.getDependencies(compiled);
            recordQueryDependenciesTree(queryDependenciesTree);

            /*
             * A compiled query may be a missing dependency for another query
             * so reexamine queries with missing dependencies
             */
            reexamineModulesWithResolvedDependencies(broker, document.getURI().toString());

            /*
             * remove any potentially re-compiled query from the
             * invalid queries list
             */ 
            removeInvalidQuery(document.getURI());

            return XQueryInspector.findServices(compiled);
        } catch(final RestXqServiceCompilationException e) {

            //if there was a missing dependency then record it
            final MissingModuleHint missingModuleHint = extractMissingModuleHint(e);
            if(missingModuleHint != null) {
                
                if(missingModuleHint.dependantModule == null) {
                    recordMissingDependency(missingModuleHint.moduleHint, document.getURI());
                } else {
                    //avoids wrong missing dependency dependant being recorded for a complex module tree
                    try {
                        recordMissingDependency(missingModuleHint.dependantModule, document.getURI());
                        recordMissingDependency(missingModuleHint.moduleHint, XmldbURI.xmldbUriFor(missingModuleHint.dependantModule));
                    } catch(final URISyntaxException use) {
                        recordInvalidQuery(document.getURI());
                        LOG.error("XQuery '{}' could not be compiled! {}", document.getURI(), e.getMessage());
                    }
                }
            } else {
                recordInvalidQuery(document.getURI());
                LOG.error("XQuery '{}' could not be compiled! {}", document.getURI(), e.getMessage());
            }

            /*
             * This may be the recompilation of a query
             * so we should unregister any of its missing
             * services. Luckily this is taken care of in
             * the before{EVENT} trigger functions
             */
        }
        
        return new ArrayList<>();
    }
    
   
    /**
     * Gets the modules that have a missing dependency
     * on the module indicated by compiledModuleURI
     * and attempts to re-compile them and register their
     * services
     */
    private void reexamineModulesWithResolvedDependencies(final DBBroker broker, final String compiledModuleUri) {
        
        final Set dependants;
        synchronized(missingDependencies) {
            final Set deps = missingDependencies.get(compiledModuleUri);
            if(deps != null) {
                dependants = new HashSet<>(deps);
            } else {
                dependants = new HashSet<>();
            }
        }
        
        for(final String dependant : dependants) {
            
            try {
                
                final DocumentImpl dependantModule = broker.getResource(XmldbURI.create(dependant), Permission.READ);
                
                /*
                 * This null check is needed, as a dependency module may have been renamed,
                 * and so is no longer accessible under its old URI.
                 *
                 * However if its dependant module (compiledModuleUri) compiles
                 * (which it must have for this function to be invoked)
                 * then we can assume that the dependant module references the new
                 * module dependency (in the case of a module move/rename)
                 * or the dependency has been removed
                 */
                if(dependantModule != null) {
                    LOG.info("Missing dependency '{}' has been added to the database, re-examining '{}'...", compiledModuleUri, dependant);
                    
                    final List services = findServices(broker, dependantModule);
                    LOG.info("Discovered {} resource functions for {}", services.size(), dependant);
                    registerServices(broker, services);
                } else {
                    LOG.info("Dependant '{}' has been resolved. Dependency on: {} was removed", compiledModuleUri, dependant);

                    //we need to remove dependant from the dependenciesTree of dependant
                    removeDependency(dependant, compiledModuleUri);
                }
            } catch(final PermissionDeniedException | ExQueryException pde) {
                LOG.error(pde.getMessage(), pde);
            }

            //remove the resolve dependencies from the missing dependencies
            removeMissingDependency(compiledModuleUri, dependant);
        }
    }
    
    private void removeMissingDependency(final String dependency, final String dependant) {
        synchronized(missingDependencies) {
            final Set missingDependants = missingDependencies.get(dependency);
            missingDependants.remove(dependant);
            if(missingDependants.isEmpty()) {
                missingDependencies.remove(dependency);
            }
        }
    }
    
    private void recordQueryDependenciesTree(final Map> queryDependenciesTree) {
        synchronized(dependenciesTree) {
            //Its not a merge its an overwrite!
            dependenciesTree.putAll(queryDependenciesTree);
        }
    }
    
    private void removeInvalidQuery(final XmldbURI xqueryUri) {
        synchronized(invalidQueries) {
            invalidQueries.remove(xqueryUri.toString());
        }
    }
    
    private void recordInvalidQuery(final XmldbURI xqueryUri) {
        synchronized(invalidQueries) {
            invalidQueries.add(xqueryUri.toString());
        }
    }

    private static class MissingModuleHint {
        public String moduleHint = null;
        public String dependantModule = null;
    }
    
    private MissingModuleHint extractMissingModuleHint(final RestXqServiceCompilationException e) {
        
        MissingModuleHint missingModuleHint = null;
        
        if(e.getCause() instanceof XPathException) {
            final XPathException xpe = (XPathException)e.getCause();
            if(xpe.getErrorCode() == ErrorCodes.XQST0059) {
                final Sequence errorVals = xpe.getErrorVal();
                
                if(errorVals != null && errorVals.getItemCount() > 0){
                    
                    final Item errorVal1 = errorVals.itemAt(0);
                    if(errorVal1 instanceof StringValue) {
                        missingModuleHint = new MissingModuleHint();
                        missingModuleHint.moduleHint = ((StringValue)errorVal1).getStringValue();
                    }
                    
                    if(errorVals.getItemCount() == 2) {
                        final Item errorVal2 = errorVals.itemAt(1);
                        if(errorVal2 instanceof StringValue) {
                            if(missingModuleHint == null) {
                                missingModuleHint = new MissingModuleHint();
                            }
                            
                            final String dependantModuleUri = ((StringValue)errorVal2).getStringValue();
                            
                            //path will be of xmldb:exist:///db/a/c/1.xqm form so change it to /db/a/c/1.xqm form
                            missingModuleHint.dependantModule = makeDbAbsolutePath(dependantModuleUri);
                        }
                    }
                }
            }
        }
        
        return missingModuleHint;
    }
    
    private void recordMissingDependency(final String moduleHint, final XmldbURI xqueryUri) {
        final String moduleUri = getAbsoluteModuleHint(moduleHint, xqueryUri);
        
        synchronized(missingDependencies) {
            final Set dependants;
            if(missingDependencies.containsKey(moduleUri)) {
                dependants = missingDependencies.get(moduleUri);
            } else {
                dependants = new HashSet<>();
            }
            
            dependants.add(xqueryUri.toString());
            
            missingDependencies.put(moduleUri, dependants);
        }

        LOG.warn("Module '{}' has a missing dependency on '{}'. Will re-examine if the missing module is added.", xqueryUri, moduleUri);
    }
    
    private void removeDependency(final String dependant, final String dependency) {
        synchronized(dependenciesTree) {
            final Set dependencies = dependenciesTree.get(dependant);
            if(dependencies != null) {
                dependencies.remove(dependency);
                if(dependencies.isEmpty()) {
                    dependenciesTree.remove(dependant);
                }
            }
        }
    }
    
    protected String getAbsoluteModuleHint(final String moduleHint, final XmldbURI xqueryUri) {
        if(moduleHint.startsWith(XmldbURI.ROOT_COLLECTION)) {
            //absolute simple path
            return moduleHint;
        } else if(moduleHint.startsWith(XmldbURI.EMBEDDED_SERVER_URI.toString())) {
            return moduleHint.replace(XmldbURI.EMBEDDED_SERVER_URI.toString(), "");
        } else if(moduleHint.startsWith(XmldbURI.EMBEDDED_SERVER_URI_PREFIX)) {
            return moduleHint.replace(XmldbURI.EMBEDDED_SERVER_URI_PREFIX, "");
        } else {
            
            //relative to the xqueryUri
            final XmldbURI xqueryPath = xqueryUri.removeLastSegment();
            
            return xqueryPath.append(moduleHint).toString();
        }
    }
    
    /**
     * Converts an xmldb:exist:// path to an Absolute DB path
     * 
     * e.g. path  xmldb:exist:///db/a/c/1.xqm form will to /db/a/c/1.xqm form
     */
    private String makeDbAbsolutePath(String dependantModuleUri) {
        dependantModuleUri = dependantModuleUri.replace(XmldbURI.EMBEDDED_SERVER_URI.toString(), "");
        dependantModuleUri = dependantModuleUri.replace(XmldbURI.EMBEDDED_SERVER_URI_PREFIX, "");
        if(!dependantModuleUri.isEmpty() && !dependantModuleUri.startsWith("/")) {
            dependantModuleUri = dependantModuleUri.substring(dependantModuleUri.indexOf("/"));
        }
        return dependantModuleUri;
    }
    
    private RestXqServiceRegistry getRegistry(final DBBroker broker) {
        return RestXqServiceRegistryManager.getRegistry(broker.getBrokerPool());
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy