コード例 #1
0
  /** A simple audit entry Currently we ignore filtering here. */
  public void audit(String source, String description, NodeRef key, Object... args) {
    final AuditState auditInfo = new AuditState(auditConfiguration);
    AuditMode auditMode = AuditMode.UNSET;
    try {
      auditMode = onApplicationAudit(auditMode, auditInfo, source, description, key, args);
      if ((auditMode == AuditMode.ALL) || (auditMode == AuditMode.SUCCESS)) {
        RetryingTransactionCallback<Object> cb =
            new RetryingTransactionCallback<Object>() {
              public Object execute() throws Throwable {
                auditDAO.audit(auditInfo);
                return null;
              }
            };
        transactionService.getRetryingTransactionHelper().doInTransaction(cb, false, false);
      }
    } catch (Throwable t) {
      auditMode = onError(auditMode, auditInfo, t, source, description, key, args);
      if ((auditMode == AuditMode.ALL) || (auditMode == AuditMode.FAIL)) {
        try {
          RetryingTransactionCallback<Object> cb =
              new RetryingTransactionCallback<Object>() {
                public Object execute() throws Throwable {
                  auditDAO.audit(auditInfo);
                  return null;
                }
              };

          transactionService.getRetryingTransactionHelper().doInTransaction(cb, false, true);
        } catch (Throwable tt) {
          throw new AuditException("Failed to audit exception", new Object[] {tt}, t);
        }
      }
      throw new AuditException("Application audit failed", t);
    }
  }
コード例 #2
0
  /** Initialise - after bootstrap of schema and tenant admin service */
  public void init() {
    PropertyCheck.mandatory(this, "dictionaryDAO", dictionaryDAO);
    PropertyCheck.mandatory(this, "contentService", contentService);
    PropertyCheck.mandatory(this, "nodeService", nodeService);
    PropertyCheck.mandatory(this, "tenantAdminService", tenantAdminService);
    PropertyCheck.mandatory(this, "namespaceService", namespaceService);
    PropertyCheck.mandatory(this, "messageService", messageService);
    PropertyCheck.mandatory(this, "transactionService", transactionService);
    PropertyCheck.mandatory(this, "policyComponent", policyComponent);

    if (onLoadDynamicModelDelegate == null) {
      onLoadDynamicModelDelegate =
          policyComponent.registerClassPolicy(DynamicModelPolicies.OnLoadDynamicModel.class);
    }

    transactionService
        .getRetryingTransactionHelper()
        .doInTransaction(
            new RetryingTransactionCallback<Object>() {
              public Object execute() throws Exception {
                onDictionaryInit();
                initMessages();

                return (Object) null;
              }
            },
            transactionService.isReadOnly(),
            false);
  }
コード例 #3
0
  /**
   * Internal audit of a method invocation
   *
   * @param mi - the method to audit
   * @return - the return object from the audited method
   * @throws Throwable - any Throwable that can be thrown by th audtied method.
   */
  public Object auditImpl(MethodInvocation mi, boolean execute) throws Throwable {
    final AuditState auditInfo = new AuditState(auditConfiguration);
    // RecordOptions recordOptions = auditModel.getAuditRecordOptions(mi);
    AuditMode auditMode = AuditMode.UNSET;
    try {
      Object o = null;
      auditMode = beforeInvocation(auditMode, auditInfo, mi);
      if (execute) {
        o = mi.proceed();
        auditMode = postInvocation(auditMode, auditInfo, mi, o);
      }
      if ((auditMode == AuditMode.ALL) || (auditMode == AuditMode.SUCCESS)) {
        RetryingTransactionCallback<Object> cb =
            new RetryingTransactionCallback<Object>() {
              public Object execute() throws Throwable {
                auditDAO.audit(auditInfo);
                return null;
              }
            };
        boolean requiresNew =
            (AlfrescoTransactionSupport.getTransactionReadState() != TxnReadState.TXN_READ_WRITE);
        transactionService.getRetryingTransactionHelper().doInTransaction(cb, false, requiresNew);
      }
      return o;
    } catch (Throwable t) {
      auditMode = onError(auditMode, auditInfo, mi, t);
      if ((auditMode == AuditMode.ALL) || (auditMode == AuditMode.FAIL)) {
        try {
          RetryingTransactionCallback<Object> cb =
              new RetryingTransactionCallback<Object>() {
                public Object execute() throws Throwable {
                  auditDAO.audit(auditInfo);
                  return null;
                }
              };

          transactionService.getRetryingTransactionHelper().doInTransaction(cb, false, true);
        } catch (Throwable tt) {
          throw new AuditException("Failed to audit exception", new Object[] {tt}, t);
        }
      }
      throw t;
    }
  }
