package org.basex.http.restxq;
import static org.basex.http.restxq.RestXqText.*;
import java.util.*;
import java.util.concurrent.atomic.*;
import org.basex.core.*;
import org.basex.http.*;
import org.basex.io.*;
import org.basex.query.value.item.*;
import org.basex.query.value.node.*;
import org.basex.util.*;
import org.basex.util.http.*;
/**
* This class caches RESTXQ modules found in the HTTP root directory.
*
* @author BaseX Team 2005-17, BSD License
* @author Christian Gruen
*/
public final class RestXqModules {
/** Singleton instance. */
private static RestXqModules instance;
/** Parsing mutex. */
private final AtomicBoolean parsed = new AtomicBoolean();
/** RESTXQ path. */
private final IOFile path;
/** Indicates if modules should be parsed with every call. */
private final boolean cached;
/** Module cache. */
private HashMap<String, RestXqModule> modules = new HashMap<>();
/** Last access. */
private long last;
/**
* Private constructor.
* @param ctx database context
*/
private RestXqModules(final Context ctx) {
final StaticOptions sopts = ctx.soptions;
final String webpath = sopts.get(StaticOptions.WEBPATH);
final String rxqpath = sopts.get(StaticOptions.RESTXQPATH);
path = new IOFile(webpath).resolve(rxqpath);
// RESTXQ parsing
final int ms = sopts.get(StaticOptions.PARSERESTXQ) * 1000;
// = 0: parse every time
cached = ms != 0;
// >= 0: activate timer
if(ms >= 0) {
new Timer(true).scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
synchronized(parsed) {
if(parsed.get() && System.currentTimeMillis() - last >= ms) parsed.set(false);
}
}
}, 0, 500);
}
}
/**
* Returns the singleton instance.
* @param ctx database context
* @return instance
*/
public static RestXqModules get(final Context ctx) {
if(instance == null) instance = new RestXqModules(ctx);
return instance;
}
/**
* Initializes the module cache.
*/
public void init() {
parsed.set(false);
}
/**
* Returns a WADL description for all available URIs.
* @param conn HTTP connection
* @return WADL description
*/
public FElem wadl(final HTTPConnection conn) {
return new RestXqWadl(conn).create(modules);
}
/**
* Returns the function that matches the current request or the specified error code.
* Returns {@code null} if no function matches.
* @param conn HTTP connection
* @param error error code (optional)
* @return function
* @throws Exception exception (including unexpected ones)
*/
RestXqFunction find(final HTTPConnection conn, final QNm error) throws Exception {
// collect all functions
final ArrayList<RestXqFunction> list = new ArrayList<>();
for(final RestXqModule mod : cache(conn.context).values()) {
for(final RestXqFunction rxf : mod.functions()) {
if(rxf.matches(conn, error)) list.add(rxf);
}
}
// no path matches
if(list.isEmpty()) return null;
// sort by relevance
Collections.sort(list);
// return best matching function
final RestXqFunction best = list.get(0);
if(list.size() == 1 || best.compareTo(list.get(1)) != 0) return best;
final RestXqFunction bestQf = bestQf(list, conn);
if(bestQf != null) return bestQf;
// show error if more than one path with the same specifity exists
final TokenBuilder tb = new TokenBuilder();
for(final RestXqFunction rxf : list) {
if(best.compareTo(rxf) != 0) break;
tb.add(Prop.NL).add(rxf.function.info.toString());
}
throw best.path == null ?
best.error(ERROR_CONFLICT, error, tb) :
best.error(PATH_CONFLICT, best.path, tb);
}
/**
* Returns the function that has a media type whose quality factor matches the HTTP request best.
* @param list list of functions
* @param conn HTTP connection
* @return best function, or {@code null} if more than one function exists
*/
private static RestXqFunction bestQf(final ArrayList<RestXqFunction> list,
final HTTPConnection conn) {
// media types accepted by the client
final MediaType[] accepts = conn.accepts();
double bestQf = 0;
RestXqFunction best = list.get(0);
for(final RestXqFunction rxf : list) {
// skip remaining functions with a weaker specifity
if(best.compareTo(rxf) != 0) break;
if(rxf.produces.isEmpty()) return null;
for(final MediaType produce : rxf.produces) {
for(final MediaType accept : accepts) {
final String value = accept.parameters().get("q");
final double qf = value == null ? 1 : Double.parseDouble(value);
if(produce.matches(accept)) {
// multiple functions with the same quality factor
if(bestQf == qf) return null;
if(bestQf < qf) {
bestQf = qf;
best = rxf;
}
}
}
}
}
return best;
}
/**
* Updates the module cache. Parses new modules and discards obsolete ones.
* @param ctx database context
* @return module cache
* @throws Exception exception (including unexpected ones)
*/
private HashMap<String, RestXqModule> cache(final Context ctx) throws Exception {
synchronized(parsed) {
if(!parsed.get()) {
if(!path.exists()) throw HTTPCode.NO_RESTXQ.get();
final HashMap<String, RestXqModule> map = new HashMap<>();
cache(ctx, path, map, modules);
modules = map;
parsed.set(cached);
}
last = System.currentTimeMillis();
return modules;
}
}
/**
* Parses the specified path for RESTXQ modules and caches new entries.
* @param root root path
* @param ctx database context
* @param cache cached modules
* @param old old cache
* @throws Exception exception (including unexpected ones)
*/
private static void cache(final Context ctx, final IOFile root,
final HashMap<String, RestXqModule> cache, final HashMap<String, RestXqModule> old)
throws Exception {
// check if directory is to be skipped
final IOFile[] files = root.children();
for(final IOFile file : files) if(file.name().equals(IO.IGNORESUFFIX)) return;
for(final IOFile file : files) {
if(file.isDir()) {
cache(ctx, file, cache, old);
} else {
final String path = file.path();
if(file.hasSuffix(IO.XQSUFFIXES)) {
RestXqModule module = old.get(path);
boolean parsed = false;
if(module != null) {
// check if module has been modified
parsed = module.uptodate();
} else {
// create new module
module = new RestXqModule(file);
}
// add module if it has been parsed, and if it contains annotations
if(parsed || module.parse(ctx)) {
module.touch();
cache.put(path, module);
}
}
}
}
}
}