/*
* FileStorage.java
*
*/
package org.atomojo.app.storage.file;
import java.io.File;
import java.io.FileFilter;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.atomojo.app.AtomResource;
import org.atomojo.app.Storage;
import org.atomojo.app.Storage.Query;
import org.atomojo.app.client.Feed;
import org.atomojo.app.client.Text;
import org.atomojo.app.db.DB;
import org.atomojo.app.db.DB.MediaEntryListener;
import org.atomojo.app.db.Entry;
import org.atomojo.app.db.EntryMedia;
import org.infoset.xml.Attribute;
import org.infoset.xml.Document;
import org.infoset.xml.DocumentLoader;
import org.infoset.xml.Element;
import org.infoset.xml.Item;
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.data.CharacterSet;
import org.restlet.data.MediaType;
import org.restlet.data.Status;
import org.restlet.representation.FileRepresentation;
import org.restlet.representation.OutputRepresentation;
import org.restlet.representation.Representation;
import org.restlet.representation.StringRepresentation;
import org.restlet.routing.Router;
import org.restlet.service.MetadataService;
/**
*
* @author alex
*/
public class FileStorage implements Storage
{
public static final String FEED_DOCUMENT_NAME = ".feed.atom";
static long syncWait = 5*60*1000;
static {
String value = System.getProperty("org.atomojo.app.storage.file.sync");
if (value!=null) {
syncWait = Long.parseLong(value);
}
}
static MetadataService metaService = new MetadataService();
DocumentLoader loader;
File contentDir;
Context context;
DB db;
boolean started = false;
Thread syncThread;
class SyncTask implements Runnable {
public void run() {
while (started) {
try {
sync();
Thread.currentThread().sleep(syncWait);
} catch (InterruptedException ex) {
} catch (Exception ex) {
getLogger().log(Level.SEVERE,"Error during synchronization.",ex);
}
}
}
}
public FileStorage(DocumentLoader loader,DB db,File contentDir) {
this.loader = loader;
this.contentDir = contentDir;
this.db = db;
}
protected Logger getLogger() {
return context.getLogger();
}
public void start()
throws Exception
{
getLogger().info("Database "+db.getName()+" storing content in "+contentDir.getAbsolutePath());
if (!contentDir.exists()) {
contentDir.mkdirs();
}
started = true;
syncThread = new Thread(new SyncTask());
syncThread.start();
}
public void init(Context context)
{
this.context = context;
}
public void stop()
throws Exception
{
started = false;
synchronized (syncThread) {
syncThread.interrupt();
}
syncThread.join(2000);
}
public Application getAdministration() {
return null;
}
public Representation getFeed(final String path,UUID id,final Iterator<Entry> entries)
throws IOException
{
final File feedFile = makeCollectionReference(path);
Representation rep = new OutputRepresentation(MediaType.APPLICATION_ATOM_XML) {
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,feedFile,entries);
try {
feedLoader.load(dest);
} catch (XMLException ex) {
throw new IOException("XML exception while loading feed: "+ex.getMessage());
}
out.flush();
}
};
rep.setCharacterSet(CharacterSet.UTF_8);
Date lastModified = new Date(makeFeedReference(path).lastModified());
rep.setModificationDate(lastModified);
return rep;
}
public Representation getFeedHead(String path,UUID id)
throws IOException
{
return new FileRepresentation(makeFeedReference(path),MediaType.APPLICATION_ATOM_XML);
}
public boolean feedUpdated(String path,UUID feedId,Date updated)
throws IOException
{
File feedFile = makeFeedReference(path);
try {
Document doc = loader.load(feedFile.toURI());
Feed feed = new Feed(doc);
feed.setUpdated(updated);
Writer out = new OutputStreamWriter(new FileOutputStream(feedFile),"UTF-8");
XMLWriter.writeDocument(doc, out);
out.close();
return true;
} catch (XMLException ex) {
throw new IOException("XML exception while updating: "+ex.getMessage());
}
}
public String getFeedTitle(String path,UUID id)
throws IOException
{
File feedFile = makeFeedReference(path);
try {
Document doc = loader.load(feedFile.toURI());
Feed feed = new Feed(doc);
return feed.getTitle();
} catch (XMLException ex) {
throw new IOException("XML exception while getting title: "+ex.getMessage());
}
}
public Status storeFeed(String path,UUID id,Document doc)
{
File feedFile = makeFeedReference(path);
File dir = feedFile.getParentFile();
if (!dir.exists()) {
if (!dir.mkdirs()) {
getLogger().log(Level.SEVERE,"Cannot make parent directory for feed: "+dir.getAbsolutePath());
return Status.SERVER_ERROR_INTERNAL;
}
}
try {
Writer out = new OutputStreamWriter(new FileOutputStream(feedFile),"UTF-8");
XMLWriter.writeDocument(doc, out);
out.close();
return Status.SUCCESS_OK;
} catch (Exception ex) {
getLogger().log(Level.SEVERE,"Cannot write feed document to "+feedFile.getAbsolutePath(),ex);
return Status.SERVER_ERROR_INTERNAL;
}
}
public boolean deleteFeed(String path, UUID id)
{
File dir = makeCollectionReference(path);
final List<File> queue = new ArrayList<File>();
queue.add(dir);
boolean ok = true;
int mark = -1;
while (ok && queue.size()>0) {
File target = queue.remove(queue.size()-1);
if (target.isDirectory()) {
if (mark==queue.size()) {
ok = target.delete();
mark = -1;
} else {
mark = queue.size();
queue.add(target);
target.listFiles(new FileFilter() {
public boolean accept(File f)
{
queue.add(f);
return false;
}
});
}
} else {
ok = target.delete();
}
}
return ok;
}
public Status storeEntry(String path,UUID feedId,UUID id,Document entryDoc)
throws IOException
{
File entryFile = makeEntryReference(path,id);
try {
Writer out = new OutputStreamWriter(new FileOutputStream(entryFile),"UTF-8");
XMLWriter.writeDocument(entryDoc, out);
out.close();
return Status.SUCCESS_OK;
} catch (Exception ex) {
getLogger().log(Level.SEVERE,"Cannot write entry document to "+entryFile.getAbsolutePath(),ex);
return Status.SERVER_ERROR_INTERNAL;
}
}
public Representation getEntry(final String feedBaseURI,String path,UUID feedId,UUID id)
throws IOException
{
final File entry = makeEntryReference(path,id);
Representation rep = new OutputRepresentation(MediaType.APPLICATION_ATOM_XML) {
public void write(OutputStream os)
throws IOException
{
Writer w = new OutputStreamWriter(os,"UTF-8");
final WriterItemDestination dest = new WriterItemDestination(w,"UTF-8",true);
dest.setOmitXMLDeclaration(true);
try {
loader.generate(entry.toURI(), new ItemDestination() {
int level = 0;
public void send(Item item)
throws XMLException
{
switch (item.getType()) {
case ElementItem:
if (level==0) {
Element e = (Element)item;
e.setAttributeValue(Attribute.XML_BASE, feedBaseURI);
} else {
Element e = (Element)item;
e.setBaseURI(null);
}
level++;
break;
case ElementEndItem:
level--;
}
dest.send(item);
}
});
} catch (XMLException ex) {
throw new IOException(ex.getMessage());
}
}
};
rep.setCharacterSet(CharacterSet.UTF_8);
rep.setModificationDate(new Date(entry.lastModified()));
return rep;
}
public boolean deleteEntry(String path,UUID feedId,UUID id)
{
File entryFile = makeEntryReference(path,id);
return entryFile.delete();
}
public Status storeMedia(String path,UUID feedId,String name,MediaType type,InputStream data)
throws IOException
{
File mediaFile = makeMediaReference(path,name);
OutputStream os = new FileOutputStream(mediaFile);
byte [] buffer = new byte[8196];
int len;
while ((len=data.read(buffer))>0) {
os.write(buffer,0,len);
}
os.flush();
os.close();
return Status.SUCCESS_CREATED;
}
public Representation getMedia(String path,UUID feedId,String name)
throws IOException
{
File mediaFile = makeMediaReference(path,name);
return new FileRepresentation(mediaFile,MediaType.APPLICATION_OCTET_STREAM);
}
public Representation getMediaHead(String path,UUID feedId,String name)
throws IOException
{
File mediaFile = makeMediaReference(path,name);
Representation rep = new StringRepresentation("",MediaType.APPLICATION_OCTET_STREAM);
rep.setModificationDate(new Date(mediaFile.lastModified()));
return rep;
}
public boolean deleteMedia(String path,UUID feedId,String name)
{
File mediaFile = makeMediaReference(path,name);
return mediaFile.delete();
}
public Query getQuery(String path, UUID feedId, String name)
throws IOException
{
throw new IOException("Queries are not supported by file storage.");
}
public Query compileQuery(String query)
throws IOException
{
throw new IOException("Queries are not supported by file storage.");
}
public Representation queryFeed(String path,UUID feedId,Query query,Map<String,String> parameters)
throws IOException
{
throw new IOException("Queries are not supported by file storage.");
}
public Representation queryCollection(String path,UUID feedId,Query query,Map<String,String> parameters)
throws IOException
{
throw new IOException("Queries are not supported by file storage.");
}
protected File makeEntryReference(String path,UUID entryId) {
return new File(new File(contentDir,path),"."+entryId.toString()+".atom");
}
protected File makeMediaReference(String path,String name) {
return new File(new File(contentDir,path),name);
}
protected File makeFeedReference(String path) {
//log.info("contentDir="+contentDir+", path="+path);
return new File(new File(contentDir,path),FEED_DOCUMENT_NAME);
}
protected File makeCollectionReference(String path) {
return new File(contentDir,path);
}
public void sync()
throws SQLException
{
getLogger().info("Synchronizing "+contentDir);
Iterator<org.atomojo.app.db.Feed> feeds = db.getFeeds();
while (feeds.hasNext()) {
org.atomojo.app.db.Feed feed = feeds.next();
if (feed==null) {
continue;
}
File dir = new File(contentDir,feed.getPath());
if (dir.lastModified()>feed.getSynchronizedAt().getTime()) {
getLogger().info("Changed detected in "+dir+" "+dir.lastModified()+">"+feed.getEdited().getTime());
sync(feed,dir);
}
}
}
protected void sync(final org.atomojo.app.db.Feed feed,final File dir)
throws SQLException
{
final AtomicBoolean ok = new AtomicBoolean(true);
// scan for new feeds and entries
dir.listFiles(new FileFilter() {
public boolean accept(File file) {
String name = file.getName();
if (file.isDirectory()) {
try {
org.atomojo.app.db.Feed child = feed.getChild(name);
if (child==null) {
importDir(feed,file);
}
} catch (Exception ex) {
getLogger().log(Level.SEVERE,"Error while importing directory "+file.getAbsolutePath(),ex);
ok.set(false);
}
} else {
if (name.charAt(0)=='.') {
if (!name.equals(".feed.atom") && name.endsWith(".atom")) {
name = name.substring(1);
name = name.substring(0,name.length()-5);
try {
UUID id = UUID.fromString(name);
Entry entry = feed.findEntry(id);
if (entry==null) {
importEntry(feed,file);
}
} catch (IllegalArgumentException ex) {
getLogger().warning("Ignoring entry-like file: "+file.getName());
ok.set(false);
} catch (Exception ex) {
getLogger().log(Level.SEVERE,"Error while import entry "+file.getName(),ex);
ok.set(false);
}
}
}
}
return false;
}
});
dir.listFiles(new FileFilter() {
public boolean accept(File file) {
String name = file.getName();
if (!file.isDirectory() && name.charAt(0)!='.') {
try {
EntryMedia media = feed.findEntryResource(name);
if (media==null) {
importMedia(feed,file);
}
} catch (Exception ex) {
getLogger().log(Level.SEVERE,"Error while importing media "+file.getAbsolutePath(),ex);
ok.set(false);
}
}
return false;
}
});
Iterator<org.atomojo.app.db.Feed> children = feed.getChildren();
while (children.hasNext()) {
org.atomojo.app.db.Feed child = children.next();
File childDir = new File(dir,child.getName());
if (!childDir.exists()) {
child.delete();
}
}
Iterator<Entry> entries = feed.getEntries();
while (entries.hasNext()) {
Entry entry = entries.next();
File entryFile = new File(dir,"."+entry.getUUID()+".atom");
boolean delete = false;
if (entryFile.exists()) {
Iterator<EntryMedia> resources = entry.getResources();
while (!delete && resources.hasNext()) {
File media = new File(dir,resources.next().getName());
if (!media.exists()) {
delete = true;
}
}
} else {
delete = true;
}
if (delete) {
entry.delete(new MediaEntryListener() {
public void onDelete(EntryMedia resource) {
File media = new File(dir,resource.getName());
if (media.exists()) {
media.delete();
}
}
});
if (entryFile.exists()) {
entryFile.delete();
}
}
}
if (ok.get()) {
feed.markSynchronized();
}
}
protected void importDir(org.atomojo.app.db.Feed parent,File dir)
throws SQLException,IOException,XMLException
{
// check for feed file
File feedFile = new File(dir,".feed.atom");
org.atomojo.app.db.Feed feed = null;
if (feedFile.exists()) {
// exists, so we'll import it
Document doc = loader.load(feedFile.toURI());
Feed feedObj = new Feed(doc);
String idS = feedObj.getId();
if (!idS.startsWith("urn:uuid:")) {
throw new IOException("Bad feed id: "+idS);
}
try {
UUID id = UUID.fromString(idS.substring(9));
feed = parent.createChild(dir.getName(), id);
} catch (IllegalArgumentException ex) {
throw new IOException(ex.getMessage());
}
} else {
// create new feed
feed = parent.createChild(dir.getName());
// create feed document
Document doc = AtomResource.createFeedDocument(dir.getName(),feed.getUUID(),feed.getCreated());
Writer out = new OutputStreamWriter(new FileOutputStream(feedFile),"UTF-8");
XMLWriter.writeDocument(doc, out);
out.close();
}
// import directory
sync(feed,dir);
}
protected void importEntry(final org.atomojo.app.db.Feed parent,File file)
throws SQLException,IOException,XMLException
{
Document doc = loader.load(file.toURI());
org.atomojo.app.client.Entry entryObj = new org.atomojo.app.client.Entry(doc);
String idS = entryObj.getId();
if (!idS.startsWith("urn:uuid:")) {
throw new IOException("Bad entry id: "+idS);
}
try {
UUID id = UUID.fromString(idS.substring(9));
// check for entry-related media
Text text = entryObj.getContent();
File media = null;
if (text!=null) {
String src = text.getSourceLink();
media = new File(file.getParentFile(),src);
if (!media.exists()) {
throw new IOException("Cannot find related media "+src+" to entry "+file.getAbsolutePath());
}
}
// create entry
Entry entry = parent.createEntry(id);
if (media!=null) {
// create entry media
MediaType type = MediaType.APPLICATION_OCTET_STREAM;
int dot = media.getName().lastIndexOf('.');
if (dot>0) {
String ext = media.getName().substring(dot+1);
type = MediaType.valueOf(metaService.getMetadata(ext).getName());
}
entry.createResource(media.getName(), type);
}
// edit entry date
entryObj.setEdited(entry.getEdited());
Writer out = new OutputStreamWriter(new FileOutputStream(file),"UTF-8");
XMLWriter.writeDocument(doc, out);
out.close();
// feed was edited
parent.edited();
feedUpdated(parent.getPath(),parent.getUUID(),entry.getEdited());
} catch (IllegalArgumentException ex) {
throw new IOException(ex.getMessage());
}
}
protected void importMedia(final org.atomojo.app.db.Feed parent,File file)
throws SQLException,IOException,XMLException
{
// get media type
String name = file.getName();
MediaType type = MediaType.APPLICATION_OCTET_STREAM;
int dot = name.lastIndexOf('.');
if (dot>0) {
String ext = name.substring(dot+1);
type = MediaType.valueOf(metaService.getMetadata(ext).getName());
}
// create entry for media
Entry entry = parent.createEntry();
// create entry media
entry.createResource(name, type);
// creae entry document
Document doc = AtomResource.createMediaEntryDocument(name,entry.getUUID(),entry.getCreated(),null,name,type);
Writer out = new OutputStreamWriter(new FileOutputStream(new File(file.getParentFile(),"."+entry.getUUID()+".atom")),"UTF-8");
XMLWriter.writeDocument(doc, out);
out.close();
// feed was edited
parent.edited();
feedUpdated(parent.getPath(),parent.getUUID(),entry.getEdited());
}
}