Java tutorial
/* Copyright (c) 2007 HomeAway, Inc. * All rights reserved. http://www.atomserver.org * * 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 org.atomserver.core; import org.apache.abdera.Abdera; import org.apache.abdera.factory.Factory; import org.apache.abdera.i18n.iri.IRI; import org.apache.abdera.i18n.iri.IRISyntaxException; import org.apache.abdera.model.*; import org.apache.abdera.protocol.server.RequestContext; import org.apache.abdera.util.Constants; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.atomserver.*; import org.atomserver.core.etc.AtomServerConstants; import org.atomserver.core.hashgenerators.SimpleContentHashGenerator; import org.atomserver.core.utils.HashUtils; import org.atomserver.exceptions.AtomServerException; import org.atomserver.exceptions.BadContentException; import org.atomserver.exceptions.BadRequestException; import org.atomserver.ext.batch.Operation; import org.atomserver.monitor.EntriesMonitor; import org.atomserver.server.servlet.AtomServerUserInfo; import org.atomserver.uri.*; import org.atomserver.utils.perf.AtomServerPerfLogTagFormatter; import org.atomserver.utils.perf.AtomServerStopWatch; import org.atomserver.utils.xml.XML; import org.perf4j.StopWatch; import java.io.IOException; import java.text.MessageFormat; import java.util.*; import java.util.Collection; /** * The abstract, base AtomCollection implementation. Subclasses must implement several specific * methods for manipulating Entries; getEntries, getEntry, modifyEntry, and deleteEntry. * * @author Chris Berry (chriswberry at gmail.com) * @author Bryon Jacob (bryon at jacob.net) */ abstract public class AbstractAtomCollection implements AtomCollection { //-------------------------------- // abstract methods //-------------------------------- /** * The getEntries() method on the AtomCollection API delegates to this method within the subclass * to do the real work. This method will, most likely, return only a "page" of results. * * @param abdera * @param iri * @param feedTarget The FeedTarget, which was decoded from the URI * @param updatedMin The minimum update Date determined using either the Header or a Query param. * @param updatedMax The maximum update Date determined using a Query param. * @param feed The Feed to which to add Entries * @return The last update Date (as a long) for an Entry in this Feed. * Used to set the <updated> element in the Feed * @throws AtomServerException */ abstract protected long getEntries(Abdera abdera, IRI iri, FeedTarget feedTarget, Date updatedMin, Date updatedMax, Feed feed) throws AtomServerException; /** * The getEntry() method on the AtomCollection API delegates to this method within the subclass * to do the real work. * * @param entryTarget The EntryTarget, which was decoded from the URI * @return The EntryMetaData associated with the requested Entry. * @throws AtomServerException */ abstract protected EntryMetaData getEntry(EntryTarget entryTarget) throws AtomServerException; /** * The updateEntry() method on the AtomCollection API delegates to this method within the subclass * to do the real work. * * @param internalId The internalId of this Entry. In general, this is pre-selected from the database, * although this is not required. The method getInternalId() is used to obtain thisi value. * It is used to determine whether this is an insert or an update within modifyEntry() * @param entryTarget The EntryTarget, which was decoded from the URI * @param mustAlreadyExist Should this Entry already exist? * @return The EntryMetaData associated with the requested Entry. * @throws AtomServerException */ abstract protected EntryMetaDataStatus modifyEntry(Object internalId, EntryTarget entryTarget, boolean mustAlreadyExist) throws AtomServerException; /** * Second call to modify Entry after determining that the categories have changed even though the content * has not. This is called only for the Entry already existing. * * @param internalId The internalId of this Entry. * @param entryTarget EntryTarget to update * @return * @throws AtomServerException */ abstract protected EntryMetaDataStatus reModifyEntry(Object internalId, EntryTarget entryTarget) throws AtomServerException; /** * The deleteEntry() method on the AtomCollection API delegates to this method within the subclass * to do the real work. * * @param entryTarget The EntryTarget, which was decoded from the URI * @param setDeletedFlag Should the deleted flag be set for this Entry? * @return The EntryMetaData associated with the requested Entry. * @throws AtomServerException */ abstract protected EntryMetaData deleteEntry(EntryTarget entryTarget, boolean setDeletedFlag) throws AtomServerException; /** * The deleteEntry() method on the AtomCollection API delegates to this method within the subclass * with the obliterate flag thrown * * @param entryDescriptor basically the EntryTarget */ abstract protected void obliterateEntry(EntryDescriptor entryDescriptor); // ---------- // statics // ---------- static private final Log log = LogFactory.getLog(AbstractAtomCollection.class); // ------------ // instance // ------------ protected CollectionOptions options = null; protected AtomWorkspace parentAtomWorkspace = null; protected String name = null; // ----------- // methods // ----------- public AbstractAtomCollection(AtomWorkspace parentAtomWorkspace, String name) { this.parentAtomWorkspace = parentAtomWorkspace; this.name = name; } /** * {@inheritDoc} */ public void setParentAtomWorkspace(AtomWorkspace parentAtomWorkspace) { this.parentAtomWorkspace = parentAtomWorkspace; } /** * Return the AtomWorkspace to which this AtomCollection belongs * * @return */ public AtomWorkspace getParentAtomWorkspace() { return parentAtomWorkspace; } /** * {@inheritDoc} */ public String getName() { return name; } /** * {@inheritDoc} */ public ContentStorage getContentStorage() { if (options != null && options.getContentStorage() != null) { return options.getContentStorage(); } else { return parentAtomWorkspace.getOptions().getDefaultContentStorage(); } } /** * {@inheritDoc} */ public ContentValidator getContentValidator() { if (options != null && options.getContentValidator() != null) { return options.getContentValidator(); } else { return parentAtomWorkspace.getOptions().getDefaultContentValidator(); } } /** * {@inheritDoc} */ public CategoriesHandler getCategoriesHandler() { return ((AbstractAtomService) parentAtomWorkspace.getParentAtomService()).getCategoriesHandler(); } /** * * A convenience method to obtain the ContentHashGenerator wired into this AtomCollection * * @return ContentHashGenerator object */ protected ContentHashGenerator getContentHashFunction() { if (options != null && options.getContentHashGenerator() != null) { return options.getContentHashGenerator(); } else { ContentHashGenerator hashFunc = parentAtomWorkspace.getOptions().getDefaultContentHashFunction(); return (hashFunc != null) ? hashFunc : new SimpleContentHashGenerator(); } } /** * Decode the Entry Target from the URI. Content hash code will not be set. * * @param request * @return */ protected EntryTarget getEntryTarget(RequestContext request) { return getURIHandler().getEntryTarget(request, true); } /** * Decode the Entry Target from the URI and set the content hash code. THe content hash code will be retrieved * from the Entry if it is already there, or compute from the content itself if it is not in the Entry. * * @param request * @param entry * @param entryXml * @return */ protected EntryTarget getEntryTarget(RequestContext request, Entry entry, String entryXml) { EntryTarget target = getEntryTarget(request); setTargetContentHashCode(target, entry, entryXml); return target; } protected boolean mustAlreadyExist() { return false; } protected boolean setDeletedFlag() { return true; } /** * {@inheritDoc} */ public EntryAutoTagger getAutoTagger() { if (options != null && options.getAutoTagger() != null) { return options.getAutoTagger(); } else { return parentAtomWorkspace.getOptions().getDefaultAutoTagger(); } } /** * Return the maximum number of "link" Entries to be returned in a page of results. * A "link" Entry is a an <entry> which contains a <link> which the Client may subsequently use to access the * actual Entry (which would contain the Entry's <content>) * * @return The maximum number of "link" Entries to be returned in a page of results */ protected int getMaxLinkEntriesPerPage() { if (options != null && options.getMaxFullEntriesPerPage() != -1) { return options.getMaxLinkEntriesPerPage(); } else { return parentAtomWorkspace.getOptions().getDefaultMaxLinkEntriesPerPage(); } } /** * Return the maximum number of "full" Entries to be returned in a page of results. * A "full" Entry is a an <entry> which contains the entire Entry, including it's <content>. * * @return */ protected int getMaxFullEntriesPerPage() { if (options != null && options.getMaxFullEntriesPerPage() != -1) { return options.getMaxFullEntriesPerPage(); } else { return parentAtomWorkspace.getOptions().getDefaultMaxFullEntriesPerPage(); } } /** * A convenience method to obtain the return the EntryIdGenerator wired into this AtomCollection * * @return The EntryIdGenerator */ protected EntryIdGenerator getEntryIdGenerator() { if (options != null && options.getEntryIdGenerator() != null) { return options.getEntryIdGenerator(); } else { return parentAtomWorkspace.getOptions().getDefaultEntryIdGenerator(); } } /** * A convenience method to obtain the isLocalized property from the Options * * @return Whether this Collection is Locale specific, which is reflected in it's URL structure, etc. */ protected boolean isLocalized() { // TODO: optionally use CollectionOptions return parentAtomWorkspace.getOptions().getDefaultLocalized(); } /** * A convenience method to obtain the isVerboseDeletions property from the Options. * Verbose deletions return the <content> of the Entry, wrapped in a <delete> element * * @return Whether this Collection */ protected boolean isVerboseDeletions() { // TODO: optionally use CollectionOptions return parentAtomWorkspace.getOptions().getDefaultVerboseDeletions(); } /** * A convenience method to obtain the isProducingEntryCategoriesFeedElement property from the Options * * @return Whether this Collection produces the <category> elements in the Feed */ protected boolean isProducingEntryCategoriesFeedElement() { // TODO: optionally use CollectionOptions return parentAtomWorkspace.getOptions().getDefaultProducingEntryCategoriesFeedElement(); } /** * A convenience method to obtain the isProducingTotalResultsFeedElement property from the Options * * @return Whether this Collection produces teh <totalResults> element in the Feed */ protected boolean isProducingTotalResultsFeedElement() { // TODO: optionally use CollectionOptions return parentAtomWorkspace.getOptions().getDefaultProducingTotalResultsFeedElement(); } /** * {@inheritDoc} */ public CollectionOptions getOptions() { return options; } /** * {@inheritDoc} */ public void setOptions(CollectionOptions options) { this.options = options; } /** * A convenience method to obtain the URIHandler from the AtomService which owns this AtomCollection * * @return The URIHandler */ protected URIHandler getURIHandler() { return parentAtomWorkspace.getParentAtomService().getURIHandler(); } /** * A convenience method to pull the Service Base URI from the affliated AtomService. * * @return The Service Base URI */ protected String getServiceBaseUri() { return parentAtomWorkspace.getParentAtomService().getServiceBaseUri(); } /** * Retrieve EntriesMonitor */ protected EntriesMonitor getEntriesMonitor() { return parentAtomWorkspace.getParentAtomService().getEntriesMonitor(); } /** * Spring configured flag on workspace indicating if the entry should be updated regardless of the content being * the same or not. * * @return true if the entry is to be updated when the content is the same. */ public boolean alwaysUpdateEntry() { return parentAtomWorkspace.getOptions().isAlwaysUpdateEntry(); } /** * A "batch method" which calls modifyEntry() * This method should be overriden whenever the concrete implementation can take advantage of batching to * do a better job, but this simple implementation will suffice for functional correctness, by simply iterating * over the batch and calling the one-at-a-time methods above. * * @param request The Abdera RequestContext * @param entriesURIData The list of EntryURIData for this batch * @return A list of BatchEntryResults * @throws AtomServerException */ protected java.util.Collection<BatchEntryResult> modifyEntries(final RequestContext request, final java.util.Collection<EntryTarget> entriesURIData) throws AtomServerException { return executeTransactionally(new TransactionalTask<Collection<BatchEntryResult>>() { public Collection<BatchEntryResult> execute() { java.util.Collection<BatchEntryResult> beans = new ArrayList<BatchEntryResult>(); for (EntryTarget entryTarget : entriesURIData) { try { EntryMetaDataStatus metaDataStatus = modifyEntry(null, entryTarget, false); beans.add(new BatchEntryResult(entryTarget, metaDataStatus.getEntryMetaData(), metaDataStatus.isModified())); } catch (Exception e) { beans.add(new BatchEntryResult(entryTarget, e)); } } return beans; } }); } /** * A "batch method" which calls deleteEntry() * This method should be overriden whenever the concrete implementation can take advantage of batching to * do a better job, but this simple implementation will suffice for functional correctness, by simply iterating * over the batch and calling the one-at-a-time methods above. * * @param request The Abdera RequestContext * @param entriesURIData The list of EntryURIData for this batch * @return A list of BatchEntryResults * @throws AtomServerException */ protected java.util.Collection<BatchEntryResult> deleteEntries(final RequestContext request, final java.util.Collection<EntryTarget> entriesURIData) throws AtomServerException { return executeTransactionally(new TransactionalTask<Collection<BatchEntryResult>>() { public Collection<BatchEntryResult> execute() { java.util.Collection<BatchEntryResult> beans = new ArrayList<BatchEntryResult>(); for (EntryTarget entryTarget : entriesURIData) { try { EntryMetaData entryMetaData = deleteEntry(entryTarget, true); beans.add(new BatchEntryResult(entryTarget, entryMetaData, true)); } catch (Exception e) { beans.add(new BatchEntryResult(entryTarget, e)); } } return beans; } }); } /** * {@inheritDoc} */ public java.util.Collection<org.apache.abdera.model.Category> listCategories(RequestContext request, String workspace, String collection) { CategoriesHandler categoriesHandler = getCategoriesHandler(); if (categoriesHandler != null) { return categoriesHandler.listCategories(workspace, collection); } return null; } /** * {@inheritDoc} */ public Feed getEntries(RequestContext request) throws AtomServerException { Abdera abdera = request.getServiceContext().getAbdera(); FeedTarget feedTarget = getURIHandler().getFeedTarget(request); Date updatedMin = getUpdatedMin(feedTarget, request); Date updatedMax = feedTarget.getUpdatedMaxParam(); if (updatedMax != null && updatedMin.after(updatedMax)) { String msg = "updated-min (" + updatedMin + ") is after updated-max (" + updatedMax + ")"; log.error(msg); throw new BadRequestException(msg); } Feed feed = AtomServer.getFactory(abdera).newFeed(); long lastUpdated = getEntries(request.getServiceContext().getAbdera(), request.getUri(), feedTarget, updatedMin, updatedMax, feed); if (lastUpdated != 0L) { try { String collection = feedTarget.getCollection(); feed.addAuthor("AtomServer APP Service"); feed.setTitle(collection + " entries"); feed.setUpdated(new java.util.Date(lastUpdated)); feed.setId("tag:atomserver.org,2008:v1:" + collection); } catch (IRISyntaxException e) { throw new BadRequestException(e); } return feed; } else { // AtomServer will interpret null as "NOT MODIFIED" return null; } } /** * {@inheritDoc} */ public Entry getEntry(RequestContext request) throws AtomServerException { Abdera abdera = request.getServiceContext().getAbdera(); EntryTarget entryTarget = getEntryTarget(request); if (entryTarget.getRawRevision() != null) { throw new BadRequestException("Do NOT include the revision number when GET-ing an Entry"); } EntryMetaData entryMetaData = getEntry(entryTarget); Date thisLastUpdated = (entryMetaData.getUpdatedDate() != null) ? entryMetaData.getUpdatedDate() : AtomServerConstants.ZERO_DATE; Date updatedMin = getUpdatedMin(entryTarget, request); Date updatedMax = (entryTarget.getUpdatedMaxParam() != null) ? entryTarget.getUpdatedMaxParam() : AtomServerConstants.FAR_FUTURE_DATE; Entry entry = null; if ((thisLastUpdated.after(updatedMin) || thisLastUpdated.equals(updatedMin)) && thisLastUpdated.before(updatedMax)) { EntryType entryType = (entryTarget.getEntryTypeParam() != null) ? entryTarget.getEntryTypeParam() : EntryType.full; entry = newEntry(abdera, entryMetaData, entryType); } return entry; } /** * {@inheritDoc} */ public UpdateCreateOrDeleteEntry.CreateOrUpdateEntry updateEntry(final RequestContext request) throws AtomServerException { StopWatch stopWatch1 = new AtomServerStopWatch(); final Entry entry; final String entryXml; final EntryTarget entryTarget; Abdera abdera; try { abdera = request.getServiceContext().getAbdera(); entryTarget = getURIHandler().getEntryTarget(request, false); ensureCollectionExists(entryTarget.getCollection()); entry = parseEntry(entryTarget, request); entryXml = validateAndPreprocessEntryContents(entry, entryTarget); if (getEntriesMonitor() != null) { getEntriesMonitor().updateNumberOfEntriesToUpdate(1); } } finally { stopWatch1.stop("AC.updateEntry.preProc", ""); } final String t_user = AtomServerUserInfo.getUser(); EntryMetaData entryMetaData = null; StopWatch stopWatch2 = new AtomServerStopWatch(); try { EntryMetaDataStatus entryMetaDataStatus = executeTransactionally( new TransactionalTask<EntryMetaDataStatus>() { public EntryMetaDataStatus execute() { AtomServerUserInfo.setUser(t_user); EntryTarget target = getEntryTarget(request, entry, entryXml); // determine if we are creating the entryId -- i.e. if this was a POST if (EntryTarget.UNASSIGNED_ID.equals(target.getEntryId())) { if (getEntryIdGenerator() == null) { throw new AtomServerException( "No EntryIdGenerator was wired into the Collection (" + target.toString() + ")"); } else { target.setEntryId(getEntryIdGenerator().generateId()); } } final Object internalId = getInternalId(target); EntryMetaDataStatus metaDataStatus = modifyEntry(internalId, target, mustAlreadyExist()); updateEntryCategories(metaDataStatus.getEntryMetaData(), entry); // Update category to see if there are changes. // Assumption here: postProcessEntryContents method does not need entry revision or timestamps. boolean categoriesUpdated = postProcessEntryContents(entryXml, metaDataStatus.getEntryMetaData()); // If both category and contents are not modified, no need to update. if (!metaDataStatus.isModified() && !categoriesUpdated) { if (getEntriesMonitor() != null) { getEntriesMonitor().updateNumberOfEntriesNotUpdatedDueToSameContent(1); } return metaDataStatus; } // if content is not modified but the categories are, call reModifyEntry to update rev/timestamp if (!metaDataStatus.isModified()) { metaDataStatus = reModifyEntry(internalId, entryTarget); } // update contents // Copy the new file contents into the File // do this as late as possible -- when we're completely sure that it has all passed if (log.isTraceEnabled()) { log.trace("ContentStorage = " + getContentStorage()); } getContentStorage().putContent(entryXml, metaDataStatus.getEntryMetaData()); if (getEntriesMonitor() != null) { getEntriesMonitor().updateNumberOfEntriesActuallyUpdated(1); } return metaDataStatus; } }); // For Create and Update, we always, by definition, return "full" Entries entryMetaData = entryMetaDataStatus.getEntryMetaData(); entryMetaData.setWorkspace(entryTarget.getWorkspace()); Entry newEntry = newEntry(abdera, entryMetaData, EntryType.full); newEntry.addSimpleExtension(AtomServerConstants.CONTENT_HASH, HashUtils.convertUUIDStandardToSimpleFormat(entryMetaData.getContentHashCode())); newEntry.addSimpleExtension(AtomServerConstants.ENTRY_UPDATED, (entryMetaDataStatus.isModified() ? "true" : "false")); if (log.isDebugEnabled()) { log.debug(" ** EntryId:" + entryMetaData.getEntryId() + (entryMetaData.isNewlyCreated() ? " Inserted" : " No-Insert") + (entryMetaData.isNewlyCreated() ? " " : " Modified:" + (entryMetaDataStatus.isModified() ? "Yes" : "No")) + " hashCode: " + entryMetaData.getContentHashCode()); } return new UpdateCreateOrDeleteEntry.CreateOrUpdateEntry(newEntry, entryMetaData.isNewlyCreated()); } finally { stopWatch2.stop("AC.updateEntry.Proc", AtomServerPerfLogTagFormatter.getPerfLogEntryString(entryMetaData)); } } private void updateEntryCategories(EntryMetaData entryMetaData, Entry entry) { // if categories were passed in on the entry, we should modify the set of entries to // exactly match that set. if (entry.getCategories() != null && !entry.getCategories().isEmpty()) { // first, grab the set of current categories on the entry, and call that "toDelete" // (we'll be removing the ones that we shouldn't delete) Set<EntryCategory> toDelete = new HashSet<EntryCategory>(entryMetaData.getCategories()); // create a new empty list of categories called "toInsert" - we'll add "new" categories // here List<EntryCategory> toInsert = new ArrayList<EntryCategory>(); for (Category category : entry.getCategories()) { // convert each category to an entry category, attached to the entry by the DB ID. EntryCategory entryCategory = new EntryCategory(); entryCategory.setEntryStoreId(entryMetaData.getEntryStoreId()); entryCategory.setScheme(category.getScheme().toString()); entryCategory.setTerm(category.getTerm()); entryCategory.setLabel(category.getLabel()); // remove it from the "toDelete" set (it's on the new request, so we want to keep it) if (!toDelete.remove(entryCategory)) { // if it wasn't there to remove, then it's new to us, so we should put it in the // toInsert set toInsert.add(entryCategory); } } if (!toDelete.isEmpty()) { getCategoriesHandler().deleteEntryCategoryBatch(new ArrayList<EntryCategory>(toDelete)); entryMetaData.getCategories().removeAll(toDelete); } if (!toInsert.isEmpty()) { getCategoriesHandler().insertEntryCategoryBatch(toInsert); entryMetaData.getCategories().addAll(toInsert); } } } /** * {@inheritDoc} * Subclasses should override this method. This implementation does nothing. */ public void ensureCollectionExists(String collection) { // do nothing. } /** * Return the internal Id for the Entry identified by this EntryTarget * In general, subclasses don't have to support internal ids. This implementation simply returns -1. * * @param entryTarget The EntryTarget, decoded from the Request URI * @return The internal Id */ protected Object getInternalId(EntryDescriptor entryTarget) { return -1; } /** * {@inheritDoc} */ public java.util.Collection<UpdateCreateOrDeleteEntry> updateEntries(final RequestContext request) throws AtomServerException { Document<Feed> document; try { document = request.getDocument(); } catch (IOException e) { throw new AtomServerException(e); } if (document.getRoot().getEntries().size() > getMaxFullEntriesPerPage()) { throw new BadRequestException(MessageFormat.format("too many entries ({0}) in batch - max is {1}", document.getRoot().getEntries().size(), getMaxFullEntriesPerPage())); } final List<EntryTarget> entriesToUpdate = new ArrayList<EntryTarget>(); final List<EntryTarget> entriesToDelete = new ArrayList<EntryTarget>(); final EntryMap<String> entryXmlMap = new EntryMap<String>(); final Map<EntryTarget, Entry> entryMap = new HashMap<EntryTarget, Entry>(); final HashMap<EntryTarget, Integer> orderMap = new HashMap<EntryTarget, Integer>(); Operation defaultOperationExtension = document.getRoot().getExtension(AtomServerConstants.OPERATION); String defaultOperation = defaultOperationExtension == null ? "update" : defaultOperationExtension.getType(); List<Entry> entries = document.getRoot().getEntries(); UpdateCreateOrDeleteEntry[] updateEntries = new UpdateCreateOrDeleteEntry[entries.size()]; Set<RelaxedEntryTarget> relaxedEntryTargetSet = new HashSet<RelaxedEntryTarget>(); int order = 0; for (Entry entry : entries) { try { IRI baseIri = new IRI(getServiceBaseUri()); IRI iri = baseIri.relativize(entry.getLink("edit").getHref()); EntryTarget entryTarget = null; try { // The request is always as PUT, so we will get back a FeedTarget when we want an insert URITarget uriTarget = getURIHandler().parseIRI(request, iri); if (uriTarget instanceof FeedTarget) { entryTarget = new EntryTarget((FeedTarget) uriTarget); // determine if we are creating the entryId -- i.e. if this was a POST if (getEntryIdGenerator() == null) { throw new AtomServerException("No EntryIdGenerator was wired into the Collection (" + entryTarget.toString() + ")"); } else { entryTarget.setEntryId(getEntryIdGenerator().generateId()); } } else { entryTarget = (EntryTarget) uriTarget; } } catch (Exception e) { throw new BadRequestException("Bad request URI: " + iri, e); } if (entryTarget == null) { throw new BadRequestException("Bad request URI: " + iri); } String collection = entryTarget.getCollection(); ensureCollectionExists(collection); // Verify that we do not have multiple <operation> elements List<Operation> operationExtensions = entry.getExtensions(AtomServerConstants.OPERATION); if (operationExtensions != null && operationExtensions.size() > 1) { throw new BadRequestException("Multiple operations applied to one entry"); } // Set to the default operation if none is set. String operation = operationExtensions == null || operationExtensions.isEmpty() ? defaultOperation : operationExtensions.get(0).getType(); if (log.isDebugEnabled()) { log.debug("operation : " + operation); } // We do not allow an Entry to occur twice in the batch. // NOTE: the first one wins !! RelaxedEntryTarget relaxedEntryTarget = new RelaxedEntryTarget(entryTarget); if (relaxedEntryTargetSet.contains(relaxedEntryTarget)) { throw new BadRequestException( "You may not include the same Entry twice (" + entryTarget + ")."); } else { relaxedEntryTargetSet.add(relaxedEntryTarget); } entryMap.put(entryTarget, entry); // Add to the processing lists. if ("delete".equalsIgnoreCase(operation)) { entriesToDelete.add(entryTarget); orderMap.put(entryTarget, order); } else if ("update".equalsIgnoreCase(operation) || "insert".equalsIgnoreCase(operation)) { String entryXml = validateAndPreprocessEntryContents(entry, entryTarget); entriesToUpdate.add(entryTarget); entryXmlMap.put(entryTarget, entryXml); orderMap.put(entryTarget, order); setTargetContentHashCode(entryTarget, entry, entryXml); } } catch (AtomServerException e) { UpdateCreateOrDeleteEntry.CreateOrUpdateEntry updateEntry = new UpdateCreateOrDeleteEntry.CreateOrUpdateEntry( entry, false); updateEntry.setException(e); updateEntries[order] = updateEntry; } order++; } // update entry count if (getEntriesMonitor() != null) { getEntriesMonitor().updateNumberOfEntriesToUpdate(entries.size()); } Abdera abdera = request.getServiceContext().getAbdera(); // ---------------- process updates ------------------ if (!entriesToUpdate.isEmpty()) { java.util.Collection<BatchEntryResult> results = executeTransactionally( new TransactionalTask<java.util.Collection<BatchEntryResult>>() { public Collection<BatchEntryResult> execute() { java.util.Collection<BatchEntryResult> results = modifyEntries(request, entriesToUpdate); for (BatchEntryResult result : results) { boolean categoriesUpdated = false; if (result.getMetaData() != null) { categoriesUpdated = postProcessEntryContents( entryXmlMap.get(result.getMetaData()), result.getMetaData()); } if (!result.isModified() && !categoriesUpdated) { // Same contents and categories if (getEntriesMonitor() != null) { getEntriesMonitor().updateNumberOfEntriesNotUpdatedDueToSameContent(1); } continue; } // if contents is the same but the categories have changed, // go back and update the entry so that it'll have a new revision and timestamp. if (!result.isModified()) { EntryMetaDataStatus mdStatus = reModifyEntry(null, result.getEntryTarget()); // update the result to indicate Entry has been modified. result.setMetaData(mdStatus.getEntryMetaData()); result.setModified(true); } if (result.getException() == null) { String entryXml = entryXmlMap.get(result.getEntryTarget()); getContentStorage().putContent(entryXml, result.getMetaData()); } if (getEntriesMonitor() != null) { getEntriesMonitor().updateNumberOfEntriesActuallyUpdated(1); } } return results; } }); for (BatchEntryResult result : results) { EntryMetaData metaData = result.getMetaData(); if (metaData == null) { EntryTarget target = result.getEntryTarget().cloneWithNewRevision(URIHandler.REVISION_OVERRIDE); try { metaData = getEntry(target); } catch (AtomServerException e) { metaData = null; } } Entry entry = metaData == null ? newEntryWithCommonContentOnly(abdera, result.getEntryTarget()) : newEntry(abdera, metaData, EntryType.full); entry.addSimpleExtension(AtomServerConstants.ENTRY_UPDATED, (result.isModified()) ? "true" : "false"); if (metaData != null && metaData.getContentHashCode() != null) { entry.addSimpleExtension(AtomServerConstants.CONTENT_HASH, metaData.getContentHashCode()); } UpdateCreateOrDeleteEntry.CreateOrUpdateEntry updateEntry = new UpdateCreateOrDeleteEntry.CreateOrUpdateEntry( entry, metaData != null && metaData.isNewlyCreated()); if (result.getException() != null) { updateEntry.setException(result.getException()); } Integer listOrder = orderMap.get(result.getEntryTarget()); if (listOrder == null) { // This should never happen.... String msg = "Could not map (" + result.getEntryTarget() + ") in Batch Order Map"; log.error(msg); throw new AtomServerException(msg); } updateEntries[listOrder] = updateEntry; } } // ---------------- process deletes ------------------ if (!entriesToDelete.isEmpty()) { java.util.Collection<BatchEntryResult> results = executeTransactionally( new TransactionalTask<Collection<BatchEntryResult>>() { public Collection<BatchEntryResult> execute() { java.util.Collection<BatchEntryResult> results = deleteEntries(request, entriesToDelete); for (BatchEntryResult result : results) { if (result.getException() == null) { EntryMetaData entryMetaDataClone = (EntryMetaData) (result.getMetaData() .clone()); int currentRevision = result.getMetaData().getRevision(); entryMetaDataClone.setRevision((currentRevision - 1)); String deletedEntryXml = createDeletedEntryXML(entryMetaDataClone); getContentStorage().deleteContent(deletedEntryXml, result.getMetaData()); } } return results; } }); for (BatchEntryResult result : results) { // TODO: WRONG! EntryMetaData metaData = result.getMetaData(); UpdateCreateOrDeleteEntry.DeleteEntry deleteEntry = null; if (metaData == null) { Factory factory = AtomServer.getFactory(abdera); Entry entry = factory.newEntry(); String workspace = result.getEntryTarget().getWorkspace(); String collection = result.getEntryTarget().getCollection(); String entryId = result.getEntryTarget().getEntryId(); Locale locale = result.getEntryTarget().getLocale(); String fileURI = getURIHandler().constructURIString(workspace, collection, entryId, locale); setEntryId(factory, entry, fileURI); setEntryTitle(factory, entry, isLocalized() ? (" Entry: " + collection + " " + entryId + "." + locale) : (" Entry: " + collection + " " + entryId)); addAuthorToEntry(factory, entry, "AtomServer APP Service"); addLinkToEntry(factory, entry, fileURI, "self"); String editURL = fileURI + "/" + (result.getEntryTarget().getRevision() + 1); addLinkToEntry(factory, entry, editURL, "edit"); deleteEntry = new UpdateCreateOrDeleteEntry.DeleteEntry(entry); } else { deleteEntry = new UpdateCreateOrDeleteEntry.DeleteEntry( newEntry(abdera, metaData, EntryType.full)); } if (result.getException() != null) { deleteEntry.setException(result.getException()); } Integer listOrder = orderMap.get(result.getEntryTarget()); if (listOrder == null) { // This should never happen.... String msg = "Could not map (" + result.getEntryTarget() + ") in Batch Order Map"; log.error(msg); throw new AtomServerException(msg); } updateEntries[listOrder] = deleteEntry; } } // Clear the maps to help out the Garbage Collector entryXmlMap.clear(); entriesToUpdate.clear(); entriesToDelete.clear(); orderMap.clear(); relaxedEntryTargetSet.clear(); return Arrays.asList(updateEntries); } /** * {@inheritDoc} */ public Entry deleteEntry(final RequestContext request) throws AtomServerException { Abdera abdera = request.getServiceContext().getAbdera(); final EntryTarget entryTarget = getEntryTarget(request); EntryMetaData entryMetaData = executeTransactionally(new TransactionalTask<EntryMetaData>() { public EntryMetaData execute() { boolean isObliterate = (Boolean) QueryParam.obliterate.parse(request); //completely delete the entryTarget and return the metaData of that deleted target if (isObliterate) { EntryMetaData lastMetaData = getEntry(entryTarget); obliterateEntry(entryTarget); return lastMetaData; } else { EntryMetaData entryMetaData = deleteEntry(entryTarget, setDeletedFlag()); // Replace the XML file with a "deleted file" // we wait to do this now that we know that the delete was successful EntryMetaData entryMetaDataClone = (EntryMetaData) (entryMetaData.clone()); int currentRevision = entryMetaData.getRevision(); entryMetaDataClone.setRevision((currentRevision - 1)); getContentStorage().deleteContent(createDeletedEntryXML(entryMetaDataClone), entryMetaData); return entryMetaData; } } }); return (entryMetaData == null) ? null : newEntry(abdera, entryMetaData, EntryType.link); } private boolean postProcessEntryContents(String entryXml, EntryMetaData entryMetaData) { log.debug("BEGIN AUTO_TAGGING................"); EntryAutoTagger autoTagger = getAutoTagger(); if (autoTagger != null) { StopWatch stopWatch = new AtomServerStopWatch(); try { return autoTagger.tag(entryMetaData, entryXml); } finally { stopWatch.stop("AC.postProcContent", AtomServerPerfLogTagFormatter.getPerfLogEntryString(entryMetaData)); } } return false; } //~~~~~~~~~~~~~~~~~~~~~~ protected String validateAndPreprocessEntryContents(Entry entry, EntryTarget entryTarget) throws BadContentException { // Let's validate upfront so we can fail-fast, so grab the entryXml String workspace = entryTarget.getWorkspace(); String collection = entryTarget.getCollection(); Locale locale = entryTarget.getLocale(); String entryId = entryTarget.getEntryId(); int revision = entryTarget.getRevision(); String entryXml = null; StopWatch stopWatch1 = new AtomServerStopWatch(); try { entryXml = entry.getContent(); } catch (Exception ee) { String msg = "Could not process PUT for [" + workspace + ", " + collection + ", " + locale + ", " + entryId + ", " + revision + "]\n Reason:: " + ee.getMessage() + "\n 1) MAKE CERTAIN THAT YOU HAVE A NAMESPACE ON THE <entry> ELEMENT!" + "\n (i.e. <entry xmlns=\"http://www.w3.org/2005/Atom\">)" + "\n 2) MAKE CERTAIN THAT YOU ARE INDEED SENDING UTF-8 CHARACTERS"; log.error(msg, ee); throw new BadContentException(msg, ee); } finally { stopWatch1.stop("AC.entry.getContent", AtomServerPerfLogTagFormatter.getPerfLogEntryString(entryTarget)); } if (entryXml == null) { String msg = "Could not process PUT for [" + workspace + ", " + collection + ", " + locale + ", " + entryId + ", " + revision + "]\n Reason:: Content is NULL"; log.error(msg); throw new BadContentException(msg); } // now validate the <content> with whatever Validator was registered (if any) ContentValidator validator = getContentValidator(); if (validator != null) { StopWatch stopWatch2 = new AtomServerStopWatch(); try { validator.validate(entryXml); } finally { stopWatch2.stop("XML.validator", AtomServerPerfLogTagFormatter.getPerfLogEntryString(entryTarget)); } } return entryXml; } //~~~~~~~~~~~~~~~~~~~~~~ private Entry parseEntry(EntryTarget entryTarget, RequestContext request) { StopWatch stopWatch = new AtomServerStopWatch(); try { String errMsgPrefix = "Could not process PUT for [" + entryTarget.getWorkspace() + ", " + entryTarget.getCollection() + ", " + entryTarget.getLocale() + ", " + entryTarget.getEntryId() + ", " + entryTarget.getRevision(); String errMsgPostfix = "\n MAKE CERTAIN THAT YOU ARE INDEED SENDING VALID XML"; Entry entry = null; try { Document<Entry> document = request.getDocument(); entry = document.getRoot(); } catch (java.lang.ClassCastException ee) { String msg = errMsgPrefix + "]\n Reason:: Could not parse a valid <entry> from the Request provided. " + ee.getMessage() + "\n 1) MAKE CERTAIN THAT YOU HAVE A NAMESPACE ON THE <entry> ELEMENT!" + "\n (i.e. <entry xmlns=\"http://www.w3.org/2005/Atom\">)" + errMsgPostfix; log.error(msg, ee); throw new BadContentException(msg, ee); } catch (java.lang.ArrayIndexOutOfBoundsException ee) { String msg = errMsgPrefix + "]\n Reason:: MOST LIKELY THE <content> IS EMPTY. " + ee.getMessage() + errMsgPostfix; log.error(msg, ee); throw new BadContentException(msg, ee); } catch (org.apache.abdera.parser.ParseException ee) { String msg = errMsgPrefix + "]\n Reason:: The <content> XML could not be parsed. " + ee.getMessage() + "\n If this was caused by an ArrayIndexOutOfBoundsException. MOST LIKELY THE <content> IS EMPTY " + errMsgPostfix; log.error(msg, ee); throw new BadContentException(msg, ee); } catch (Exception ee) { String msg = errMsgPrefix + "]\n Reason:: UNKNOWN EXCEPTION THROWN while parsing the <entry>" + ee.getMessage() + errMsgPostfix; log.error(msg, ee); throw new BadContentException(msg, ee); } if (entry == null) { String msg = errMsgPrefix + "]\n Reason:: Content is NULL. Is the <content> element missing? "; log.error(msg); throw new BadContentException(msg); } return entry; } finally { stopWatch.stop("AC.parseEntry", AtomServerPerfLogTagFormatter.getPerfLogEntryString(entryTarget)); } } //~~~~~~~~~~~~~~~~~~~~~~ protected Date getUpdatedMin(URITarget uriTarget, RequestContext request) { java.util.Date ifModifiedSinceDate = uriTarget.getUpdatedMinParam(); // The URL Query param takes precedence, if it is provided if (ifModifiedSinceDate == null) { ifModifiedSinceDate = request.getIfModifiedSince(); } return (ifModifiedSinceDate == null) ? AtomServerConstants.ZERO_DATE : ifModifiedSinceDate; } //~~~~~~~~~~~~~~~~~~~~~~ protected Entry newEntry(Abdera abdera, EntryMetaData entryMetaData, EntryType entryType) throws AtomServerException { StopWatch outerStopWatch = new AtomServerStopWatch(); try { Entry entry = newEntryWithCommonContentOnly(abdera, entryMetaData); java.util.Date updated = entryMetaData.getUpdatedDate(); java.util.Date published = entryMetaData.getPublishedDate(); entry.setUpdated(updated); if (published != null) { entry.setPublished(published); } String workspace = entryMetaData.getWorkspace(); String collection = entryMetaData.getCollection(); String entryId = entryMetaData.getEntryId(); java.util.Locale locale = entryMetaData.getLocale(); if (locale != null) { entry.addSimpleExtension(AtomServerConstants.LOCALE, locale.toString()); } String fileURI = getURIHandler().constructURIString(workspace, collection, entryId, locale); addCategoriesToEntry(entry, entryMetaData, abdera); StopWatch contentStopWatch = new AtomServerStopWatch(); try { if (entryType == EntryType.full) { addFullEntryContent(abdera, entryMetaData, entry); } else if (entryType == EntryType.link) { addLinkToEntry(AtomServer.getFactory(abdera), entry, fileURI, "alternate"); } else { throw new AtomServerException("Must define the EntryType -- full or link"); } } finally { contentStopWatch.stop("XML.entry.addContent", AtomServerPerfLogTagFormatter.getPerfLogEntryString(entryMetaData)); } return entry; } catch (Exception ee) { String msg = "Exception " + ee.getClass().getSimpleName() + " while creating XML for: " + entryMetaData; log.error(ee); throw (ee instanceof AtomServerException) ? (AtomServerException) ee : new AtomServerException(msg, ee); } finally { outerStopWatch.stop("XML.entry.all", AtomServerPerfLogTagFormatter.getPerfLogEntryString(entryMetaData)); } } //~~~~~~~~~~~~~~~~~~~~~~ protected Entry newEntryWithCommonContentOnly(Abdera abdera, EntryDescriptor entryDescriptor) throws AtomServerException { if (log.isTraceEnabled()) { log.trace("RETURNING ENTRY:: " + entryDescriptor); } String workspace = entryDescriptor.getWorkspace(); String collection = entryDescriptor.getCollection(); String entryId = entryDescriptor.getEntryId(); java.util.Locale locale = entryDescriptor.getLocale(); int revision = entryDescriptor.getRevision(); Factory factory = AtomServer.getFactory(abdera); Entry entry = factory.newEntry(); String fileURI = getURIHandler().constructURIString(workspace, collection, entryId, locale); entry.setId(fileURI); entry.setTitle(isLocalized() ? (" Entry: " + collection + " " + entryId + "." + locale) : (" Entry: " + collection + " " + entryId)); entry.addAuthor("AtomServer APP Service"); addLinkToEntry(factory, entry, fileURI, "self"); addEditLink(revision, factory, entry, fileURI); entry.addSimpleExtension(AtomServerConstants.ENTRY_ID, entryId); if (entryDescriptor instanceof EntryMetaData) { EntryMetaData entryMetaData = (EntryMetaData) entryDescriptor; entry.addSimpleExtension(AtomServerConstants.UPDATE_INDEX, String.valueOf(entryMetaData.getUpdateTimestamp())); if (entryMetaData.getRevision() >= 0) { entry.addSimpleExtension(AtomServerConstants.REVISION, String.valueOf(entryMetaData.getRevision())); } } return entry; } //~~~~~~~~~~~~~~~~~~~~~~ protected void addEditLink(int revision, Factory factory, Entry entry, String fileURI) { String editURL = (revision != URIHandler.REVISION_OVERRIDE) ? (fileURI + "/" + (revision + 1)) : fileURI; addLinkToEntry(factory, entry, editURL, "edit"); } //~~~~~~~~~~~~~~~~~~~~~~ protected void addFullEntryContent(Abdera abdera, EntryDescriptor entryMetaData, Entry entry) { ContentStorage contentStorage = getContentStorage(); String xml = contentStorage.getContent(entryMetaData); if (xml == null) { throw new AtomServerException("Could not read entry (" + entryMetaData + ")"); } xml = xml.replaceFirst("<[?].*[?]>", ""); //Note that we cannot just send a FileInputStream because we must strip out the XML declaration entry.setContent(xml, org.apache.abdera.model.Content.Type.XML); } //~~~~~~~~~~~~~~~~~~~~~~ // Add Categories to the Entry, if a CategoriesHandler has been registered protected void addCategoriesToEntry(Entry entry, EntryMetaData entryMetaData, Abdera abdera) { if ((entryMetaData.getCategories() != null) && (entryMetaData.getCategories().size() > 0)) { Collections.sort(entryMetaData.getCategories(), new Comparator<EntryCategory>() { public int compare(EntryCategory a, EntryCategory b) { // compare first by scheme, then by term int schemeCompare = a.getScheme().compareTo(b.getScheme()); return schemeCompare == 0 ? a.getTerm().compareTo(b.getTerm()) : schemeCompare; } }); for (EntryCategory entryCategory : entryMetaData.getCategories()) { if ((entryCategory.getScheme() == null) && (entryCategory.getTerm() == null)) { if (log.isTraceEnabled()) { log.trace("WARNING:: empty Category encountered"); } continue; } // Ideally, we would call the following method on our entry here: // // entry.addCategory(entryCategory.getScheme(), // entryCategory.getTerm(), // entryCategory.getLabel()); // // but, there is a memory leak in Abdera related to the way it caches elements... I believe that // in 0.4.0, they have applied our patch to rectify the problem, so once we upgrade to the newest // version, we should revert to the simpler code above (which is functionally equivalent). // -Bryon // org.apache.abdera.model.Category category = AtomServer.getFactory(abdera).newCategory(); if (entryCategory.getScheme() != null) { category.setAttributeValue(Constants.SCHEME, entryCategory.getScheme()); } else { category.removeAttribute(Constants.SCHEME); } category.setTerm(entryCategory.getTerm()); category.setLabel(entryCategory.getLabel()); entry.addCategory(category); } } } /** * Adds an XML deletion element around the content * like that shown below * <pre> * <deletion xmlns="http://schemas.atomserver.org/atomserver/v1/rev0" * collection="acme" id="12345" locale="en" workspace="widgets"> * [.... content ....] * </deletion> * </pre> */ private String createDeletedEntryXML(EntryDescriptor descriptor) { // we need to know if the deletion tag has already been applied. // there are other ways to do this, but nothing as transparent as this // e.g we could do a SELECT first in the DAO to see if the deleted flag // had been set, and then add this info to the EntryMetaData // and because verbose Deletions are the norm, this is not really expensive. String content = getContentStorage().getContent(descriptor); if (log.isTraceEnabled()) { log.trace("content= " + content); } if (content != null) { content = content.replaceFirst("<[?].*[?]>", ""); if (content.startsWith("<deletion")) { return content; } } XML xml = XML.element("deletion", AtomServerConstants.SCHEMAS_NAMESPACE) .attr("collection", descriptor.getCollection()).attr("id", descriptor.getEntryId()); if (descriptor.getLocale() != null) { xml.attr("locale", descriptor.getLocale().toString()); } xml.attr("workspace", descriptor.getWorkspace()); if (isVerboseDeletions()) { xml.add(content); } return xml.toString(); } public interface TransactionalTask<T> { T execute(); } protected <T> T executeTransactionally(TransactionalTask<T> task) { // by default, this method doesn't do a thing differently - it simply executes the task // as given. subclasses should override this method to ensure transactionality return task.execute(); } // The following methods are a workaround to a memory leak in abdera - we should remove these as // soon as we can update to the trunk of abdera and get their fix. // // <workaround> protected void setEntryId(Factory factory, Entry entry, String id) { if (entry.getIdElement() == null) { factory.newID(entry).setValue(id); } else { entry.getIdElement().setValue(id); } } protected void setEntryTitle(Factory factory, Entry entry, String title) { factory.newTitle(entry).setText(title); } protected void addAuthorToEntry(Factory factory, Entry entry, String name) { Person author = factory.newAuthor(entry); factory.newName(author).setText(name); } protected void addLinkToEntry(Factory factory, Element entry, String href, String rel) { Link link = factory.newLink(entry); link.setAttributeValue(Constants.HREF, href); link.setRel(rel); } // </workaround> protected boolean isContentChanged(EntryTarget entryTarget, EntryMetaData metaData) { if ((entryTarget == null) || (metaData == null) || (entryTarget.getContentHashCode() == null) || (metaData.getContentHashCode() == null)) { return true; } return !metaData.getContentHashCode().equals(entryTarget.getContentHashCode()); } protected void setTargetContentHashCode(EntryTarget target, final Entry entry, final String entryXml) { String clientHash = entry.getSimpleExtension(AtomServerConstants.CONTENT_HASH); if (clientHash == null && entryXml != null) { clientHash = HashUtils.converToUUIDStandardFormat(getContentHashFunction().hashCode(entryXml)); } if (clientHash != null) { clientHash = HashUtils.convertToUUIDStandardFormat(clientHash); } target.setContentHashCode(clientHash); } /** * Wrapper class which holds EntryMetaData and a flag which indicates if it has been modified. * This object is returned from modifyEntry and reModifyEntry method calls. */ public class EntryMetaDataStatus { private EntryMetaData entryMetaData; private boolean modified; public EntryMetaDataStatus(EntryMetaData entryMetaData, boolean modified) { this.entryMetaData = entryMetaData; this.modified = modified; } public EntryMetaData getEntryMetaData() { return entryMetaData; } public boolean isModified() { return modified; } } }