/* * JBoss, Home of Professional Open Source. * Copyright 2009, Red Hat Middleware LLC, and individual contributors * as indicated by the @author tags. See the copyright.txt file in the * distribution for a full listing of individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.jboss.system.server.profileservice.repository.clustered.local; import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.jboss.logging.Logger; import org.jboss.profileservice.spi.DeploymentRepository; import org.jboss.profileservice.spi.ProfileKey; import org.jboss.system.server.profileservice.repository.clustered.metadata.RepositoryContentMetadata; import org.jboss.system.server.profileservice.repository.clustered.metadata.RepositoryItemMetadata; import org.jboss.system.server.profileservice.repository.clustered.metadata.RepositoryRootMetadata; import org.jboss.system.server.profileservice.repository.clustered.sync.ContentModification; import org.jboss.system.server.profileservice.repository.clustered.sync.NoOpSynchronizationAction; import org.jboss.system.server.profileservice.repository.clustered.sync.RemovalMetadataInsertionAction; import org.jboss.system.server.profileservice.repository.clustered.sync.SynchronizationAction; import org.jboss.system.server.profileservice.repository.clustered.sync.SynchronizationActionContext; import org.jboss.system.server.profileservice.repository.clustered.sync.SynchronizationId; import org.jboss.system.server.profileservice.repository.clustered.sync.TwoPhaseCommitAction; import org.jboss.virtual.VFS; import org.jboss.virtual.VirtualFile; /** * Abstract base class for a {@link LocalContentManager} implementation. * * @author Brian Stansberry * * @version $Revision: $ */ public abstract class AbstractLocalContentManager<T extends SynchronizationActionContext> implements LocalContentManager<T> { private final Logger log = Logger.getLogger(getClass()); private RepositoryContentMetadata officialContentMetadata; private RepositoryContentMetadata currentContentMetadata; private final Map<String, URI> namedURIMap; private final Map<String, VirtualFile> vfCache = new ConcurrentHashMap<String, VirtualFile>(); private final ProfileKey profileKey; private final String storeName; private final String localNodeName; private final ContentMetadataPersister contentMetadataPersister; private List<TwoPhaseCommitAction<T>> currentSynchronizationActions; private T currentSynchronizationActionContext; private Map<RepositoryItemMetadata, InputStream> pendingStreams = new ConcurrentHashMap<RepositoryItemMetadata, InputStream>(); protected static String createStoreName(ProfileKey key) { // Normal case if (ProfileKey.DEFAULT.equals(key.getDomain()) && ProfileKey.DEFAULT.equals(key.getServer())) { return key.getName(); } StringBuilder sb = new StringBuilder(); if (ProfileKey.DEFAULT.equals(key.getDomain()) == false) { sb.append(key.getDomain()); sb.append('-'); } if (ProfileKey.DEFAULT.equals(key.getServer()) == false) { sb.append(key.getServer()); sb.append('-'); } sb.append(key.getName()); return sb.toString(); } /** * Create a new AbstractLocalContentManager. * * @param namedURIMap Map of URIs managed by this object, keyed by a * String identifier * @param profileKey key of the profile the content of which this * object is managing * @param localNodeName name of the local node in the cluster * @param contentMetadataPersister object to use for storing/retrieving content metadata */ protected AbstractLocalContentManager(Map<String, URI> namedURIMap, ProfileKey profileKey, String localNodeName, ContentMetadataPersister contentMetadataPersister) { if (namedURIMap == null) { throw new IllegalArgumentException("Null namedURIMap"); } if (profileKey == null) { throw new IllegalArgumentException("Null profileKey"); } if (localNodeName == null) { throw new IllegalArgumentException("Null localNodeName"); } if (contentMetadataPersister == null) { throw new IllegalArgumentException("Null contentMetadataPersister"); } this.namedURIMap = namedURIMap; this.profileKey = profileKey; this.storeName = createStoreName(profileKey); this.localNodeName = localNodeName; this.contentMetadataPersister = contentMetadataPersister; this.officialContentMetadata = this.contentMetadataPersister.load(this.storeName); } // ------------------------------------------------------------- Properties public String getLocalNodeName() { return localNodeName; } public String getStoreName() { return storeName; } // ---------------------------------------------------- LocalContentManager public RepositoryContentMetadata getOfficialContentMetadata() { return officialContentMetadata; } public RepositoryContentMetadata createEmptyContentMetadata() { RepositoryContentMetadata md = new RepositoryContentMetadata(profileKey); List<RepositoryRootMetadata> roots = new ArrayList<RepositoryRootMetadata>(); for (Map.Entry<String, URI> entry : namedURIMap.entrySet()) { RepositoryRootMetadata rmd = new RepositoryRootMetadata(); rmd.setName(entry.getKey()); roots.add(rmd); } md.setRepositories(roots); return md; } public RepositoryContentMetadata getCurrentContentMetadata() throws IOException { synchronized (this) { RepositoryContentMetadata md = new RepositoryContentMetadata(profileKey); List<RepositoryRootMetadata> roots = new ArrayList<RepositoryRootMetadata>(); RepositoryContentMetadata official = getOfficialContentMetadata(); for (Map.Entry<String, URI> entry : namedURIMap.entrySet()) { RepositoryRootMetadata rmd = new RepositoryRootMetadata(); rmd.setName(entry.getKey()); RepositoryRootMetadata existingRmd = official == null ? null : official.getRepositoryRootMetadata(entry.getKey()); VirtualFile root = getCachedVirtualFile(entry.getValue()); if (isDirectory(root)) { for(VirtualFile child: root.getChildren()) { createItemMetadataFromScan(rmd, existingRmd, child, root); } } else { // The root is itself an item. Treat it as a "child" of // itself with no relative path createItemMetadataFromScan(rmd, existingRmd, root, root); } roots.add(rmd); } md.setRepositories(roots); // Retain any existing "removed item" metadata -- but only if // it wasn't re-added!! RepositoryContentMetadata existing = getOfficialContentMetadata(); if (existing != null) { for (RepositoryRootMetadata existingRoot : existing.getRepositories()) { RepositoryRootMetadata rmd = md.getRepositoryRootMetadata(existingRoot.getName()); if (rmd != null) { Collection<RepositoryItemMetadata> rimds = rmd.getContent(); for (RepositoryItemMetadata existingItem : existingRoot.getContent()) { if (existingItem.isRemoved() // but check for re-add && rmd.getItemMetadata(existingItem.getRelativePathElements()) == null) { rimds.add(new RepositoryItemMetadata(existingItem)); } } } } } this.currentContentMetadata = md; return this.currentContentMetadata; } } public List<? extends SynchronizationAction<T>> initiateSynchronization(SynchronizationId<?> id, List<ContentModification> modifications, RepositoryContentMetadata toInstall, boolean localLed) { if (id == null) { throw new IllegalArgumentException("Null id"); } if (modifications == null) { throw new IllegalArgumentException("Null modifications"); } if (toInstall == null) { throw new IllegalArgumentException("Null toInstall"); } synchronized (this) { if (currentSynchronizationActionContext != null) { throw new IllegalStateException("Synchronization " + currentSynchronizationActionContext.getId() + " is already in progress"); } this.currentSynchronizationActionContext = createSynchronizationActionContext(id, toInstall); } List<TwoPhaseCommitAction<T>> actions = new ArrayList<TwoPhaseCommitAction<T>>(); for (ContentModification mod : modifications) { TwoPhaseCommitAction<T> action = null; switch (mod.getType()) { case PULL_FROM_CLUSTER: action = createPullFromClusterAction(mod, localLed); break; case PUSH_TO_CLUSTER: InputStream stream = pendingStreams.remove(mod.getItem()); if (stream == null) { action = createPushToClusterAction(mod, localLed); } else { action = createPushStreamToClusterAction(mod, stream); } break; case REMOVE_FROM_CLUSTER: action = createRemoveFromClusterAction(mod, localLed); break; case REMOVE_TO_CLUSTER: action = createRemoveToClusterAction(mod, localLed); break; case PREPARE_RMDIR_FROM_CLUSTER: action = createPrepareRmdirFromClusterAction(mod, localLed); break; case PREPARE_RMDIR_TO_CLUSTER: action = createPrepareRmdirToClusterAction(mod, localLed); break; case DIR_TIMESTAMP_MISMATCH: action = createDirectoryTimestampMismatchAction(mod, localLed); break; case MKDIR_FROM_CLUSTER: action = createMkdirFromClusterAction(mod, localLed); break; case MKDIR_TO_CLUSTER: action = createMkdirToClusterAction(mod, localLed); break; case REMOVAL_METADATA_FROM_CLUSTER: action = createRemovalMetadataAction(mod, localLed); break; default: throw new IllegalStateException("Unknown " + ContentModification.Type.class.getSimpleName() + " " + mod.getType()); } actions.add(action); } this.currentSynchronizationActions = actions; return Collections.unmodifiableList(actions); } public boolean prepareSynchronization(SynchronizationId<?> id) { validateSynchronization(id); for (TwoPhaseCommitAction<T> action : this.currentSynchronizationActions) { if (action.prepare() == false) { if (log.isTraceEnabled()) { ContentModification mod = action.getRepositoryContentModification(); log.trace("prepare failed for " + mod.getType() + " " + mod.getItem().getRelativePath()); } return false; } } if (log.isTraceEnabled()) { log.trace("prepared synchronization " + id); } return true; } public void commitSynchronization(SynchronizationId<?> id) { validateSynchronization(id); for (TwoPhaseCommitAction<T> action : this.currentSynchronizationActions) { action.commit(); } updateContentMetadata(this.currentSynchronizationActionContext.getInProgressMetadata()); synchronized (this) { this.currentSynchronizationActions = null; this.currentSynchronizationActionContext = null; } if (log.isTraceEnabled()) { log.trace("committed synchronization " + id); } } public void rollbackSynchronization(SynchronizationId<?> id) { validateSynchronization(id); for (TwoPhaseCommitAction<T> action : this.currentSynchronizationActions) { action.rollback(); } synchronized (this) { this.currentSynchronizationActionContext = null; this.currentSynchronizationActions = null; } if (log.isTraceEnabled()) { log.trace("rolled back synchronization " + id); } } public void installCurrentContentMetadata() { synchronized (this) { if (this.currentContentMetadata == null) { throw new IllegalStateException("No currentContentMetadata"); } if (this.currentSynchronizationActionContext != null) { throw new IllegalStateException("Cannot install currentContentMetadata; " + "cluster synchronization " + this.currentSynchronizationActionContext.getId() + " is in progress"); } updateContentMetadata(this.currentContentMetadata); } } public RepositoryItemMetadata getItemForAddition(String vfsPath) throws IOException { RepositoryItemMetadata item = new RepositoryItemMetadata(); item.setRelativePath(vfsPath); List<String> pathElements = item.getRelativePathElements(); String rootName = null; for (RepositoryRootMetadata rmd : getOfficialContentMetadata().getRepositories()) { if (rmd.getItemMetadata(pathElements) != null) { // Exact match to existing item -- done rootName = rmd.getName(); break; } else if (rootName == null) { // Use the first root that can accept children URI rootURI = namedURIMap.get(rmd.getName()); VirtualFile vf = getCachedVirtualFile(rootURI); if (isDirectory(vf)) { rootName = rmd.getName(); break; } } } if (rootName == null) { throw new IllegalStateException("No roots can accept children"); } item.setRootName(rootName); return item; } public RepositoryContentMetadata getContentMetadataForAdd(RepositoryItemMetadata toAdd, InputStream contentIS) throws IOException { RepositoryContentMetadata result = new RepositoryContentMetadata(getOfficialContentMetadata()); RepositoryRootMetadata rmd = result.getRepositoryRootMetadata(toAdd.getRootName()); if (rmd == null) { throw new IllegalArgumentException("Unknown root name " + toAdd.getRootName()); } RepositoryItemMetadata remove = rmd.getItemMetadata(toAdd.getRelativePathElements()); if (remove != null) { Collection<RepositoryItemMetadata> content = rmd.getContent(); if (remove.isDirectory()) { for (Iterator<RepositoryItemMetadata> it = content.iterator(); it.hasNext(); ) { if (it.next().isChildOf(remove)) { it.remove(); } } } content.remove(remove); } rmd.getContent().add(toAdd); pendingStreams.put(toAdd, contentIS); return result; } public VirtualFile getVirtualFileForItem(RepositoryItemMetadata item) throws IOException { URI uri = namedURIMap.get(item.getRootName()); VirtualFile vf = getCachedVirtualFile(uri); VirtualFile parent = null; List<String> path = item.getRelativePathElements(); for (String element : path) { parent = vf; vf = parent.getChild(element); if (vf == null) { throw new IllegalStateException("No child " + element + " under " + parent); } } return vf; } public RepositoryContentMetadata getContentMetadataForRemove(VirtualFile vf) throws IOException { List<String> path = null; RepositoryRootMetadata root = null; RepositoryContentMetadata cmd = new RepositoryContentMetadata(getOfficialContentMetadata()); for (RepositoryRootMetadata rmd : cmd.getRepositories()) { URI uri = namedURIMap.get(rmd.getName()); VirtualFile vfRoot = getCachedVirtualFile(uri); try { path = getRelativePath(vf, vfRoot); root = rmd; break; } catch (IllegalStateException ise) { // vf wasn't a child; ignore and move on to next root } } if (root == null) { throw new IllegalArgumentException(vf + " is not a child of any known roots"); } RepositoryItemMetadata remove = root.getItemMetadata(path); if (remove != null) { Collection<RepositoryItemMetadata> items = root.getContent(); if (isDirectory(vf)) { for (Iterator<RepositoryItemMetadata> it = items.iterator(); it.hasNext(); ) { if (it.next().isChildOf(remove)) { it.remove(); } } } items.remove(remove); } return cmd; } // -------------------------------------------------------------- Protected /** * Create a {@link SynchronizationActionContext} for the given cluster-wide * content synchronization. * * @param id the id of the synchronization * @param toUpdate metadata object that should be updated as synchronization * actions are performed. */ protected abstract T createSynchronizationActionContext(SynchronizationId<?> id, RepositoryContentMetadata toUpdate); /** * Create an action to handle the local end of a node pulling content from * the cluster. * * @param mod object describing the content modification this action is * part of * @param localLed <code>true</code> if this node is driving the synchronization * process the action is part of; <code>false</code> if * another node is * * @return the action. Will not return <code>null</code>. */ protected abstract TwoPhaseCommitAction<T> createPullFromClusterAction(ContentModification mod, boolean localLed); /** * Create an action to handle the local end of a node pushing content to * the cluster. * * @param mod object describing the content modification this action is * part of * @param localLed <code>true</code> if this node is driving the synchronization * process the action is part of; <code>false</code> if * another node is * * @return the action. Will not return <code>null</code>. */ protected abstract TwoPhaseCommitAction<T> createPushToClusterAction(ContentModification mod, boolean localLed); /** * Create an action to handle the local end of a node pushing content that is * read from an external-to-the-repository stream to the cluster. Used to * handle installation of content to the repository via * {@link DeploymentRepository#addDeployment(String, org.jboss.profileservice.spi.ProfileDeployment)}. * <p> * This is only invoked on the node that is driving the synchronization process. * </p> * * @param mod object describing the content modification this action is * part of * * @param stream the stream from which content will be read. * * @return an action that will handle both the local end of pushing the stream content to * other nodes in the cluster <b>and</b> storing the stream content * in this node's repository. Will not return <code>null</code>. */ protected abstract TwoPhaseCommitAction<T> createPushStreamToClusterAction(ContentModification mod, InputStream stream); /** * Create an action to handle the local end of a node removing content that * the rest of the cluster regards as invalid. * * @param mod object describing the content modification this action is * part of * @param localLed <code>true</code> if this node is driving the synchronization * process the action is part of; <code>false</code> if * another node is * * @return the action. Will not return <code>null</code>. */ protected abstract TwoPhaseCommitAction<T> createRemoveFromClusterAction(ContentModification mod, boolean localLed); /** * Create an action to handle the local end of a node removing content from * the cluster. * * @param mod object describing the content modification this action is * part of * @param localLed <code>true</code> if this node is driving the synchronization * process the action is part of; <code>false</code> if * another node is * * @return the action. Will not return <code>null</code>. */ protected abstract TwoPhaseCommitAction<T> createRemoveToClusterAction(ContentModification mod, boolean localLed); /** * Create an action to handle the local end of a node removing a directory * from the cluster. * * @param mod object describing the content modification this action is * part of * @param localLed <code>true</code> if this node is driving the synchronization * process the action is part of; <code>false</code> if * another node is * * @return the action. Will not return <code>null</code>. */ protected abstract TwoPhaseCommitAction<T> createPrepareRmdirToClusterAction(ContentModification mod, boolean localLed); protected abstract TwoPhaseCommitAction<T> createPrepareRmdirFromClusterAction(ContentModification mod, boolean localLed); /** * Create an action to handle the local end of a node adding a directory * to the cluster. * * @param mod object describing the content modification this action is * part of * @param localLed <code>true</code> if this node is driving the synchronization * process the action is part of; <code>false</code> if * another node is * * @return the action. Will not return <code>null</code>. */ protected abstract TwoPhaseCommitAction<T> createMkdirToClusterAction(ContentModification mod, boolean localLed); /** * Create an action to handle the local end of a node adding a directory * due to its presence on the cluster. * * @param mod object describing the content modification this action is * part of * @param localLed <code>true</code> if this node is driving the synchronization * process the action is part of; <code>false</code> if * another node is * * @return the action. Will not return <code>null</code>. */ protected abstract TwoPhaseCommitAction<T> createMkdirFromClusterAction( ContentModification mod, boolean localLed); /** * Create an action to handle the local end of a node updating a directory * timestamp to match the cluster. * * @param mod object describing the content modification this action is * part of * @param localLed <code>true</code> if this node is driving the synchronization * process the action is part of; <code>false</code> if * another node is * * @return the action. Will not return <code>null</code>. */ protected abstract TwoPhaseCommitAction<T> createDirectoryTimestampMismatchAction( ContentModification mod, boolean localLed); /** * Gets the current {@link SynchronizationActionContext}. * * @return the current context, or <code>null</code> if there isn't one */ protected T getSynchronizationActionContext() { return currentSynchronizationActionContext; } /** * Gets a {@link VirtualFile} corresponding to the given URI. * * @param uri the uri. Cannot be <code>null</code>. * @return the virtual file * * @throws IOException * @throws NullPointerException if <code>uri</code> is <code>null</code>. */ protected VirtualFile getCachedVirtualFile(URI uri) throws IOException { VirtualFile vf = this.vfCache.get(uri.toString()); if(vf == null) { vf = VFS.getRoot(uri); this.vfCache.put(uri.toString(), vf); } return vf; } /** * Gets the URI of the repository root with which the given modification * is associated. * * @param mod the modification. Cannot be <code>null</code> * @return the URI. May be <code>null</code> if the modification is for * an unknown root * * @throws NullPointerException if <code>uri</code> is <code>null</code>. * * @see ContentModification#getRootName() */ protected URI getRootURIForModification(ContentModification mod) { return namedURIMap.get(mod.getRootName()); } // -------------------------------------------------------------- Private private TwoPhaseCommitAction<T> createRemovalMetadataAction(ContentModification mod, boolean localLed) { if (localLed) { return new RemovalMetadataInsertionAction<T>(getSynchronizationActionContext(), mod); } else { return new NoOpSynchronizationAction<T>(getSynchronizationActionContext(), mod); } } private void updateContentMetadata(RepositoryContentMetadata newOfficial) { if (newOfficial.equals(this.officialContentMetadata) == false) { try { this.contentMetadataPersister.store(this.storeName, newOfficial); } catch (Exception e) { log.error("Caught exception persisting " + RepositoryContentMetadata.class.getSimpleName(), e); } this.officialContentMetadata = newOfficial; if (log.isTraceEnabled()) { log.trace("updateContentMetadata(): updated official metadata"); } } else if (log.isTraceEnabled()) { log.trace("updateContentMetadata(): content is unchanged"); } this.currentContentMetadata = null; } private void createItemMetadataFromScan(RepositoryRootMetadata rmd, RepositoryRootMetadata existingRMD, VirtualFile file, VirtualFile root) throws IOException { boolean directory = isDirectory(file); long timestamp = file.getLastModified(); List<String> pathElements = getRelativePath(file, root); RepositoryItemMetadata existing = existingRMD == null ? null : existingRMD.getItemMetadata(pathElements); // If there's an existing item, assume for now it's unchanged and keep existing originator String originator = existing == null ? this.localNodeName : existing.getOriginatingNode(); RepositoryItemMetadata md = new RepositoryItemMetadata(pathElements, timestamp, originator, directory, false); if (md.equals(existing) == false) { // above if test failing means this is a new item or // timestamp, removed or directory status has changed // In any case, this node is now the originator md.setOriginatingNode(this.localNodeName); } rmd.getContent().add(md); if (directory) { for(VirtualFile child: file.getChildren()) { createItemMetadataFromScan(rmd, existingRMD, child, root); } } } private void validateSynchronization(SynchronizationId<?> id) { if (id == null) { throw new IllegalArgumentException("Null id"); } if (this.currentSynchronizationActionContext == null) { throw new IllegalStateException("No active synchronization"); } SynchronizationId<?> ours = this.currentSynchronizationActionContext.getId(); if (id.equals(ours) == false) { throw new IllegalStateException(id + " does not match the current synchronization " + ours); } } private static boolean isDirectory(VirtualFile file) throws IOException { return (!file.isLeaf() && !file.isArchive()); } private static List<String> getRelativePath(VirtualFile file, VirtualFile root) throws IOException { List<String> reversed = new ArrayList<String>(); VirtualFile now = file; while(now != null && now.equals(root) == false) { reversed.add(now.getName()); now = now.getParent(); } if (now == null) { throw new IllegalArgumentException(file + " is not a child of " + root); } List<String> forward = new ArrayList<String>(reversed.size()); for (int i = reversed.size() - 1; i > -1; i--) { forward.add(reversed.get(i)); } return forward; } }