/* * Copyright 2007 Sun Microsystems, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.rometools.propono.atom.server.impl; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Locale; import java.util.StringTokenizer; import javax.activation.FileTypeMap; import javax.activation.MimetypesFileTypeMap; import org.jdom2.Document; import org.jdom2.output.XMLOutputter; import com.rometools.propono.atom.common.Categories; import com.rometools.propono.atom.common.Collection; import com.rometools.propono.atom.common.rome.AppModule; import com.rometools.propono.atom.common.rome.AppModuleImpl; import com.rometools.propono.atom.server.AtomException; import com.rometools.propono.atom.server.AtomMediaResource; import com.rometools.propono.atom.server.AtomNotFoundException; import com.rometools.propono.utils.Utilities; import com.rometools.rome.feed.WireFeed; import com.rometools.rome.feed.atom.Category; import com.rometools.rome.feed.atom.Content; import com.rometools.rome.feed.atom.Entry; import com.rometools.rome.feed.atom.Feed; import com.rometools.rome.feed.atom.Link; import com.rometools.rome.feed.module.Module; import com.rometools.rome.io.FeedException; import com.rometools.rome.io.WireFeedInput; import com.rometools.rome.io.WireFeedOutput; import com.rometools.rome.io.impl.Atom10Generator; import com.rometools.rome.io.impl.Atom10Parser; /** * File based Atom collection implementation. This is the heart of the file-based Atom service * implementation. It provides methods for adding, getting updating and deleting Atom entries and * media entries. * * @deprecated Propono will be removed in Rome 2. */ @Deprecated public class FileBasedCollection extends Collection { private String handle = null; private String singular = null; private String collection = null; private boolean inlineCats = false; private String[] catNames = null; private boolean relativeURIs = false; private String contextURI = null; private String servletPath = null; private String baseDir = null; private static final String FEED_TYPE = "atom_1.0"; /** * Construct by providing title (plain text, no HTML), a workspace handle, a plural collection * name (e.g. entries), a singular collection name (e.g. entry), the base directory for file * storage, the content-type range accepted by the collection and the root Atom protocol URI for * the service. * * @param title Title of collection (plain text, no HTML) * @param handle Workspace handle * @param collection Collection handle, plural * @param singular Collection handle, singular * @param accept Content type range accepted by collection * @param inlineCats True for inline categories * @param catNames Category names for this workspace * @param baseDir Base directory for file storage * @param relativeURIs True for relative URIs * @param contextURI Absolute URI of context that hosts APP service * @param contextPath Context path of APP service (e.g. "/sample-atomserver") * @param servletPath Servlet path of APP service (e.g. "/app") */ public FileBasedCollection(final String title, final String handle, final String collection, final String singular, final String accept, final boolean inlineCats, final String[] catNames, final boolean relativeURIs, final String contextURI, final String contextPath, final String servletPath, final String baseDir) { super(title, "text", relativeURIs ? servletPath.substring(1) + "/" + handle + "/" + collection : contextURI + servletPath + "/" + handle + "/" + collection); this.handle = handle; this.collection = collection; this.singular = singular; this.inlineCats = inlineCats; this.catNames = catNames; this.baseDir = baseDir; this.relativeURIs = relativeURIs; this.contextURI = contextURI; this.servletPath = servletPath; addAccept(accept); } /** * Get feed document representing collection. * * @throws com.rometools.rome.propono.atom.server.AtomException On error retrieving feed file. * @return Atom Feed representing collection. */ public Feed getFeedDocument() throws AtomException { InputStream in = null; synchronized (FileStore.getFileStore()) { in = FileStore.getFileStore().getFileInputStream(getFeedPath()); if (in == null) { in = createDefaultFeedDocument(contextURI + servletPath + "/" + handle + "/" + collection); } } try { final WireFeedInput input = new WireFeedInput(); final WireFeed wireFeed = input.build(new InputStreamReader(in, "UTF-8")); return (Feed) wireFeed; } catch (final Exception ex) { throw new AtomException(ex); } } /** * Get list of one Categories object containing categories allowed by collection. * * @param inline True if Categories object should contain collection of in-line Categories * objects or false if it should set the Href for out-of-line categories. */ public List<Categories> getCategories(final boolean inline) { final Categories cats = new Categories(); cats.setFixed(true); cats.setScheme(contextURI + "/" + handle + "/" + singular); if (inline) { for (final String catName : catNames) { final Category cat = new Category(); cat.setTerm(catName); cats.addCategory(cat); } } else { cats.setHref(getCategoriesURI()); } return Collections.singletonList(cats); } /** * Get list of one Categories object containing categories allowed by collection, returns * in-line categories if collection set to use in-line categories. */ @Override public List<Categories> getCategories() { return getCategories(inlineCats); } /** * Add entry to collection. * * @param entry Entry to be added to collection. Entry will be saved to disk in a directory * under the collection's directory and the path will follow the pattern * [collection-plural]/[entryid]/entry.xml. The entry will be added to the * collection's feed in [collection-plural]/feed.xml. * @throws java.lang.Exception On error. * @return Entry as it exists on the server. */ public Entry addEntry(final Entry entry) throws Exception { synchronized (FileStore.getFileStore()) { final Feed f = getFeedDocument(); final String fsid = FileStore.getFileStore().getNextId(); updateTimestamps(entry); // Save entry to file final String entryPath = getEntryPath(fsid); final OutputStream os = FileStore.getFileStore().getFileOutputStream(entryPath); updateEntryAppLinks(entry, fsid, true); Atom10Generator.serializeEntry(entry, new OutputStreamWriter(os, "UTF-8")); os.flush(); os.close(); // Update feed file updateEntryAppLinks(entry, fsid, false); updateFeedDocumentWithNewEntry(f, entry); return entry; } } /** * Add media entry to collection. Accepts a media file to be added to collection. The file will * be saved to disk in a directory under the collection's directory and the path will follow the * pattern <code>[collection-plural]/[entryid]/media/[entryid]</code>. An Atom entry will be * created to store metadata for the entry and it will exist at the path * <code>[collection-plural]/[entryid]/entry.xml</code>. The entry will be added to the * collection's feed in [collection-plural]/feed.xml. * * @param entry Entry object * @param slug String to be used in file-name * @param is Source of media data * @throws java.lang.Exception On Error * @return Location URI of entry */ public String addMediaEntry(final Entry entry, final String slug, final InputStream is) throws Exception { synchronized (FileStore.getFileStore()) { // Save media file temp file final Content content = entry.getContents().get(0); if (entry.getTitle() == null) { entry.setTitle(slug); } final String fileName = createFileName(slug != null ? slug : entry.getTitle(), content.getType()); final File tempFile = File.createTempFile(fileName, "tmp"); final FileOutputStream fos = new FileOutputStream(tempFile); Utilities.copyInputToOutput(is, fos); fos.close(); // Save media file final FileInputStream fis = new FileInputStream(tempFile); saveMediaFile(fileName, content.getType(), tempFile.length(), fis); fis.close(); final File resourceFile = new File(getEntryMediaPath(fileName)); // Create media-link entry updateTimestamps(entry); // Save media-link entry final String entryPath = getEntryPath(fileName); final OutputStream os = FileStore.getFileStore().getFileOutputStream(entryPath); updateMediaEntryAppLinks(entry, resourceFile.getName(), true); Atom10Generator.serializeEntry(entry, new OutputStreamWriter(os, "UTF-8")); os.flush(); os.close(); // Update feed with new entry final Feed f = getFeedDocument(); updateMediaEntryAppLinks(entry, resourceFile.getName(), false); updateFeedDocumentWithNewEntry(f, entry); return getEntryEditURI(fileName, false, true); } } /** * Get an entry from the collection. * * @param fsid Internal ID of entry to be returned * @throws java.lang.Exception On error * @return Entry specified by fileName/ID */ public Entry getEntry(String fsid) throws Exception { if (fsid.endsWith(".media-link")) { fsid = fsid.substring(0, fsid.length() - ".media-link".length()); } final String entryPath = getEntryPath(fsid); checkExistence(entryPath); final InputStream in = FileStore.getFileStore().getFileInputStream(entryPath); final Entry entry; final File resource = new File(fsid); if (resource.exists()) { entry = loadAtomResourceEntry(in, resource); updateMediaEntryAppLinks(entry, fsid, true); } else { entry = loadAtomEntry(in); updateEntryAppLinks(entry, fsid, true); } return entry; } /** * Get media resource wrapping a file. */ public AtomMediaResource getMediaResource(final String fileName) throws Exception { final String filePath = getEntryMediaPath(fileName); final File resource = new File(filePath); return new AtomMediaResource(resource); } /** * Update an entry in the collection. * * @param entry Updated entry to be stored * @param fsid Internal ID of entry * @throws java.lang.Exception On error */ public void updateEntry(final Entry entry, String fsid) throws Exception { synchronized (FileStore.getFileStore()) { final Feed f = getFeedDocument(); if (fsid.endsWith(".media-link")) { fsid = fsid.substring(0, fsid.length() - ".media-link".length()); } updateTimestamps(entry); updateEntryAppLinks(entry, fsid, false); updateFeedDocumentWithExistingEntry(f, entry); final String entryPath = getEntryPath(fsid); final OutputStream os = FileStore.getFileStore().getFileOutputStream(entryPath); updateEntryAppLinks(entry, fsid, true); Atom10Generator.serializeEntry(entry, new OutputStreamWriter(os, "UTF-8")); os.flush(); os.close(); } } /** * Update media associated with a media-link entry. * * @param fileName Internal ID of entry being updated * @param contentType Content type of data * @param is Source of updated data * @throws java.lang.Exception On error * @return Updated Entry as it exists on server */ public Entry updateMediaEntry(final String fileName, final String contentType, final InputStream is) throws Exception { synchronized (FileStore.getFileStore()) { final File tempFile = File.createTempFile(fileName, "tmp"); final FileOutputStream fos = new FileOutputStream(tempFile); Utilities.copyInputToOutput(is, fos); fos.close(); // Update media file final FileInputStream fis = new FileInputStream(tempFile); saveMediaFile(fileName, contentType, tempFile.length(), fis); fis.close(); final File resourceFile = new File(getEntryMediaPath(fileName)); // Load media-link entry to return final String entryPath = getEntryPath(fileName); final InputStream in = FileStore.getFileStore().getFileInputStream(entryPath); final Entry atomEntry = loadAtomResourceEntry(in, resourceFile); updateTimestamps(atomEntry); updateMediaEntryAppLinks(atomEntry, fileName, false); // Update feed with new entry final Feed f = getFeedDocument(); updateFeedDocumentWithExistingEntry(f, atomEntry); // Save updated media-link entry final OutputStream os = FileStore.getFileStore().getFileOutputStream(entryPath); updateMediaEntryAppLinks(atomEntry, fileName, true); Atom10Generator.serializeEntry(atomEntry, new OutputStreamWriter(os, "UTF-8")); os.flush(); os.close(); return atomEntry; } } /** * Delete an entry and any associated media file. * * @param fsid Internal ID of entry * @throws java.lang.Exception On error */ public void deleteEntry(final String fsid) throws Exception { synchronized (FileStore.getFileStore()) { // Remove entry from Feed final Feed feed = getFeedDocument(); updateFeedDocumentRemovingEntry(feed, fsid); final String entryFilePath = getEntryPath(fsid); FileStore.getFileStore().deleteFile(entryFilePath); final String entryMediaPath = getEntryMediaPath(fsid); if (entryMediaPath != null) { FileStore.getFileStore().deleteFile(entryMediaPath); } final String entryDirPath = getEntryDirPath(fsid); FileStore.getFileStore().deleteDirectory(entryDirPath); try { Thread.sleep(500L); } catch (final Exception ignored) { } } } private void updateFeedDocumentWithNewEntry(final Feed f, final Entry e) throws AtomException { boolean inserted = false; for (int i = 0; i < f.getEntries().size(); i++) { final Entry entry = f.getEntries().get(i); final AppModule mod = (AppModule) entry.getModule(AppModule.URI); final AppModule newMod = (AppModule) e.getModule(AppModule.URI); if (newMod.getEdited().before(mod.getEdited())) { f.getEntries().add(i, e); inserted = true; break; } } if (!inserted) { f.getEntries().add(0, e); } updateFeedDocument(f); } private void updateFeedDocumentRemovingEntry(final Feed f, final String id) throws AtomException { final Entry e = findEntry("urn:uuid:" + id, f); f.getEntries().remove(e); updateFeedDocument(f); } private void updateFeedDocumentWithExistingEntry(final Feed f, final Entry e) throws AtomException { final Entry old = findEntry(e.getId(), f); f.getEntries().remove(old); boolean inserted = false; for (int i = 0; i < f.getEntries().size(); i++) { final Entry entry = f.getEntries().get(i); final AppModule entryAppModule = (AppModule) entry.getModule(AppModule.URI); final AppModule eAppModule = (AppModule) entry.getModule(AppModule.URI); if (eAppModule.getEdited().before(entryAppModule.getEdited())) { f.getEntries().add(i, e); inserted = true; break; } } if (!inserted) { f.getEntries().add(0, e); } updateFeedDocument(f); } private Entry findEntry(final String id, final Feed feed) { for (final Entry entry : feed.getEntries()) { if (id.equals(entry.getId())) { return entry; } } return null; } private void updateFeedDocument(final Feed f) throws AtomException { try { synchronized (FileStore.getFileStore()) { final WireFeedOutput wireFeedOutput = new WireFeedOutput(); final Document feedDoc = wireFeedOutput.outputJDom(f); final XMLOutputter outputter = new XMLOutputter(); // outputter.setFormat(Format.getPrettyFormat()); final OutputStream fos = FileStore.getFileStore().getFileOutputStream(getFeedPath()); outputter.output(feedDoc, new OutputStreamWriter(fos, "UTF-8")); } } catch (final FeedException fex) { throw new AtomException(fex); } catch (final IOException ex) { throw new AtomException(ex); } } private InputStream createDefaultFeedDocument(final String uri) throws AtomException { final Feed f = new Feed(); f.setTitle("Feed"); f.setId(uri); f.setFeedType(FEED_TYPE); final Link selfLink = new Link(); selfLink.setRel("self"); selfLink.setHref(uri); f.getOtherLinks().add(selfLink); try { final WireFeedOutput wireFeedOutput = new WireFeedOutput(); final Document feedDoc = wireFeedOutput.outputJDom(f); final XMLOutputter outputter = new XMLOutputter(); // outputter.setFormat(Format.getCompactFormat()); final OutputStream fos = FileStore.getFileStore().getFileOutputStream(getFeedPath()); outputter.output(feedDoc, new OutputStreamWriter(fos, "UTF-8")); } catch (final FeedException ex) { throw new AtomException(ex); } catch (final IOException ex) { throw new AtomException(ex); } catch (final Exception e) { e.printStackTrace(); } return FileStore.getFileStore().getFileInputStream(getFeedPath()); } private Entry loadAtomResourceEntry(final InputStream in, final File file) { try { final Entry entry = Atom10Parser.parseEntry(new BufferedReader(new InputStreamReader(in)), null, Locale.US); updateMediaEntryAppLinks(entry, file.getName(), true); return entry; } catch (final Exception e) { e.printStackTrace(); return null; } } private void updateEntryAppLinks(final Entry entry, final String fsid, final boolean singleEntry) { entry.setId("urn:uuid:" + fsid); // Look for existing alt links and the alt link Link altLink = null; List<Link> altLinks = entry.getAlternateLinks(); if (altLinks != null) { for (final Link link : altLinks) { if (link.getRel() == null || "alternate".equals(link.getRel())) { altLink = link; break; } } } else { // No alt links found, so add them now altLinks = new ArrayList<Link>(); entry.setAlternateLinks(altLinks); } // The alt link not found, so add it now if (altLink == null) { altLink = new Link(); altLinks.add(altLink); } // Set correct value for the alt link altLink.setRel("alternate"); altLink.setHref(getEntryViewURI(fsid)); // Look for existing other links and the edit link Link editLink = null; List<Link> otherLinks = entry.getOtherLinks(); if (otherLinks != null) { for (final Link link : otherLinks) { if ("edit".equals(link.getRel())) { editLink = link; break; } } } else { // No other links found, so add them now otherLinks = new ArrayList<Link>(); entry.setOtherLinks(otherLinks); } // The edit link not found, so add it now if (editLink == null) { editLink = new Link(); otherLinks.add(editLink); } // Set correct value for the edit link editLink.setRel("edit"); editLink.setHref(getEntryEditURI(fsid, relativeURIs, singleEntry)); } private void updateMediaEntryAppLinks(final Entry entry, final String fileName, final boolean singleEntry) { // TODO: figure out why PNG is missing from Java MIME types final FileTypeMap map = FileTypeMap.getDefaultFileTypeMap(); if (map instanceof MimetypesFileTypeMap) { try { ((MimetypesFileTypeMap) map).addMimeTypes("image/png png PNG"); } catch (final Exception ignored) { } } entry.setId(getEntryMediaViewURI(fileName)); entry.setTitle(fileName); entry.setUpdated(new Date()); final List<Link> otherlinks = new ArrayList<Link>(); entry.setOtherLinks(otherlinks); final Link editlink = new Link(); editlink.setRel("edit"); editlink.setHref(getEntryEditURI(fileName, relativeURIs, singleEntry)); otherlinks.add(editlink); final Link editMedialink = new Link(); editMedialink.setRel("edit-media"); editMedialink.setHref(getEntryMediaEditURI(fileName, relativeURIs, singleEntry)); otherlinks.add(editMedialink); final Content content = entry.getContents().get(0); content.setSrc(getEntryMediaViewURI(fileName)); final List<Content> contents = new ArrayList<Content>(); contents.add(content); entry.setContents(contents); } /** * Create a Rome Atom entry based on a Roller entry. Content is escaped. Link is stored as * rel=alternate link. */ private Entry loadAtomEntry(final InputStream in) { try { return Atom10Parser.parseEntry(new BufferedReader(new InputStreamReader(in, "UTF-8")), null, Locale.US); } catch (final Exception e) { e.printStackTrace(); return null; } } /** * Update existing or add new app:edited. */ private void updateTimestamps(final Entry entry) { // We're not differenting between an update and an edit (yet) entry.setUpdated(new Date()); AppModule appModule = (AppModule) entry.getModule(AppModule.URI); if (appModule == null) { appModule = new AppModuleImpl(); final List<Module> modules = entry.getModules() == null ? new ArrayList<Module>() : entry.getModules(); modules.add(appModule); entry.setModules(modules); } appModule.setEdited(entry.getUpdated()); } /** * Save file to website's resource directory. * * @param handle Weblog handle to save to * @param name Name of file to save * @param size Size of file to be saved * @param is Read file from input stream */ private void saveMediaFile(final String name, final String contentType, final long size, final InputStream is) throws AtomException { final byte[] buffer = new byte[8192]; int bytesRead = 0; final File dirPath = new File(getEntryMediaPath(name)); if (!dirPath.getParentFile().exists()) { dirPath.getParentFile().mkdirs(); } OutputStream bos = null; try { bos = new FileOutputStream(dirPath.getAbsolutePath()); while ((bytesRead = is.read(buffer, 0, 8192)) != -1) { bos.write(buffer, 0, bytesRead); } } catch (final Exception e) { throw new AtomException("ERROR uploading file", e); } finally { try { bos.flush(); bos.close(); } catch (final Exception ignored) { } } } /** * Creates a file name for a file based on a weblog handle, title string and a content-type. * * @param handle Weblog handle * @param title Title to be used as basis for file name (or null) * @param contentType Content type of file (must not be null) * * If a title is specified, the method will apply the same create-anchor logic we use * for weblog entries to create a file name based on the title. * * If title is null, the base file name will be the weblog handle plus a YYYYMMDDHHSS * timestamp. * * The extension will be formed by using the part of content type that comes after he * slash. * * For example: weblog.handle = "daveblog" title = "Port Antonio" content-type = * "image/jpg" Would result in port_antonio.jpg * * Another example: weblog.handle = "daveblog" title = null content-type = * "image/jpg" Might result in daveblog-200608201034.jpg */ private String createFileName(final String title, final String contentType) { if (handle == null) { throw new IllegalArgumentException("weblog handle cannot be null"); } if (contentType == null) { throw new IllegalArgumentException("contentType cannot be null"); } String fileName = null; final SimpleDateFormat sdf = new SimpleDateFormat(); sdf.applyPattern("yyyyMMddHHssSSS"); // Determine the extension based on the contentType. This is a hack. // The info we need to map from contentType to file extension is in // JRE/lib/content-type.properties, but Java Activation doesn't provide // a way to do a reverse mapping or to get at the data. final String[] typeTokens = contentType.split("/"); final String ext = typeTokens[1]; if (title != null && !title.trim().equals("")) { // We've got a title, so use it to build file name final String base = Utilities.replaceNonAlphanumeric(title, ' '); final StringTokenizer toker = new StringTokenizer(base); String tmp = null; int count = 0; while (toker.hasMoreTokens() && count < 5) { String s = toker.nextToken(); s = s.toLowerCase(); tmp = tmp == null ? s : tmp + "_" + s; count++; } fileName = tmp + "-" + sdf.format(new Date()) + "." + ext; } else { // No title or text, so instead we'll use the item's date // in YYYYMMDD format to form the file name fileName = handle + "-" + sdf.format(new Date()) + "." + ext; } return fileName; } // ------------------------------------------------------------ URI methods private String getEntryEditURI(final String fsid, final boolean relative, final boolean singleEntry) { String entryURI = null; if (relative) { if (singleEntry) { entryURI = fsid; } else { entryURI = singular + "/" + fsid; } } else { entryURI = contextURI + servletPath + "/" + handle + "/" + singular + "/" + fsid; } return entryURI; } private String getEntryViewURI(final String fsid) { return contextURI + "/" + handle + "/" + collection + "/" + fsid + "/entry.xml"; } private String getEntryMediaEditURI(final String fsid, final boolean relative, final boolean singleEntry) { String entryURI = null; if (relative) { if (singleEntry) { entryURI = "media/" + fsid; } else { entryURI = singular + "/media/" + fsid; } } else { entryURI = contextURI + servletPath + "/" + handle + "/" + singular + "/media/" + fsid; } return entryURI; } private String getEntryMediaViewURI(final String fsid) { return contextURI + "/" + handle + "/" + collection + "/" + fsid + "/media/" + fsid; } private String getCategoriesURI() { if (!relativeURIs) { return contextURI + servletPath + "/" + handle + "/" + singular + "/categories"; } else { return servletPath + "/" + handle + "/" + singular + "/categories"; } } // ------------------------------------------------------- File path methods private String getBaseDir() { return baseDir; } private String getFeedPath() { return getBaseDir() + handle + File.separator + collection + File.separator + "feed.xml"; } private String getEntryDirPath(final String id) { return getBaseDir() + handle + File.separator + collection + File.separator + id; } private String getEntryPath(final String id) { return getEntryDirPath(id) + File.separator + "entry.xml"; } private String getEntryMediaPath(final String id) { return getEntryDirPath(id) + File.separator + "media" + File.separator + id; } private static void checkExistence(final String path) throws AtomNotFoundException { if (!FileStore.getFileStore().exists(path)) { throw new AtomNotFoundException("Entry does not exist"); } } }