/*
* 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.Closeable;
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.vfs.VFS;
import org.jboss.vfs.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>();
/** The closeables. */
private Map<String, Closeable> mounts = new ConcurrentHashMap<String, Closeable>();
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 rootMap 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> rootMap,
ProfileKey profileKey, String localNodeName,
ContentMetadataPersister contentMetadataPersister)
{
if (rootMap == null)
{
throw new IllegalArgumentException("Null rootMap");
}
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 = rootMap;
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 void initialize()
{
String profileName = profileKey.getName();
VirtualFile backupRoot = VFS.getChild("/profileservice/originals/");
for (Map.Entry<String, URI> entry : namedURIMap.entrySet())
{
VirtualFile backup = backupRoot.getChild(profileName).getChild("roots").getChild(entry.getKey());
Closeable closeable = mountRepositoryRoot(entry.getValue(), backup);
vfCache.put(entry.getKey(), backup);
mounts.put(entry.getKey(), closeable);
}
}
public void shutdown()
{
for (Iterator<Map.Entry<String, Closeable>> it = mounts.entrySet().iterator(); it.hasNext();)
{
Map.Entry<String, Closeable> entry = it.next();
vfCache.remove(entry.getKey());
try
{
entry.getValue().close();
}
catch (IOException e)
{
log.error("Problem clearing repository root VFS mount for root " + entry.getKey(), e);
}
it.remove();
}
}
public RepositoryContentMetadata getOfficialContentMetadata()
{
return officialContentMetadata;
}
public RepositoryContentMetadata createEmptyContentMetadata()
{
RepositoryContentMetadata md = new RepositoryContentMetadata(profileKey);
List<RepositoryRootMetadata> roots = new ArrayList<RepositoryRootMetadata>();
for (String rootName : vfCache.keySet())
{
RepositoryRootMetadata rmd = new RepositoryRootMetadata();
rmd.setName(rootName);
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, VirtualFile> entry : vfCache.entrySet())
{
RepositoryRootMetadata rmd = new RepositoryRootMetadata();
rmd.setName(entry.getKey());
RepositoryRootMetadata existingRmd = official == null ? null : official.getRepositoryRootMetadata(entry.getKey());
VirtualFile root = 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;
Collection<RepositoryRootMetadata> roots = getOfficialContentMetadata().getRepositories();
for (RepositoryRootMetadata rmd : roots)
{
if (rmd.getItemMetadata(pathElements) != null)
{
// Exact match to existing item -- done
rootName = rmd.getName();
break;
}
}
if (rootName == null)
{
// Use the first root that can accept children
for (RepositoryRootMetadata rmd : roots)
{
VirtualFile vf = vfCache.get(rmd.getName());
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
{
VirtualFile vf = vfCache.get(item.getRootName());
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(String repositoryRoot, String relativePath) throws IOException
{
RepositoryContentMetadata cmd = new RepositoryContentMetadata(getOfficialContentMetadata());
RepositoryRootMetadata root = cmd.getRepositoryRootMetadata(repositoryRoot);
if (root == null)
{
throw new IllegalArgumentException(repositoryRoot + " is not a child of any known roots");
}
RepositoryItemMetadata remove = root.getItemMetadata(RepositoryItemMetadata.getPathElements(relativePath));
if (remove != null)
{
Collection<RepositoryItemMetadata> items = root.getContent();
if (remove.isDirectory())
{
for (Iterator<RepositoryItemMetadata> it = items.iterator(); it.hasNext(); )
{
if (it.next().isChildOf(remove))
{
it.remove();
}
}
}
items.remove(remove);
}
return cmd;
}
// -------------------------------------------------------------- Protected
protected abstract Closeable mountRepositoryRoot(URI realURI, VirtualFile backup);
/**
* 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;
}
protected Map<String, VirtualFile> getRootVirtualFiles()
{
return Collections.unmodifiableMap(vfCache);
}
/**
* 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 VirtualFile getRootVirtualFileForModification(ContentModification mod)
{
return vfCache.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.isDirectory();
}
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;
}
}