/*
* App.java
*
* Created on June 8, 2007, 2:52 PM
*
* To change this template, choose Tools | Template Manager
* and open the template in the editor.
*/
package org.atomojo.app;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URLDecoder;
import java.sql.SQLException;
import java.util.Date;
import java.util.Iterator;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.atomojo.app.auth.User;
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.atomojo.app.db.Feed;
import org.atomojo.app.db.Term;
import org.infoset.xml.Document;
import org.infoset.xml.Element;
import org.infoset.xml.XMLException;
import org.infoset.xml.util.XMLWriter;
import org.restlet.data.MediaType;
import org.restlet.data.Status;
import org.restlet.representation.Representation;
import org.restlet.service.MetadataService;
/**
*
* @author alex
*/
public class App
{
public final static String DB_ATTR = "org.atomojo.app.db";
public final static String STORAGE_ATTR = "org.atomojo.app.storage";
public final static String RESOURCE_BASE_ATTR = "org.atomojo.app.resource-base";
public final static String AUTH_SERVICE_ATTR = "org.atomojo.app.auth-service";
public final static String USER_ATTR = "org.atomojo.app.user";
public static String join(String [] values, int start, int length,char delimiter) {
StringBuilder buffer = new StringBuilder();
int end = start+length;
for (int i=start; i<end; i++) {
if (i!=start) {
buffer.append(delimiter);
}
buffer.append(values[i]);
}
return buffer.toString();
}
Logger log;
DB db;
Storage storage;
MetadataService metaService;
/** Creates a new instance of App */
public App(Logger log,DB db,Storage storage,MetadataService metaService)
{
this.log = log;
this.db = db;
this.storage = storage;
this.metaService = metaService;
}
public Storage getStorage() {
return storage;
}
public DB getDB() {
return db;
}
public Feed createFeed(String feedPath,Document feedDoc)
throws AppException
{
String [] segments = feedPath.split("/");
FeedIndex index = AtomResource.indexFeed(log,feedDoc.getDocumentElement());
// Find the existing paths
Feed parent = null;
try {
parent = db.getRoot();
} catch (SQLException ex) {
throw new AppException(Status.SERVER_ERROR_INTERNAL,"Failed to get root feed due to exception.",ex);
}
if (parent==null && feedPath.length()==0) {
try {
parent = db.createRoot();
// This is the actual feed we're creating, so massage the
// document
AtomResource.mergeFeedDocument(feedDoc,parent.getUUID(),parent.getCreated(),parent.getEdited());
log.fine("Creating root...");
Status status = storage.storeFeed(parent.getPath(),parent.getUUID(),feedDoc);
if (!status.isSuccess()) {
throw new AppException(status,"Failed to create root feed.");
}
} catch (IOException ex) {
throw new AppException(Status.SERVER_ERROR_INTERNAL,"Failed to create root feed due to exception.",ex);
} catch (SQLException ex) {
throw new AppException(Status.SERVER_ERROR_INTERNAL,"Failed to create root feed due to exception.",ex);
}
} else {
if (parent==null) {
try {
parent = db.createRoot();
} catch (SQLException ex) {
throw new AppException(Status.SERVER_ERROR_INTERNAL,"Failed to get root feed due to exception.",ex);
}
log.fine("Creating root feed.");
try {
Status status = storage.storeFeed(parent.getPath(),parent.getUUID(),AtomResource.createFeedDocument("",parent.getUUID(),parent.getCreated()));
if (!status.isSuccess()) {
throw new AppException(status,"Failed to create root feed");
}
} catch (XMLException ex) {
throw new AppException(Status.SERVER_ERROR_INTERNAL,"Failed to create root feed due to exception.",ex);
} catch (IOException ex) {
throw new AppException(Status.SERVER_ERROR_INTERNAL,"Failed to create root feed due to exception.",ex);
} catch (SQLException ex) {
throw new AppException(Status.SERVER_ERROR_INTERNAL,"Failed to create root feed due to exception.",ex);
}
}
if (feedPath.length()==0) {
throw new AppException(Status.CLIENT_ERROR_CONFLICT,"The root feed already exists.");
}
int current = 0;
for (; current<segments.length; current++) {
try {
Feed next = parent.getChild(segments[current]);
if (next==null) {
break;
} else {
parent = next;
}
} catch (SQLException ex) {
throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot get segment "+segments[current]+" from database due to exception.",ex);
}
}
// If we found the whole path, the feed alreayd exists
if (current==segments.length) {
throw new AppException(Status.CLIENT_ERROR_CONFLICT,"Feed already exists at path: "+feedPath);
}
if (log.isLoggable(Level.FINE)) {
log.fine("Parentage '"+join(segments,0,current,'/')+"' already exists...");
}
// Create all the path segments
for (int i=current; i<segments.length; i++) {
String path = join(segments,0,i+1,'/');
if (log.isLoggable(Level.FINE)) {
log.fine("Creating path: "+path);
}
boolean last = i==(segments.length-1);
boolean created = false;
try {
Feed next = parent.getChild(segments[i]);
if (next!=null && last) {
// This shouldn't happen
throw new AppException(Status.CLIENT_ERROR_CONFLICT,"Collection already exists at "+path);
}
if (next==null) {
parent = parent.createChild(segments[i],last ? index.getId() : null);
created = true;
} else {
parent = next;
}
} catch (SQLException ex) {
throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot create segment "+segments[i]+" due to SQL exception.",ex);
}
// Create the feeds along the way
if (last) {
// This is the actual feed we're creating, so massage the
// document
AtomResource.mergeFeedDocument(feedDoc,parent.getUUID(),parent.getCreated(),parent.getEdited());
}
if (created) {
if (log.isLoggable(Level.FINE)) {
log.fine("Creating feed for: "+path);
}
try {
Status status = storage.storeFeed(parent.getPath(),parent.getUUID(),last ? feedDoc : AtomResource.createFeedDocument(segments[i],parent.getUUID(),parent.getCreated()));
if (!status.isSuccess()) {
throw new AppException(Status.SERVER_ERROR_INTERNAL,"Failed to create feed at path "+path);
}
} catch (XMLException ex) {
throw new AppException(Status.SERVER_ERROR_INTERNAL,"Failed to create feed at path "+path+" due to exception.",ex);
} catch (IOException ex) {
throw new AppException(Status.SERVER_ERROR_INTERNAL,"Failed to create feed at path "+path+" due to exception.",ex);
} catch (SQLException ex) {
throw new AppException(Status.SERVER_ERROR_INTERNAL,"Failed to create feed at path "+path+" due to exception.",ex);
}
}
}
}
// Index the feed categories
try {
for (URI term : index.keySet()) {
Object value = index.get(term);
Term t = parent.getDB().createTerm(term);
parent.categorize(t,value);
}
} catch (SQLException ex) {
log.log(Level.SEVERE,"Cannot categorize entry due to SQL Exception. Continuing...",ex);
}
return parent;
}
public Feed getFeed(String path)
throws AppException
{
String [] segments = path.split("/");
try {
Feed f = db.findFeedByPath(segments);
if (f==null) {
throw new AppException(Status.CLIENT_ERROR_NOT_FOUND,"Feed at path '"+path+"' cannot be found.");
}
return f;
} catch (SQLException ex) {
throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot feed segments from database due to exception.",ex);
}
}
public void updateFeed(Feed feed,Document doc)
throws AppException
{
// Check to make sure the document is an entry
Element top = doc.getDocumentElement();
if (!top.getName().equals(AtomResource.FEED_NAME)) {
throw new AppException(Status.CLIENT_ERROR_BAD_REQUEST,"Document element is not a feed: "+top.getName());
}
try {
feed.edited();
} catch (SQLException ex) {
throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot mark feed as edited.",ex);
}
// Index the feed categories
FeedIndex index = AtomResource.indexFeed(log,top);
try {
feed.uncategorize();
for (URI term : index.keySet()) {
Object value = index.get(term);
Term t = feed.getDB().createTerm(term);
feed.categorize(t,value);
}
} catch (SQLException ex) {
throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot categorize entry due to SQL Exception.",ex);
}
AtomResource.mergeFeedDocument(doc,feed.getUUID(),feed.getCreated(),feed.getEdited());
try {
Status status = storage.storeFeed(feed.getPath(),feed.getUUID(),doc);
if (!status.isSuccess()) {
throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot store udpated XML");
}
} catch (IOException ex) {
throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot store updated XML.",ex);
} catch (SQLException ex) {
throw new AppException(Status.SERVER_ERROR_INTERNAL,"Database error while updating feed.",ex);
}
}
public void delete(Feed feed)
throws AppException
{
try {
String path = feed.getPath();
feed.delete();
if (!storage.deleteFeed(path,feed.getUUID())) {
throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot delete feed collection.");
}
} catch (IOException ex) {
throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot delete feed from storage.",ex);
} catch (SQLException ex) {
throw new AppException(Status.SERVER_ERROR_INTERNAL,"Database error while deleting feed.",ex);
}
}
public Entry createEntry(User user,Feed feed,Document entryDoc)
throws AppException
{
// Check for non-entry document elements
Element top = entryDoc.getDocumentElement();
if (!top.getName().equals(AtomResource.ENTRY_NAME)) {
throw new AppException(Status.CLIENT_ERROR_BAD_REQUEST,"Only entries can be posted to feeds with the Atom mime type. Found: "+top.getName());
}
EntryIndex index = AtomResource.indexEntry(log,top);
if (index.getId()!=null) {
try {
if (feed.findEntry(index.getId())!=null) {
index.setId(UUID.randomUUID());
}
} catch (SQLException ex) {
throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot find entry in database.",ex);
}
} else {
index.setId(UUID.randomUUID());
}
// Create entry index
Entry entry = null;
try {
entry = feed.createEntry(index.getId());
} catch (SQLException ex) {
throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot create entry in database.",ex);
}
try {
for (URI term : index.keySet()) {
Object value = index.get(term);
Term t = feed.getDB().createTerm(term);
entry.categorize(t,value);
}
} catch (SQLException ex) {
throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot categorize entry due to SQL Exception.",ex);
}
// Get the identity's author
String authorName = user.getName();
// Store in XML DB
AtomResource.mergeEntry(top,entry.getUUID(),entry.getCreated(),entry.getEdited(),authorName,null);
try {
Status status = storage.storeEntry(feed.getPath(),feed.getUUID(),entry.getUUID(),entryDoc);
if (!status.isSuccess()) {
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
log.log(Level.SEVERE,"Cannot serialize entry for error message.",ex);
}
throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot store entry, status="+status.getCode()+"\n"+xml);
}
storage.feedUpdated(feed.getPath(),feed.getUUID(),feed.getEdited());
} catch (SQLException ex) {
try {
entry.delete(null);
} catch (SQLException ox) {
log.log(Level.SEVERE,"Cannot delete entry for exception cleanup.",ox);
}
throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot store entry.",ex);
} catch (IOException ex) {
try {
entry.delete(null);
} catch (SQLException ox) {
log.log(Level.SEVERE,"Cannot delete entry for exception cleanup.",ox);
}
throw new AppException(Status.SERVER_ERROR_INTERNAL,"Database error while storing entry.",ex);
}
return entry;
}
public Entry createMediaEntry(User user,Feed feed, Representation entity, String slug, UUID id)
throws AppException
{
// Create entry index
Entry entry = null;
try {
entry = feed.createEntry(id);
} catch (SQLException ex) {
throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot create entry in database.",ex);
}
String file = slug;
try {
InputStream is = entity.getStream();
MediaType mediaType = entity.getMediaType();
if (mediaType==null) {
throw new AppException(Status.CLIENT_ERROR_BAD_REQUEST,"The media type is missing.");
}
MediaType baseMediaType = mediaType.valueOf(mediaType.getName());
// Make sure we have a filename for the media resource
if (file==null) {
String ext = metaService.getExtension(baseMediaType);
if (ext!=null) {
file = entry.getUUID()+"."+ext;
} else {
file = entry.getUUID().toString();
}
slug = entry.getUUID().toString();
} else if (file.indexOf('.')<0) {
String ext = metaService.getExtension(baseMediaType);
if (ext!=null) {
file += "."+ext;
}
}
EntryMedia media;
try {
media = entry.createResource(file,mediaType);
} catch (SQLException ex) {
try {
entry.delete(null);
} catch (SQLException ox) {
log.log(Level.SEVERE,"Cannot delete entry for exception cleanup.",ox);
}
throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot create entry media resource in database.",ex);
}
if (media==null) {
throw new AppException(Status.CLIENT_ERROR_BAD_REQUEST,"Media entry name "+file+" refused.");
}
// Get author name for identity
String authorName = user.getName();
// Create entry document
Document doc = null;
try {
String title = URLDecoder.decode(slug,"UTF-8");
doc = AtomResource.createMediaEntryDocument(title,entry.getUUID(),entry.getCreated(),authorName,file,mediaType);
} catch (XMLException ex) {
try {
entry.delete(null);
} catch (SQLException ox) {
log.log(Level.SEVERE,"Cannot delete entry for exception cleanup.",ox);
}
throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot create media entry document.",ex);
} catch (UnsupportedEncodingException ex) {
try {
entry.delete(null);
} catch (SQLException ox) {
log.log(Level.SEVERE,"Cannot delete entry for exception cleanup.",ox);
}
throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot decode slug for media entry title.",ex);
}
try {
String path = feed.getPath();
Status entryStatus = storage.storeEntry(path,feed.getUUID(),entry.getUUID(),doc);
if (entryStatus.isSuccess()) {
Status mediaStatus = storage.storeMedia(path,feed.getUUID(),media.getName(),mediaType,is);
// TODO: storage may change media type parameters and the entry needs to be updated
if (!mediaStatus.isSuccess()) {
try {
entry.delete(null);
} catch (SQLException ox) {
log.log(Level.SEVERE,"Cannot delete entry for exception cleanup.",ox);
}
try {
storage.deleteEntry(path,feed.getUUID(),entry.getUUID());
} catch (IOException ex) {
log.log(Level.SEVERE,"Cannot delete entry storage for exception cleanup.",ex);
}
throw new AppException(mediaStatus,"Cannot store entry's media (refused)");
} else {
storage.feedUpdated(feed.getPath(),feed.getUUID(),feed.getEdited());
}
} else {
try {
entry.delete(null);
} catch (SQLException ox) {
log.log(Level.SEVERE,"Cannot delete entry for exception cleanup.",ox);
}
throw new AppException(entryStatus,"Cannot store entry (refused)");
}
} catch (SQLException ex) {
try {
entry.delete(null);
} catch (SQLException ox) {
log.log(Level.SEVERE,"Cannot delete entry for exception cleanup.",ox);
}
throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot store entry.",ex);
}
} catch (IOException ex) {
try {
entry.delete(null);
} catch (SQLException ox) {
log.log(Level.SEVERE,"Cannot delete entry for exception cleanup.",ox);
}
throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot store entry due to request I/O exception.",ex);
}
return entry;
}
public Representation getEntryRepresentation(String feedBaseURI,Feed feed, UUID id)
throws AppException
{
try {
Entry entry = feed.findEntry(id);
if (entry==null) {
throw new AppException(Status.CLIENT_ERROR_NOT_FOUND,"Entry "+id+" not found.");
}
return getEntryRepresentation(feedBaseURI,entry);
} catch (SQLException ex) {
throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot get entry XML.",ex);
}
}
public Representation getEntryRepresentation(String feedBaseURI,Entry entry)
throws AppException
{
try {
Feed feed = entry.getFeed();
return storage.getEntry(feedBaseURI,feed.getPath(),feed.getUUID(),entry.getUUID());
} catch (IOException ex) {
throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot get entry XML.",ex);
} catch (SQLException ex) {
throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot get entry XML.",ex);
}
}
public Entry updateEntry(User user,Feed feed, UUID entryId,Document doc)
throws AppException
{
try {
Entry entry = feed.findEntry(entryId);
return updateEntry(user,feed,entry,doc);
} catch (SQLException ex) {
throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot get entry "+entryId+" from feed "+feed.getUUID(),ex);
}
}
public Entry updateEntry(User user,Feed feed, Entry entry,Document doc)
throws AppException
{
// Check to make sure the document is an entry
Element top = doc.getDocumentElement();
if (!top.getName().equals(AtomResource.ENTRY_NAME)) {
throw new AppException(Status.CLIENT_ERROR_BAD_REQUEST,"Document element is not an entry: "+top.getName());
}
// Set the modification time
Date modified = null;
try {
modified = entry.edited();
} catch (SQLException ex) {
throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot mark entry as edited.",ex);
}
try {
EntryIndex index = AtomResource.indexEntry(log,top);
entry.uncategorize();
for (URI term : index.keySet()) {
Object value = index.get(term);
Term t = entry.getDB().createTerm(term);
entry.categorize(t,value);
}
} catch (SQLException ex) {
throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot categorize entry due to SQL Exception.",ex);
}
EntryMedia media = null;
try {
Iterator<EntryMedia> resources = entry.getResources();
if (resources.hasNext()) {
media = resources.next();
while (resources.hasNext()) {
resources.next();
}
}
} catch (SQLException ex) {
throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot check for media resources for entry.",ex);
}
// Get author name for identity
String authorName = user.getName();
// merge the data with the new entry
AtomResource.mergeEntry(top,entry.getUUID(),entry.getCreated(),modified,authorName,media);
try {
Status status = storage.storeEntry(feed.getPath(),feed.getUUID(),entry.getUUID(),doc);
if (!status.isSuccess()) {
throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot store entry document.");
}
storage.feedUpdated(feed.getPath(),feed.getUUID(),feed.getEdited());
} catch (IOException ex) {
throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot store entry document.",ex);
} catch (SQLException ex) {
throw new AppException(Status.SERVER_ERROR_INTERNAL,"Database error while storing entry document.",ex);
}
return entry;
}
public void deleteEntry(Feed feed,UUID id)
throws AppException
{
try {
Entry entry = feed.findEntry(id);
deleteEntry(feed,entry);
} catch (SQLException ex) {
throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot get entry "+id,ex);
}
}
public void deleteEntry(final Feed feed,Entry entry)
throws AppException
{
String path;
try {
path = feed.getPath();
} catch (SQLException ex) {
throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot get feed path.",ex);
}
final String fpath = path;
try {
entry.delete(new MediaEntryListener() {
public void onDelete(EntryMedia resource) {
try {
storage.deleteMedia(fpath,feed.getUUID(),resource.getName());
} catch (IOException ex) {
log.log(Level.SEVERE,"Cannot delete media: "+resource.getName(),ex);
}
}
});
} catch (SQLException ex) {
throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot enumerate entry media.",ex);
}
try {
if (!storage.deleteEntry(fpath,feed.getUUID(),entry.getUUID())) {
throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot delete entry document.");
}
Date date = feed.touch();
storage.feedUpdated(feed.getPath(),feed.getUUID(),date);
} catch (SQLException ex) {
throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot update feed updated element",ex);
} catch (IOException ex) {
throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot delete entry document",ex);
}
}
public void updateMedia(Feed feed, String file, Representation entity)
throws AppException
{
try {
EntryMedia media = feed.findEntryResource(file);
MediaType mediaType = entity.getMediaType();
mediaType = MediaType.valueOf(mediaType.getName());
media.setMediaType(mediaType);
storage.storeMedia(feed.getPath(),feed.getUUID(),media.getName(),mediaType,entity.getStream());
media.edited();
} catch (IOException ex) {
throw new AppException(Status.SERVER_ERROR_INTERNAL,"Cannot update entry media "+file,ex);
} catch (SQLException ex) {
throw new AppException(Status.SERVER_ERROR_INTERNAL,"Database error while updating entry media "+file,ex);
}
}
}