コード例 #4
0
  public void testImmediateRemoval() throws Exception {
    eagerCleaner.setEagerOrphanCleanup(false);

    final StoreRef storeRef = nodeService.createStore("test", getName() + "-" + GUID.generate());
    RetryingTransactionCallback<ContentData> testCallback =
        new RetryingTransactionCallback<ContentData>() {
          public ContentData execute() throws Throwable {
            // Create some content
            NodeRef rootNodeRef = nodeService.getRootNode(storeRef);
            Map<QName, Serializable> properties = new HashMap<QName, Serializable>(13);
            properties.put(ContentModel.PROP_NAME, (Serializable) "test.txt");
            NodeRef contentNodeRef =
                nodeService
                    .createNode(
                        rootNodeRef,
                        ContentModel.ASSOC_CHILDREN,
                        ContentModel.ASSOC_CHILDREN,
                        ContentModel.TYPE_CONTENT,
                        properties)
                    .getChildRef();
            ContentWriter writer =
                contentService.getWriter(contentNodeRef, ContentModel.PROP_CONTENT, true);
            writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN);
            writer.putContent("INITIAL CONTENT");
            ContentData contentData = writer.getContentData();

            // Delete the first node
            nodeService.deleteNode(contentNodeRef);

            // Done
            return contentData;
          }
        };
    ContentData contentData =
        transactionService.getRetryingTransactionHelper().doInTransaction(testCallback);
    // Make sure that the content URL still exists
    ContentReader reader = contentService.getRawReader(contentData.getContentUrl());
    assertNotNull(reader);
    assertTrue("Content should not have been eagerly deleted.", reader.exists());

    // fire the cleaner
    cleaner.setProtectDays(0);
    cleaner.execute();

    reader = contentService.getRawReader(contentData.getContentUrl());
    // the content should have disappeared as it is not in the database
    assertFalse("Unprotected content was not deleted", reader.exists());
    assertTrue("Content listener was not called", deletedUrls.contains(reader.getContentUrl()));
  }
コード例 #5
0
 /** Initialise the dictionary, ensuring that a transaction is available */
 @Override
 public void onDictionaryInit() {
   if (onLoadDynamicModelDelegate == null) {
     onLoadDynamicModelDelegate =
         policyComponent.registerClassPolicy(DynamicModelPolicies.OnLoadDynamicModel.class);
   }
   RetryingTransactionCallback<Void> initCallback =
       new RetryingTransactionCallback<Void>() {
         @Override
         public Void execute() throws Throwable {
           onDictionaryInitInTxn();
           return null;
         }
       };
   transactionService.getRetryingTransactionHelper().doInTransaction(initCallback, true, false);
 }
コード例 #6
0
  private void assertDocumentVersionAndName(final String versionLabel, final String name) {
    transactionService
        .getRetryingTransactionHelper()
        .doInTransaction(
            new RetryingTransactionCallback<Void>() {
              @Override
              public Void execute() throws Throwable {
                Map<QName, Serializable> properties =
                    getAndAssertProperties(document, versionLabel);
                assertEquals(name, properties.get(ContentModel.PROP_NAME));

                return null;
              }
            },
            true);
  }
