/*
Copyright (c) 2012, 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 Adam Retter Consulting 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 Adam Retter 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.io.IOException;
import java.io.LineNumberReader;
import java.io.PrintWriter;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.util.*;
import java.util.Map.Entry;
import javax.xml.namespace.QName;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.exist.EXistException;
import org.exist.storage.BrokerPool;
import org.exist.storage.DBBroker;
import org.exist.util.Configuration;
import org.exist.util.FileUtils;
import org.exist.xquery.CompiledXQuery;
import org.exquery.ExQueryException;
import org.exquery.restxq.RestXqService;
import org.exquery.restxq.RestXqServiceRegistry;
import org.exquery.restxq.RestXqServiceRegistryListener;
import org.exquery.xquery3.FunctionSignature;
/**
*
* @author Adam Retter <adam.retter@googlemail.com>
*/
public class RestXqServiceRegistryPersistence implements RestXqServiceRegistryListener {
public final static int REGISTRY_FILE_VERSION = 0x1;
private final static String VERSION_LABEL = "version";
private final static String LABEL_SEP = ": ";
public final static String FIELD_SEP = ",";
public final static String ARITY_SEP = "#";
public final static String REGISTRY_FILENAME = "restxq.registry";
private final Logger log = LogManager.getLogger(getClass());
private final BrokerPool pool;
private final RestXqServiceRegistry registry;
public RestXqServiceRegistryPersistence(final BrokerPool pool, final RestXqServiceRegistry registry) {
this.pool = pool;
this.registry = registry;
}
private BrokerPool getBrokerPool() {
return pool;
}
private RestXqServiceRegistry getRegistry() {
return registry;
}
public void loadRegistry() {
//only load the registry if a serialized registry exists on disk
getRegistryFile(false)
.filter(r -> Files.exists(r))
.filter(r -> Files.isRegularFile(r))
.ifPresent(this::loadRegistry);
}
private void loadRegistry(final Path fRegistry) {
log.info("Loading RESTXQ registry from: " + fRegistry.toAbsolutePath().toString());
try(final LineNumberReader reader = new LineNumberReader(Files.newBufferedReader(fRegistry));
final DBBroker broker = getBrokerPool().getBroker()) {
//read version line first
String line = reader.readLine();
final String versionStr = line.substring(line.indexOf(VERSION_LABEL) + VERSION_LABEL.length() + LABEL_SEP.length());
if(REGISTRY_FILE_VERSION != Integer.parseInt(versionStr)) {
log.error("Unable to load RESTXQ registry file: " + fRegistry.toAbsolutePath().toString() + ". Expected version: " + REGISTRY_FILE_VERSION + " but saw version: " + versionStr);
} else {
while((line = reader.readLine()) != null) {
final String xqueryLocation = line.substring(0, line.indexOf(FIELD_SEP));
final CompiledXQuery xquery = XQueryCompiler.compile(broker, new URI(xqueryLocation));
final List<RestXqService> services = XQueryInspector.findServices(xquery);
getRegistry().register(services);
}
}
} catch(final ExQueryException | IOException | EXistException | URISyntaxException eqe) {
log.error(eqe.getMessage(), eqe);
}
log.info("RESTXQ registry loaded.");
}
@Override
public void registered(final RestXqService service) {
//TODO consider a pause before writing to disk of maybe 1 second or so
//to allow updates to batched together i.e. when one xquery has many resource functions
updateRegistryOnDisk(service, UpdateAction.ADD);
}
@Override
public void deregistered(final RestXqService service) {
//TODO consider a pause before writing to disk of maybe 1 second or so
//to allow updates to batched together i.e. when one xquery has many resource functions
updateRegistryOnDisk(service, UpdateAction.REMOVE);
}
private synchronized void updateRegistryOnDisk(final RestXqService restXqService, final UpdateAction updateAction) {
//we can ignore the change in service provided to this function as args, as we just write the details of all
//services to disk, overwriting the old registry
final Optional<Path> optNewRegistry = getRegistryFile(true);
if(!optNewRegistry.isPresent()) {
log.error("Could not save RESTXQ Registry to disk!");
} else {
final Path newRegistry = optNewRegistry.get();
log.info("Preparing new RESTXQ registry on disk: " + newRegistry.toAbsolutePath().toString());
try {
try(final PrintWriter writer = new PrintWriter(Files.newBufferedWriter(newRegistry, StandardOpenOption.TRUNCATE_EXISTING))) {
writer.println(VERSION_LABEL + LABEL_SEP + REGISTRY_FILE_VERSION);
//get details of RESTXQ functions in XQuery modules
final Map<URI, List<FunctionSignature>> xqueryServices = new HashMap<>();
for (final RestXqService service : getRegistry()) {
List<FunctionSignature> fnNames = xqueryServices.get(service.getResourceFunction().getXQueryLocation());
if (fnNames == null) {
fnNames = new ArrayList<>();
}
fnNames.add(service.getResourceFunction().getFunctionSignature());
xqueryServices.put(service.getResourceFunction().getXQueryLocation(), fnNames);
}
//iterate and save to disk
for (final Entry<URI, List<FunctionSignature>> xqueryServiceFunctions : xqueryServices.entrySet()) {
writer.print(xqueryServiceFunctions.getKey() + FIELD_SEP);
final List<FunctionSignature> fnSigs = xqueryServiceFunctions.getValue();
for (final FunctionSignature fnSig : fnSigs) {
writer.print(qnameToClarkNotation(fnSig.getName()) + ARITY_SEP + fnSig.getArgumentCount());
}
writer.println();
}
}
final Optional<Path> optRegistry = getRegistryFile(false);
if(optRegistry.isPresent()) {
final Path registry = optRegistry.get();
//replace the original registry with the new registry
Files.move(newRegistry, registry, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
log.info("Replaced RESTXQ registry: " + FileUtils.fileName(newRegistry) + " -> " + FileUtils.fileName(registry));
} else {
throw new IOException("Unable to retrieve existing RESTXQ registry");
}
} catch(final IOException ioe) {
log.error(ioe.getMessage(), ioe);
}
}
}
public static String qnameToClarkNotation(final QName qname) {
if(qname.getNamespaceURI() == null) {
return qname.getLocalPart();
} else {
return "{" + qname.getNamespaceURI() + "}" + qname.getLocalPart();
}
}
private enum UpdateAction {
ADD,
REMOVE
}
private Optional<Path> getRegistryFile(final boolean temp) {
try(final DBBroker broker = getBrokerPool().getBroker()) {
final Configuration configuration = broker.getConfiguration();
final Path dataDir = (Path)configuration.getProperty(BrokerPool.PROPERTY_DATA_DIR);
final Path registryFile;
if(temp) {
registryFile = Files.createTempFile(dataDir, REGISTRY_FILENAME, ".tmp");
} else {
registryFile = dataDir.resolve(REGISTRY_FILENAME);
}
return Optional.of(registryFile);
} catch(final EXistException | IOException e) {
log.error(e.getMessage(), e);
return Optional.empty();
}
}
}