/*
* XMLDBStorage.java
*
* Created on April 27, 2007, 8:22 AM
*
* To change this template, choose Tools | Template Manager
* and open the template in the editor.
*/
package org.atomojo.app.storage.xmldb;
import java.io.File;
import java.io.FileOutputStream;
import org.atomojo.app.*;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.net.URLEncoder;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.atomojo.app.db.Entry;
import org.exist.restlet.XMLDB;
import org.exist.restlet.XMLDBResource;
import org.infoset.xml.Document;
import org.infoset.xml.DocumentLoader;
import org.infoset.xml.ItemDestination;
import org.infoset.xml.XMLException;
import org.infoset.xml.util.WriterItemDestination;
import org.infoset.xml.util.XMLWriter;
import org.restlet.Application;
import org.restlet.Context;
import org.restlet.Request;
import org.restlet.Response;
import org.restlet.Restlet;
import org.restlet.data.CharacterSet;
import org.restlet.data.MediaType;
import org.restlet.data.Method;
import org.restlet.data.Reference;
import org.restlet.data.Status;
import org.restlet.representation.InputRepresentation;
import org.restlet.representation.OutputRepresentation;
import org.restlet.representation.Representation;
import org.restlet.representation.StringRepresentation;
import org.restlet.routing.Router;
/**
*
* @author alex
*/
public class XMLDBStorage implements Storage
{
public static final String FEED_DOCUMENT_NAME = ".feed.atom";
class XQuery implements Query {
Reference mediaRef;
String query;
XQuery(Reference ref) {
this.mediaRef = ref;
this.query = null;
}
XQuery(String query) {
this.mediaRef = null;
this.query = query;
}
}
DocumentLoader loader;
String dbName;
XMLDB xmldb;
File xmldbDir;
Reference atomBase;
boolean makeCollections;
Context context;
Map<String,Reference> queries;
/** Creates a new instance of XMLDBStorage */
public XMLDBStorage(File xmldbDir,String dbName,Reference atomBase,DocumentLoader loader)
throws IOException
{
this.loader = loader;
this.xmldbDir = xmldbDir;
this.atomBase = atomBase;
this.makeCollections = false;
this.dbName = dbName;
this.queries = new HashMap<String,Reference>();
File confFile = new File(xmldbDir,"conf.xml");
File dbDir = xmldbDir.getParentFile();
xmldb = new XMLDB(dbName,confFile);
String log4j = System.getProperty("log4j.configuration");
if (log4j == null) {
File lf = new File(dbDir,"log4j.xml");
if (!lf.exists()) {
String [] resources = { "log4j.dtd", "log4j.xml" };
String [] names = { "log4j.dtd", "log4j.xml" };
for (int i=0; i<resources.length; i++) {
File outFile = new File(dbDir.getParentFile(),names[i]);
if (!outFile.exists()) {
copyResource(resources[i],outFile);
}
}
}
if (lf.canRead()) {
System.setProperty("log4j.configuration", lf.toURI().toASCIIString());
}
}
}
protected Logger getLogger() {
return context.getLogger();
}
public void start()
throws Exception
{
getLogger().info("Starting XMLDB "+dbName);
if (!xmldbDir.exists()) {
xmldbDir.mkdirs();
writeXMLDBConfiguration();
}
xmldb.start();
loadQueries();
getLogger().info("XMLDB "+dbName+" started.");
}
protected Response put(Reference uri,Representation entity) {
Request request = new Request(Method.PUT,uri);
request.setEntity(entity);
return context.getClientDispatcher().handle(request);
}
protected Response post(Reference uri,Representation entity) {
Request request = new Request(Method.POST,uri);
request.setEntity(entity);
return context.getClientDispatcher().handle(request);
}
protected Response head(Reference uri) {
Request request = new Request(Method.HEAD,uri);
return context.getClientDispatcher().handle(request);
}
protected Response get(Reference uri) {
Request request = new Request(Method.GET,uri);
return context.getClientDispatcher().handle(request);
}
protected Response getWithQuery(Reference uri,Reference query) {
Request request = new Request(Method.GET,uri);
//context.getLogger().info("Query: "+XMLDBResource.XQUERY_ATTR+" = "+query);
request.getAttributes().put(XMLDBResource.XQUERY_NAME, query);
return context.getClientDispatcher().handle(request);
}
protected Response delete(Reference uri) {
Request request = new Request(Method.DELETE,uri);
return context.getClientDispatcher().handle(request);
}
protected void loadQuery(String name,Reference uri,String query) {
//context.getLogger().info("Storing query: "+name+" -> "+uri);
queries.put(name,uri);
Response response = put(uri,new StringRepresentation(query,AtomResource.XQUERY_TYPE));
if (!response.getStatus().isSuccess()) {
String text = response.isEntityAvailable() ? response.getEntityAsText() : "";
context.getLogger().log(Level.SEVERE,"Cannot store query "+query+", status="+response.getStatus().getCode()+", "+text);
}
}
protected void loadQueries() {
context.getLogger().info("base: "+atomBase);
loadQuery("feed-head",new Reference(atomBase+"queries/feed-head.xq"),
"declare namespace atom='http://www.w3.org/2005/Atom'; "+
"let $cpath := substring-before(base-uri(/),'.feed.atom') return " +
"<feed xml:base='./' xmlns='http://www.w3.org/2005/Atom' xmlns:app='"+AtomResource.APP_NAMESPACE+"'>\n{ (for $e in (xcollection($cpath)/atom:feed/*) return ($e,'
'))} </feed>"
);
loadQuery("feed-updated",new Reference(atomBase+"queries/feed-updated.xq"),
"declare namespace atom='http://www.w3.org/2005/Atom'; declare variable $updated as xs:string external; update replace /atom:feed/atom:updated with <updated xmlns='http://www.w3.org/2005/Atom'>{$updated}</updated>"
);
loadQuery("feed-title",new Reference(atomBase+"queries/feed-title.xq"),
"declare namespace atom='http://www.w3.org/2005/Atom'; /atom:feed/atom:title/text()"
);
loadQuery("entry-get",new Reference(atomBase+"queries/entry-get.xq"),
"declare namespace atom='http://www.w3.org/2005/Atom'; "+
"declare variable $base as xs:string external; "+
"<entry xmlns='http://www.w3.org/2005/Atom' xmlns:app='"+AtomResource.APP_NAMESPACE+"'>\n{ ( attribute xml:base { $base }, for $e in (/atom:entry/*) return ($e,'
'))} </entry>"
);
}
public void init(Context context)
{
this.context = context;
}
public void stop()
throws Exception
{
getLogger().info("Stopping XMLDB "+dbName);
xmldb.stop();
getLogger().info("XMLDB "+dbName+" stopped.");
}
public Application getAdministration() {
return new Application(context.createChildContext()) {
public Restlet createRoot() {
getContext().getAttributes().put(XMLDBResource.DBNAME_NAME, dbName);
Router router = new Router(getContext());
Restlet reindexer = new Restlet(getContext()) {
public void handle(Request request,Response response) {
if (request.getMethod()!=Method.GET) {
response.setStatus(Status.CLIENT_ERROR_METHOD_NOT_ALLOWED);
return;
}
try {
xmldb.reindex(request.getResourceRef().getRemainingPart());
response.setStatus(Status.SUCCESS_NO_CONTENT);
} catch (Exception ex) {
response.setStatus(Status.SERVER_ERROR_INTERNAL,"Cannot reindex collection due to exception.");
getLogger().log(Level.SEVERE,"Cannot reindex collection due to exception.",ex);
}
}
};
router.attach("/data/",XMLDBResource.class);
router.attach("/reindex",reindexer);
return router;
}
};
}
protected void writeXMLDBConfiguration()
throws java.io.IOException
{
File dataDir = new File(xmldbDir,"data");
if (!dataDir.exists()) {
dataDir.mkdir();
}
String [] resources = { "catalog.xml", "conf.xml" };
String [] names = { "catalog.xml", "conf.xml" };
for (int i=0; i<resources.length; i++) {
File outFile = new File(xmldbDir,names[i]);
if (!outFile.exists()) {
copyResource(resources[i],outFile);
}
}
}
protected void copyResource(String resourcePath,File outFile)
throws IOException
{
InputStream in = getClass().getResourceAsStream(resourcePath);
if (in==null) {
throw new IOException("Cannot open resource "+resourcePath);
}
FileOutputStream out = new FileOutputStream(outFile);
byte [] buffer = new byte[8192];
int len;
while ((len=in.read(buffer))>0) {
out.write(buffer,0,len);
}
out.close();
in.close();
}
public Representation getFeed(final String path,UUID id,final Iterator<Entry> entries)
throws IOException
{
Reference feedRef = makeFeedReference(path);
if (getLogger().isLoggable(Level.FINE)) {
getLogger().fine("Feed ref: "+feedRef);
}
Response response = get(feedRef);
if (response.getStatus().isSuccess()) {
final Representation feedRep = response.getEntity();
Representation rep = new OutputRepresentation(MediaType.APPLICATION_ATOM) {
boolean released = false;
public void write(OutputStream os)
throws IOException
{
Writer out = new OutputStreamWriter(os,"UTF-8");
ItemDestination dest = new WriterItemDestination(out,"UTF-8");
FeedLoader feedLoader = new FeedLoader(getLogger(),loader,feedRep,entries);
try {
feedLoader.load(XMLDBStorage.this,context.getClientDispatcher(),path,dest);
} catch (XMLException ex) {
throw new IOException("XML exception while loading feed: "+ex.getMessage());
}
feedRep.release();
released = true;
out.flush();
out.close();
}
public void release() {
if (!released) {
feedRep.release();
}
}
};
rep.setCharacterSet(CharacterSet.UTF_8);
return rep;
} else {
throw new IOException("Cannot get feed document "+feedRef+", status="+response.getStatus().getCode());
}
}
public Representation getFeedHead(String path,UUID id)
throws IOException
{
Reference feedRef = makeFeedReference(path);
if (getLogger().isLoggable(Level.FINE)) {
getLogger().fine("Feed ref: "+feedRef);
}
Response response = getWithQuery(feedRef,queries.get("feed-head"));
//Response response = client.get(feedRef);
if (response.getStatus().isSuccess()) {
Representation rep = response.getEntity();
rep.setMediaType(MediaType.APPLICATION_ATOM);
rep.setCharacterSet(CharacterSet.UTF_8);
return rep;
} else {
boolean hasEntity = response.isEntityAvailable();
String msg = hasEntity ? response.getEntity().getText() : "";
if (hasEntity) {
response.getEntity().release();
}
throw new IOException("Cannot get feed due to status "+response.getStatus()+": "+msg);
}
}
public boolean feedUpdated(String path,UUID feedId,Date updated)
throws IOException
{
String updatedS = AtomResource.toXSDDate(updated);
Reference feedRef = makeFeedReference(path);
if (context.getLogger().isLoggable(Level.FINE)) {
context.getLogger().fine("Marking feed "+feedId+" updated at "+updatedS+" in XML DB "+dbName+" at "+feedRef);
}
feedRef.setQuery("updated="+updatedS);
Response response = getWithQuery(feedRef,queries.get("feed-updated"));
if (response.isEntityAvailable()) {
response.getEntity().release();
}
return response.getStatus().isSuccess();
}
public String getFeedTitle(String path,UUID id)
throws IOException
{
Reference feedRef = makeFeedReference(path);
Response response = getWithQuery(feedRef,queries.get("feed-title"));
boolean release = response.isEntityAvailable();
String value = response.getStatus().isSuccess() ? response.getEntity().getText() : null;
if (release) {
response.getEntity().release();
}
return value;
}
public Status storeFeed(String path,UUID id,Document doc)
{
Reference feedRef = makeFeedReference(path);
InfosetRepresentation feedRep = new InfosetRepresentation(MediaType.APPLICATION_ATOM,doc);
Response putResponse = put(feedRef,feedRep);
if (putResponse.isEntityAvailable()) {
putResponse.getEntity().release();
}
boolean success = putResponse.getStatus().isSuccess();
if (!success) {
String msg = "";
try {
Representation rep = putResponse.getEntity();
if (rep!=null) {
msg = rep.getText();
}
} catch (IOException ex) {
}
getLogger().log(Level.SEVERE,"Cannot store feed document "+feedRef+": "+msg);
}
return putResponse.getStatus();
}
public boolean deleteFeed(String path, UUID id)
{
Reference feedRef = makeFeedReference(path);
Reference collection = feedRef.getParentRef();
Response delResponse = delete(collection);
boolean success = delResponse.getStatus().isSuccess();
boolean hasEntity = delResponse.isEntityAvailable();
if (!success) {
String msg = "";
try {
Representation rep = delResponse.getEntity();
if (rep!=null) {
msg = rep.getText();
}
} catch (IOException ex) {
}
getLogger().log(Level.SEVERE,"Cannot delete feed document "+feedRef+": ("+delResponse.getStatus().getCode()+") "+msg);
}
if (hasEntity) {
delResponse.getEntity().release();
}
return success;
}
public Status storeEntry(String path,UUID feedId,UUID id,Document entryDoc)
throws IOException
{
Reference entryDocRef = makeEntryReference(path,id);
//Reference entryDocRef = new Reference(feedRef.getParentRef()+"."+id.toString()+".atom");
if (context.getLogger().isLoggable(Level.FINE)) {
context.getLogger().fine("Creating entry "+id+" in "+feedId+" in XML DB "+dbName+" at "+entryDocRef);
}
String xml = null;
try {
StringWriter w = new StringWriter();
XMLWriter.writeDocument(entryDoc,w);
xml = w.toString();
//log.info(xml);
} catch (Exception ex) {
// TODO: need to delete
getLogger().log(Level.SEVERE,"Cannot serialize entry for storage.",ex);
return Status.SERVER_ERROR_INTERNAL;
}
Representation srep = new StringRepresentation(xml,MediaType.APPLICATION_ATOM);
srep.setCharacterSet(CharacterSet.UTF_8);
Response response = put(entryDocRef,srep);
boolean hasEntity = response.isEntityAvailable();
boolean success = response.getStatus().isSuccess();
if (!success) {
Representation rep = response.getEntity();
if (rep!=null) {
try {
getLogger().log(Level.SEVERE,"Cannot store entry: "+response.getStatus()+" "+response.getEntity().getText());
} catch (IOException ex) {
getLogger().log(Level.SEVERE,"Cannot store entry: "+response.getStatus());
}
}
}
if (hasEntity) {
response.getEntity().release();
}
return response.getStatus();
}
public Representation getEntry(String feedBaseURI,String path,UUID feedId,UUID id)
throws IOException
{
Reference entryDocRef = makeEntryReference(path,id);
entryDocRef.setQuery("base="+feedBaseURI);
//Response response = client.get(entryDocRef);
Response response = getWithQuery(entryDocRef,queries.get("entry-get"));
if (response.getStatus().isSuccess()) {
Representation rep = response.getEntity();
rep.setMediaType(MediaType.APPLICATION_ATOM);
rep.setCharacterSet(CharacterSet.UTF_8);
return rep;
} else {
boolean hasEntity = response.isEntityAvailable();
String msg = hasEntity ? response.getEntity().getText() : "";
if (hasEntity) {
response.getEntity().release();
}
throw new IOException("Cannot get entry document "+entryDocRef+" due to status "+response.getStatus()+": "+msg);
}
}
public boolean deleteEntry(String path,UUID feedId,UUID id)
{
Reference entryDocRef = makeEntryReference(path,id);
Response delResponse = delete(entryDocRef);
boolean success = delResponse.getStatus().isSuccess();
boolean hasEntity = delResponse.isEntityAvailable();
if (!success) {
String msg = "";
try {
Representation rep = delResponse.getEntity();
msg = rep==null ? "" : rep.getText();
} catch (IOException ex) {
}
getLogger().log(Level.SEVERE,"Cannot delete entry document ("+delResponse.getStatus().getCode()+") "+entryDocRef+": "+msg);
}
if (hasEntity) {
delResponse.getEntity().release();
}
return success || delResponse.getStatus().getCode()==404;
}
public Status storeMedia(String path,UUID feedId,String name,MediaType type,InputStream data)
throws IOException
{
Reference mediaRef = makeMediaReference(path,name);
Response response = put(mediaRef,new InputRepresentation(data,type));
boolean success = response.getStatus().isSuccess();
boolean hasEntity = response.isEntityAvailable();
if (!success) {
Representation rep = response.getEntity();
if (rep!=null) {
try {
getLogger().log(Level.SEVERE,"Cannot store media: "+response.getStatus()+" "+response.getEntity().getText());
} catch (IOException ex) {
getLogger().log(Level.SEVERE,"Cannot store media: "+response.getStatus());
}
}
}
if (hasEntity) {
response.getEntity().release();
}
return response.getStatus();
}
public Representation getMedia(String path,UUID feedId,String name)
throws IOException
{
Reference mediaRef = makeMediaReference(path,name);
Response response = get(mediaRef);
if (response.getStatus().isSuccess()) {
return response.getEntity();
} else {
boolean hasEntity = response.isEntityAvailable();
String msg = hasEntity ? response.getEntity().getText() : "";
if (hasEntity) {
response.getEntity().release();
}
throw new IOException("Cannot get entry media "+mediaRef+" due to status "+response.getStatus()+": "+msg);
}
}
public Representation getMediaHead(String path,UUID feedId,String name)
throws IOException
{
Reference mediaRef = makeMediaReference(path,name);
Response response = head(mediaRef);
if (response.getStatus().isSuccess()) {
return response.getEntity();
} else {
boolean hasEntity = response.isEntityAvailable();
String msg = hasEntity ? response.getEntity().getText() : "";
if (hasEntity) {
response.getEntity().release();
}
throw new IOException("Cannot get entry media "+mediaRef+" due to status "+response.getStatus()+": "+msg);
}
}
public boolean deleteMedia(String path,UUID feedId,String name)
{
Reference mediaRef = makeMediaReference(path,name);
Response delResponse = delete(mediaRef);
boolean success = delResponse.getStatus().isSuccess();
boolean hasEntity = delResponse.isEntityAvailable();
if (!success) {
String msg = "";
try {
Representation rep = delResponse.getEntity();
if (rep!=null) {
msg = rep.getText();
}
} catch (IOException ex) {
}
getLogger().log(Level.SEVERE,"Cannot delete media "+mediaRef+": "+msg);
}
if (hasEntity) {
delResponse.getEntity().release();
}
return success;
}
public Query getQuery(String path, UUID feedId, String name)
throws IOException
{
Reference mediaRef = makeMediaReference(path,name);
Response response = head(mediaRef);
if (response.getStatus().isSuccess()) {
if (!response.getEntity().getMediaType().getName().equals("application/xquery")) {
response.getEntity().release();
throw new IOException("Media type "+response.getEntity().getMediaType().getName()+" on is not an XQuery.");
}
response.getEntity().release();
return new XQuery(mediaRef);
} else {
if (response.isEntityAvailable()) {
response.getEntity().release();
}
throw new IOException("Cannot retrieve media, status="+response.getStatus().getCode());
}
}
public Query compileQuery(String query)
throws IOException
{
return new XQuery(query);
}
public Representation queryFeed(String path,UUID feedId,Query query,Map<String,String> parameters)
throws IOException
{
return internalQueryFeed(false,path,feedId,query,parameters);
}
public Representation queryCollection(String path,UUID feedId,Query query,Map<String,String> parameters)
throws IOException
{
return internalQueryFeed(true,path,feedId,query,parameters);
}
Representation internalQueryFeed(boolean collection,String path,UUID feedId,Query query,Map<String,String> parameters)
throws IOException
{
XQuery xquery = (XQuery)query;
Reference feedRef = collection ? makeFeedCollectionReference(path) : makeFeedReference(path);
if (parameters!=null) {
StringBuilder refBuilder = new StringBuilder();
refBuilder.append(feedRef.toString());
refBuilder.append("?");
boolean first = true;
for (String name : parameters.keySet()) {
String value = parameters.get(name);
if (!first) {
refBuilder.append("&");
}
refBuilder.append(name);
refBuilder.append("=");
refBuilder.append(URLEncoder.encode(value,"UTF-8"));
first = false;
}
feedRef = new Reference(refBuilder.toString());
}
Response response = null;
if (xquery.mediaRef!=null) {
response = getWithQuery(feedRef,xquery.mediaRef);
} else {
response = post(feedRef,new StringRepresentation(xquery.query,AtomResource.XQUERY_TYPE));
}
if (response.getStatus().isSuccess()) {
return response.getEntity();
} else {
String text = response.getStatus().getDescription();
if (text==null) {
text = "";
}
throw new IOException("Cannot query feed, status="+response.getStatus().getCode()+", "+text);
}
}
public Reference makeEntryReference(String path,UUID entryId) {
return new Reference(atomBase+"feeds/"+path+"."+entryId.toString()+".atom");
}
public Reference makeMediaReference(String path,String name) {
try {
return new Reference(atomBase+"feeds/"+path+URLEncoder.encode(name,"UTF-8"));
} catch (UnsupportedEncodingException ex) {
throw new RuntimeException("Encoding not supported.",ex);
}
}
public Reference makeFeedReference(String path) {
return new Reference(atomBase+"feeds/"+path+FEED_DOCUMENT_NAME);
}
public Reference makeFeedCollectionReference(String path) {
return new Reference(atomBase+"feeds/"+path);
}
}