コード例 #7
0
  /**
   * {@inheritDoc}
   *
   * @since 3.2
   */
  public Map<String, Serializable> recordAuditValues(
      String rootPath, Map<String, Serializable> values) {
    ParameterCheck.mandatory("rootPath", rootPath);
    AuditApplication.checkPathFormat(rootPath);

    if (values == null || values.isEmpty() || !isSourcePathMapped(rootPath)) {
      return Collections.emptyMap();
    }

    // Build the key paths using the session root path
    Map<String, Serializable> pathedValues = new HashMap<String, Serializable>(values.size() * 2);
    for (Map.Entry<String, Serializable> entry : values.entrySet()) {
      String pathElement = entry.getKey();
      String path = AuditApplication.buildPath(rootPath, pathElement);
      pathedValues.put(path, entry.getValue());
    }

    // Translate the values map
    PathMapper pathMapper = auditModelRegistry.getAuditPathMapper();
    final Map<String, Serializable> mappedValues = pathMapper.convertMap(pathedValues);
    if (mappedValues.isEmpty()) {
      return mappedValues;
    }

    // We have something to record.  Start a transaction, if necessary
    TxnReadState txnState = AlfrescoTransactionSupport.getTransactionReadState();
    switch (txnState) {
      case TXN_NONE:
      case TXN_READ_ONLY:
        // New transaction
        RetryingTransactionCallback<Map<String, Serializable>> callback =
            new RetryingTransactionCallback<Map<String, Serializable>>() {
              public Map<String, Serializable> execute() throws Throwable {
                return recordAuditValuesImpl(mappedValues);
              }
            };
        return transactionService
            .getRetryingTransactionHelper()
            .doInTransaction(callback, false, true);
      case TXN_READ_WRITE:
        return recordAuditValuesImpl(mappedValues);
      default:
        throw new IllegalStateException("Unknown txn state: " + txnState);
    }
  }
コード例 #8
0
  @Override
  protected void tearDown() throws Exception {
    globalProperties.setProperty(AUTO_VERSION_PROPS_KEY, "false");
    transactionService
        .getRetryingTransactionHelper()
        .doInTransaction(
            new RetryingTransactionCallback<Void>() {
              @Override
              public Void execute() throws Throwable {
                if (null != parentFolder) {
                  nodeService.deleteNode(parentFolder);
                }

                authenticationService.clearCurrentSecurityContext();

                return null;
              }
            });
  }
コード例 #9
0
  public void testAutoVersionIncrementOnPropertiesUpdateByLockOwnerAlf14584() throws Exception {
    final String name = generateDocumentName(DOCUMENT_NAME, "0.2");

    transactionService
        .getRetryingTransactionHelper()
        .doInTransaction(
            new RetryingTransactionCallback<Void>() {
              @Override
              public Void execute() throws Throwable {
                Map<QName, Serializable> properties = getAndAssertProperties(document, "0.1");

                Serializable autoVersionProps =
                    properties.get(ContentModel.PROP_AUTO_VERSION_PROPS);
                assertNotNull(
                    ("Autoversion property is NULL! NodeRef = '" + document.toString() + "'"),
                    autoVersionProps);
                assertTrue(
                    ("Autoversion must be TRUE! NodeRef = '" + document.toString() + "'"),
                    (Boolean) autoVersionProps);

                lockService.lock(document, LockType.WRITE_LOCK);

                LockStatus lockStatus = lockService.getLockStatus(document);
                assertFalse(
                    ("Node with NodeRef = '"
                        + document.toString()
                        + "' must not be locked for "
                        + AuthenticationUtil.getFullyAuthenticatedUser()
                        + " user! The user is lock owner"),
                    isLocked(document));
                assertEquals(LockStatus.LOCK_OWNER, lockService.getLockStatus(document));

                nodeService.setProperty(document, ContentModel.PROP_NAME, name);

                return null;
              }
            });

    assertDocumentVersionAndName("0.2", name);
  }
  private void loadData(final int maxCount) {
    final MutableInt doneCount = new MutableInt(0);
    // Batches of 1000 objects
    RetryingTransactionCallback<Integer> makeNodesCallback =
        new RetryingTransactionCallback<Integer>() {
          public Integer execute() throws Throwable {
            for (int i = 0; i < 1000; i++) {
              // We don't need to write anything
              String contentUrl = FileContentStore.createNewFileStoreUrl();
              ContentData contentData =
                  new ContentData(contentUrl, MimetypeMap.MIMETYPE_TEXT_PLAIN, 10, "UTF-8");
              nodeHelper.makeNode(contentData);

              int count = doneCount.intValue();
              count++;
              doneCount.setValue(count);

              // Do some reporting
              if (count % 1000 == 0) {
                System.out.println(
                    String.format("   " + (new Date()) + "Total created: %6d", count));
              }

              // Double check for shutdown
              if (vmShutdownListener.isVmShuttingDown()) {
                break;
              }
            }
            return maxCount;
          }
        };
    int repetitions = (int) Math.floor((double) maxCount / 1000.0);
    for (int i = 0; i < repetitions; i++) {
      transactionService.getRetryingTransactionHelper().doInTransaction(makeNodesCallback);
    }
  }
コード例 #11
0
  @Override
  protected void setUp() throws Exception {
    globalProperties.setProperty(AUTO_VERSION_PROPS_KEY, "true");
    transactionService
        .getRetryingTransactionHelper()
        .doInTransaction(
            new RetryingTransactionCallback<Void>() {
              @Override
              public Void execute() throws Throwable {
                authenticationService.authenticate(
                    ADMIN_CREDENTIAL, ADMIN_CREDENTIAL.toCharArray());

                ResultSet query = null;
                NodeRef rootNode = null;
                try {
                  query =
                      searchService.query(
                          StoreRef.STORE_REF_WORKSPACE_SPACESSTORE,
                          SearchService.LANGUAGE_LUCENE,
                          ROOT_NODE_TERM);
                  rootNode = query.getNodeRef(0);
                } finally {
                  if (null != query) {
                    query.close();
                  }
                }

                Map<QName, Serializable> properties = new HashMap<QName, Serializable>();
                properties.put(ContentModel.PROP_NAME, PARENT_FOLDER_NAME);
                parentFolder =
                    nodeService
                        .createNode(
                            rootNode,
                            ContentModel.ASSOC_CONTAINS,
                            QName.createQName(ContentModel.USER_MODEL_URI, PARENT_FOLDER_NAME),
                            ContentModel.TYPE_FOLDER,
                            properties)
                        .getChildRef();

                properties.clear();
                properties.put(ContentModel.PROP_NAME, DOCUMENT_NAME);

                document =
                    nodeService
                        .createNode(
                            parentFolder,
                            ContentModel.ASSOC_CONTAINS,
                            QName.createQName(ContentModel.USER_MODEL_URI, DOCUMENT_NAME),
                            ContentModel.TYPE_CONTENT,
                            properties)
                        .getChildRef();
                contentService
                    .getWriter(document, ContentModel.PROP_CONTENT, true)
                    .putContent(TEST_CONTENT_01);

                if (!nodeService.hasAspect(document, ContentModel.ASPECT_VERSIONABLE)) {
                  Map<QName, Serializable> versionProperties = new HashMap<QName, Serializable>();
                  versionProperties.put(ContentModel.PROP_VERSION_LABEL, "0.1");
                  versionProperties.put(ContentModel.PROP_INITIAL_VERSION, true);
                  versionProperties.put(ContentModel.PROP_VERSION_TYPE, VersionType.MINOR);
                  nodeService.addAspect(
                      document, ContentModel.ASPECT_VERSIONABLE, versionProperties);
                }

                return null;
              }
            });
  }
コード例 #12
0
  public void testAutoVersionIncrementOnPropertiesUpdateAfterCheckInAlf14584() throws Exception {
    final String name02 = generateDocumentName(DOCUMENT_NAME, "0.2");
    final String name11 = generateDocumentName(DOCUMENT_NAME, "1.1");

    transactionService
        .getRetryingTransactionHelper()
        .doInTransaction(
            new RetryingTransactionCallback<Void>() {
              @Override
              public Void execute() throws Throwable {
                Map<QName, Serializable> properties = getAndAssertProperties(document, "0.1");

                Serializable autoVersionProps =
                    properties.get(ContentModel.PROP_AUTO_VERSION_PROPS);
                assertNotNull(
                    ("Autoversion property is NULL! NodeRef = '" + document.toString() + "'"),
                    autoVersionProps);
                assertTrue(
                    ("Autoversion must be TRUE! NodeRef = '" + document.toString() + "'"),
                    (Boolean) autoVersionProps);

                nodeService.setProperty(document, ContentModel.PROP_NAME, name02);

                return null;
              }
            });

    transactionService
        .getRetryingTransactionHelper()
        .doInTransaction(
            new RetryingTransactionCallback<Void>() {
              @Override
              public Void execute() throws Throwable {
                Map<QName, Serializable> properties = getAndAssertProperties(document, "0.2");
                assertEquals(name02, properties.get(ContentModel.PROP_NAME));

                NodeRef workingCopy = checkOutCheckInService.checkout(document);
                contentService
                    .getWriter(workingCopy, ContentModel.PROP_CONTENT, true)
                    .putContent(TEST_CONTENT_10);

                Map<String, Serializable> versionProperties = new HashMap<String, Serializable>();
                versionProperties.put(VersionModel.PROP_VERSION_TYPE, VersionType.MAJOR);
                document = checkOutCheckInService.checkin(workingCopy, versionProperties);

                return null;
              }
            });

    assertDocumentVersionAndName("1.0", name02);

    transactionService
        .getRetryingTransactionHelper()
        .doInTransaction(
            new RetryingTransactionCallback<Void>() {
              @Override
              public Void execute() throws Throwable {
                nodeService.setProperty(document, ContentModel.PROP_NAME, name11);

                return null;
              }
            });

    assertDocumentVersionAndName("1.1", name11);
  }
コード例 #13
0
  public void testMultipleCheckoutsCheckInsWithPropChange() {
    // Note: this test assumes cm:autoVersionProps=true by default (refer to cm:versionableAspect in
    // contentModel.xml)

    // Create a new node
    ChildAssociationRef childAssocRef =
        nodeService.createNode(
            rootNodeRef,
            ContentModel.ASSOC_CHILDREN,
            QName.createQName("test"),
            ContentModel.TYPE_CONTENT,
            null);
    final NodeRef testNodeRef = childAssocRef.getChildRef();

    // Add the version aspect to the created node
    nodeService.addAspect(testNodeRef, ContentModel.ASPECT_VERSIONABLE, null);

    setComplete();
    endTransaction();

    // Checkout
    final NodeRef workingCopy1 =
        transactionService
            .getRetryingTransactionHelper()
            .doInTransaction(
                new RetryingTransactionCallback<NodeRef>() {
                  public NodeRef execute() throws Exception {
                    return cociService.checkout(testNodeRef);
                  }
                });

    // Change property and checkin
    transactionService
        .getRetryingTransactionHelper()
        .doInTransaction(
            new RetryingTransactionCallback<Object>() {
              public Object execute() throws Exception {
                nodeService.setProperty(workingCopy1, ContentModel.PROP_AUTHOR, "author1");

                Map<String, Serializable> versionProperties = new HashMap<String, Serializable>();
                versionProperties.put(Version.PROP_DESCRIPTION, "This is a test version 1");
                cociService.checkin(workingCopy1, versionProperties);

                return null;
              }
            });

    // Checkout
    final NodeRef workingCopy2 =
        transactionService
            .getRetryingTransactionHelper()
            .doInTransaction(
                new RetryingTransactionCallback<NodeRef>() {
                  public NodeRef execute() throws Exception {
                    return cociService.checkout(testNodeRef);
                  }
                });

    // Change property and checkin
    transactionService
        .getRetryingTransactionHelper()
        .doInTransaction(
            new RetryingTransactionCallback<Object>() {
              public Object execute() throws Exception {
                nodeService.setProperty(workingCopy2, ContentModel.PROP_AUTHOR, "author2");

                Map<String, Serializable> versionProperties = new HashMap<String, Serializable>();
                versionProperties.put(Version.PROP_DESCRIPTION, "This is a test version 2");
                cociService.checkin(workingCopy2, versionProperties);

                return null;
              }
            });

    // Checkout
    final NodeRef workingCopy3 =
        transactionService
            .getRetryingTransactionHelper()
            .doInTransaction(
                new RetryingTransactionCallback<NodeRef>() {
                  public NodeRef execute() throws Exception {
                    return cociService.checkout(testNodeRef);
                  }
                });

    // Change property and checkin
    transactionService
        .getRetryingTransactionHelper()
        .doInTransaction(
            new RetryingTransactionCallback<Object>() {
              public Object execute() throws Exception {
                nodeService.setProperty(workingCopy3, ContentModel.PROP_AUTHOR, "author3");

                Map<String, Serializable> versionProperties = new HashMap<String, Serializable>();
                versionProperties.put(Version.PROP_DESCRIPTION, "This is a test version 3");
                cociService.checkin(workingCopy3, versionProperties);

                return null;
              }
            });
  }
コード例 #14
0
  /**
   * Create ContentData set it on a Node, delete the Node, then set the ContentData on a new node
   * and check that the content is preserved during eager cleanup.
   */
  public void testEagerCleanupDereferencing() throws Exception {
    eagerCleaner.setEagerOrphanCleanup(true);

    final StoreRef storeRef = nodeService.createStore("test", getName() + "-" + GUID.generate());
    RetryingTransactionCallback<ContentData> testCallback =
        new RetryingTransactionCallback<ContentData>() {
          public ContentData execute() throws Throwable {
            // Create some content
            NodeRef rootNodeRef = nodeService.getRootNode(storeRef);
            Map<QName, Serializable> properties = new HashMap<QName, Serializable>(13);
            properties.put(ContentModel.PROP_NAME, (Serializable) "test.txt");
            NodeRef contentNodeRef =
                nodeService
                    .createNode(
                        rootNodeRef,
                        ContentModel.ASSOC_CHILDREN,
                        ContentModel.ASSOC_CHILDREN,
                        ContentModel.TYPE_CONTENT,
                        properties)
                    .getChildRef();
            ContentWriter writer =
                contentService.getWriter(contentNodeRef, ContentModel.PROP_CONTENT, true);
            writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN);
            writer.putContent("INITIAL CONTENT");
            ContentData contentData = writer.getContentData();

            // Delete the first node
            nodeService.deleteNode(contentNodeRef);

            ContentReader reader = contentService.getRawReader(contentData.getContentUrl());
            assertNotNull(reader);
            assertTrue("Content was cleaned before end of transaction", reader.exists());

            // Make a new copy using the same ContentData
            properties.put(ContentModel.PROP_NAME, (Serializable) "test2.txt");
            properties.put(ContentModel.PROP_CONTENT, contentData);
            contentNodeRef =
                nodeService
                    .createNode(
                        rootNodeRef,
                        ContentModel.ASSOC_CHILDREN,
                        ContentModel.ASSOC_CHILDREN,
                        ContentModel.TYPE_CONTENT,
                        properties)
                    .getChildRef();

            reader = contentService.getRawReader(contentData.getContentUrl());
            assertNotNull(reader);
            assertTrue("Content was cleaned before end of transaction", reader.exists());

            // Done
            return contentData;
          }
        };
    ContentData contentData =
        transactionService.getRetryingTransactionHelper().doInTransaction(testCallback);
    // Make sure that the content URL still exists
    ContentReader reader = contentService.getRawReader(contentData.getContentUrl());
    assertNotNull(reader);
    assertTrue(
        "Content was cleaned despite being re-referenced in the transaction", reader.exists());
  }
コード例 #15
0
  public void testEagerCleanupOnCommit() throws Exception {
    eagerCleaner.setEagerOrphanCleanup(true);
    // Create a new file
    RetryingTransactionCallback<NodeRef> makeContentCallback =
        new RetryingTransactionCallback<NodeRef>() {
          public NodeRef execute() throws Throwable {
            // Create some content
            StoreRef storeRef =
                nodeService.createStore("test", "testEagerCleanupOnCommit-" + GUID.generate());
            NodeRef rootNodeRef = nodeService.getRootNode(storeRef);
            Map<QName, Serializable> properties =
                Collections.singletonMap(ContentModel.PROP_NAME, (Serializable) "test.txt");
            NodeRef contentNodeRef =
                nodeService
                    .createNode(
                        rootNodeRef,
                        ContentModel.ASSOC_CHILDREN,
                        ContentModel.ASSOC_CHILDREN,
                        ContentModel.TYPE_CONTENT,
                        properties)
                    .getChildRef();
            ContentWriter writer =
                contentService.getWriter(contentNodeRef, ContentModel.PROP_CONTENT, true);
            writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN);
            writer.putContent("INITIAL CONTENT");
            // Done
            return contentNodeRef;
          }
        };
    final NodeRef contentNodeRef =
        transactionService.getRetryingTransactionHelper().doInTransaction(makeContentCallback);
    ContentReader contentReader =
        contentService.getReader(contentNodeRef, ContentModel.PROP_CONTENT);
    assertTrue("Expect content to exist", contentReader.exists());

    // Now update the node, but force a failure i.e. txn rollback
    final List<String> newContentUrls = new ArrayList<String>();
    RetryingTransactionCallback<String> failUpdateCallback =
        new RetryingTransactionCallback<String>() {
          public String execute() throws Throwable {
            ContentWriter writer =
                contentService.getWriter(contentNodeRef, ContentModel.PROP_CONTENT, true);
            writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN);
            writer.putContent("CONTENT FOR FAIL");
            // This will have updated the metadata, so we can fail now
            newContentUrls.add(writer.getContentUrl());
            // Done
            throw new RuntimeException("FAIL");
          }
        };
    try {
      transactionService.getRetryingTransactionHelper().doInTransaction(failUpdateCallback);
      fail("Transaction was meant to fail");
    } catch (RuntimeException e) {
      if (e.getMessage().equals("FAIL")) {
        // Expected
      } else {
        // Ooops
        throw e;
      }
    }
    // Make sure that the new content is not there
    // The original content must still be there
    assertEquals("Expected one content URL to play with", 1, newContentUrls.size());
    ContentReader readerMissing = contentService.getRawReader(newContentUrls.get(0));
    assertFalse("Newly created content should have been removed.", readerMissing.exists());
    assertTrue("Original content should still be there.", contentReader.exists());

    // Now update the node successfully
    RetryingTransactionCallback<String> successUpdateCallback =
        new RetryingTransactionCallback<String>() {
          public String execute() throws Throwable {
            ContentWriter writer =
                contentService.getWriter(contentNodeRef, ContentModel.PROP_CONTENT, true);
            writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN);
            writer.putContent("CONTENT FOR SUCCESS");
            // Done
            return writer.getContentUrl();
          }
        };
    String newContentUrl =
        transactionService.getRetryingTransactionHelper().doInTransaction(successUpdateCallback);
    // Make sure that the new content is there
    // The original content was disposed of
    ContentReader contentReaderNew = contentService.getRawReader(newContentUrl);
    assertTrue("Newly created content should be present.", contentReaderNew.exists());
    assertFalse("Original content should have been removed.", contentReader.exists());

    // Now delete the node
    RetryingTransactionCallback<Object> deleteNodeCallback =
        new RetryingTransactionCallback<Object>() {
          public Object execute() throws Throwable {
            nodeService.deleteNode(contentNodeRef);
            // Done
            return null;
          }
        };
    transactionService.getRetryingTransactionHelper().doInTransaction(deleteNodeCallback);
    // The new content must have disappeared
    assertFalse("Newly created content should be removed.", contentReaderNew.exists());
  }