/* * JBoss, Home of Professional Open Source * Copyright 2009, Red Hat Middleware LLC, and individual contributors * by the @authors tag. See the copyright.txt in the distribution for a * full listing of individual contributors. * * 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.jboss.shrinkwrap.impl.base; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.UUID; import org.jboss.shrinkwrap.api.Archive; import org.jboss.shrinkwrap.api.ArchiveEvent; import org.jboss.shrinkwrap.api.ArchiveEventHandler; import org.jboss.shrinkwrap.api.ArchivePath; import org.jboss.shrinkwrap.api.ArchivePaths; import org.jboss.shrinkwrap.api.Configuration; import org.jboss.shrinkwrap.api.Filter; import org.jboss.shrinkwrap.api.IllegalArchivePathException; import org.jboss.shrinkwrap.api.IllegalOverwriteException; import org.jboss.shrinkwrap.api.Node; import org.jboss.shrinkwrap.api.asset.ArchiveAsset; import org.jboss.shrinkwrap.api.asset.Asset; import org.jboss.shrinkwrap.api.exporter.StreamExporter; import org.jboss.shrinkwrap.impl.base.path.BasicPath; import org.jboss.shrinkwrap.impl.base.path.PathUtil; /** * MemoryMapArchiveBase * * A base implementation for all MemoryMap archives. Thread-safe. * * @author <a href="mailto:baileyje@gmail.com">John Bailey</a> * @author <a href="mailto:aslak@conduct.no">Aslak Knutsen</a> * @version $Revision: $ * @param <T> */ public abstract class MemoryMapArchiveBase<T extends Archive<T>> extends ArchiveBase<T> implements Archive<T> { // -------------------------------------------------------------------------------------|| // Instance Members -------------------------------------------------------------------|| // -------------------------------------------------------------------------------------|| /** * Storage for the {@link Node}s. */ private final Map<ArchivePath, NodeImpl> content = Collections.synchronizedMap(new LinkedHashMap<ArchivePath, NodeImpl>()); /** * Storage for the {@link ArchiveAsset}s. Used to help get access to nested archive content. */ private final Map<ArchivePath, ArchiveAsset> nestedArchives = Collections.synchronizedMap(new LinkedHashMap<ArchivePath, ArchiveAsset>()); private final List<ArchiveEventHandler> handlers = new ArrayList<ArchiveEventHandler>(); // -------------------------------------------------------------------------------------|| // Constructor ------------------------------------------------------------------------|| // -------------------------------------------------------------------------------------|| /** * Constructor * * This constructor will generate a unique {@link Archive#getName()} per instance. * * @param configuration * The configuration for this archive * @throws IllegalArgumentException * If the configuration is not specified */ public MemoryMapArchiveBase(final Configuration configuration) throws IllegalArgumentException { this("Archive-" + UUID.randomUUID().toString() + ".jar", configuration); } /** * Constructor * * This constructor will generate an {@link Archive} with the provided name. * * @param archiveName * @param configuration * The configuration for this archive * @throws IllegalArgumentException * If the name or configuration is not specified */ public MemoryMapArchiveBase(final String archiveName, final Configuration configuration) throws IllegalArgumentException { super(archiveName, configuration); // Add the root node to the content final ArchivePath rootPath = new BasicPath("/"); content.put(rootPath, new NodeImpl(rootPath)); } // -------------------------------------------------------------------------------------|| // Required Implementations - Archive -------------------------------------------------|| // -------------------------------------------------------------------------------------|| /** * {@inheritDoc} * * @see org.jboss.shrinkwrap.api.Archive#add(org.jboss.shrinkwrap.api.asset.Asset, * org.jboss.shrinkwrap.api.ArchivePath) */ @Override public T add(Asset asset, ArchivePath path) { Validate.notNull(asset, "No asset was specified"); Validate.notNull(path, "No path was specified"); return addAsset(path, asset); } /** * {@inheritDoc} * * @see org.jboss.shrinkwrap.api.Archive#add(org.jboss.shrinkwrap.api.Archive, java.lang.String, java.lang.Class) */ @Override public T add(final Archive<?> archive, final String path, final Class<? extends StreamExporter> exporter) { Validate.notNull(archive, "Archive must be specified"); Validate.notNullOrEmpty(path, "Archive Path must be specified"); Validate.notNull(exporter, "exporter must be specified"); return this.add(archive, ArchivePaths.create(path), exporter); } /** * {@inheritDoc} * * @see org.jboss.shrinkwrap.impl.base.ArchiveBase#add(org.jboss.shrinkwrap.api.Archive, * org.jboss.shrinkwrap.api.ArchivePath, java.lang.Class) */ @Override public T add(final Archive<?> archive, final ArchivePath path, final Class<? extends StreamExporter> exporter) { // Add archive asset super.add(archive, path, exporter); // Expected Archive Path final ArchivePath archivePath = new BasicPath(path, archive.getName()); // Get the Asset that was just added final Node node = get(archivePath); // Make sure it is an ArchiveAsset if (node.getAsset() != null && node.getAsset() instanceof ArchiveAsset) { final ArchiveAsset archiveAsset = ArchiveAsset.class.cast(node.getAsset()); // Add asset to ArchiveAsset Map nestedArchives.put(archivePath, archiveAsset); } return covariantReturn(); } /** * {@inheritDoc} * * @see org.jboss.shrinkwrap.api.Archive#addAsDirectory(org.jboss.shrinkwrap.api.ArchivePath) */ @Override public T addAsDirectory(final ArchivePath path) throws IllegalArgumentException { // Precondition check Validate.notNull(path, "path must be specified"); // Adjust the path to remove any trailing slash ArchivePath adjustedPath = new BasicPath(PathUtil.optionallyRemoveFollowingSlash(path.get())); return addAsset(adjustedPath, null); } private T addAsset(ArchivePath path, Asset asset) { final Asset handledAsset = invokeHandlers(path, asset); // Disallow if we're dealing with a non-empty dir if (contains(path)) { if (asset != null) { // we're adding a file final Node node = this.get(path); if (node.getAsset() == null) { // Path exists as a dir, throw an exception throw new IllegalOverwriteException("Cannot add requested asset " + asset + " to path " + path.get() + " to archive " + this.getName() + "; path already exists as directory"); } else { // path exists as a file, overwrite addNewNode(path, handledAsset); } } // we're adding dir, it exists, do nothing } else { // Path does not exists, add new node addNewNode(path, handledAsset); } return covariantReturn(); } private void addNewNode(ArchivePath path, Asset handledAsset) { // Add the node to the content of the archive final NodeImpl newNode = new NodeImpl(path, handledAsset); content.put(path, newNode); // Add the new node to the parent as a child final NodeImpl parentNode = obtainParent(path.getParent()); if (parentNode != null) { parentNode.addChild(newNode); } } /** * {@inheritDoc} * @see org.jboss.shrinkwrap.api.Archive#addListener(org.jboss.shrinkwrap.api.Filter, org.jboss.shrinkwrap.api.ArchiveEventHandler) */ @Override public T addHandlers(ArchiveEventHandler... handlers) { for (ArchiveEventHandler handler : handlers) { this.handlers.add(handler); } return covariantReturn(); } private Asset invokeHandlers(ArchivePath path, Asset asset) { final ArchiveEvent event = new ArchiveEvent(path, asset); for (ArchiveEventHandler handler : handlers) { handler.handle(event); } return event.getHandledAsset(); } /** * {@inheritDoc} * * @see org.jboss.shrinkwrap.api.Archive#contains(org.jboss.shrinkwrap.api.ArchivePath) */ @Override public boolean contains(ArchivePath path) { Validate.notNull(path, "No path was specified"); boolean found = content.containsKey(path); if (!found) { found = nestedContains(path); } return found; } /** * {@inheritDoc} * * @see org.jboss.shrinkwrap.api.Archive#contains(java.lang.String) */ @Override public boolean contains(final String path) throws IllegalArgumentException { Validate.notNull(path, "Path must be specified"); final ArchivePath archivePath = ArchivePaths.create(path); return this.contains(archivePath); } /** * {@inheritDoc} * * @see org.jboss.shrinkwrap.api.Archive#delete(org.jboss.declarchive.api.ArchivePath) */ @Override public Node delete(ArchivePath path) { Validate.notNull(path, "No path was specified"); ArchivePath safePath = path; NodeImpl node = content.get(safePath); if (node == null) { if (path.get().endsWith("/")) { safePath = ArchivePaths.create(path.get().substring(0, path.get().length() - 1)); node = content.get(safePath); } if (node == null) { return null; } } return removeNodeRecursively(node, safePath); } /** * Removes the specified node and its associated children from the contents * of this archive. * * @param node the node to remove recursively * @param path the path denoting the specified node * @return the removed node itself */ private Node removeNodeRecursively(final NodeImpl node, final ArchivePath path) { final NodeImpl parentNode = content.get(path.getParent()); if (parentNode != null) { parentNode.removeChild(node); } // Remove from nested archives if present nestedArchives.remove(path); // Recursively delete children if present if (node.getChildren() != null) { final Set<Node> children = node.getChildren(); // can't remove from collection inside of the iteration final Set<Node> childrenCopy = new HashSet<Node>(children); for (Node child : childrenCopy) { node.removeChild(child); content.remove(child.getPath()); } } return content.remove(path); } /** * {@inheritDoc} * * @see org.jboss.shrinkwrap.api.Archive#delete(java.lang.String) */ @Override public Node delete(String archivePath) { Validate.notNull(archivePath, "No path was specified"); return delete(ArchivePaths.create(archivePath)); } /** * {@inheritDoc} * * @see org.jboss.shrinkwrap.api.Archive#get(org.jboss.shrinkwrap.api.ArchivePath) */ @Override public Node get(ArchivePath path) { Validate.notNull(path, "No path was specified"); Node node = content.get(path); if (node == null && contains(path)) { node = getNestedNode(path); } return node; } /** * {@inheritDoc} * * @see org.jboss.shrinkwrap.api.Archive#getContent() */ @Override public Map<ArchivePath, Node> getContent() { Map<ArchivePath, Node> ret = new LinkedHashMap<ArchivePath, Node>(); for (Map.Entry<ArchivePath, NodeImpl> item : content.entrySet()) { if (!item.getKey().equals(new BasicPath("/"))) { ret.put(item.getKey(), item.getValue()); } } return Collections.unmodifiableMap(ret); } /** * {@inheritDoc} * * @see org.jboss.shrinkwrap.api.Archive#getContent(org.jboss.shrinkwrap.api.Filter) */ @Override public Map<ArchivePath, Node> getContent(Filter<ArchivePath> filter) { Validate.notNull(filter, "Filter must be specified"); Map<ArchivePath, Node> filteredContent = new LinkedHashMap<ArchivePath, Node>(); for (Map.Entry<ArchivePath, NodeImpl> contentEntry : content.entrySet()) { if (filter.include(contentEntry.getKey())) { if (!contentEntry.getKey().equals(new BasicPath("/"))) { filteredContent.put(contentEntry.getKey(), contentEntry.getValue()); } } } return filteredContent; } // -------------------------------------------------------------------------------------|| // Internal Helper Methods ------------------------------------------------------------|| // -------------------------------------------------------------------------------------|| /** * Check to see if a path is found in a nested archive */ private boolean nestedContains(ArchivePath path) { // Iterate through nested archives for (Entry<ArchivePath, ArchiveAsset> nestedArchiveEntry : nestedArchives.entrySet()) { ArchivePath archivePath = nestedArchiveEntry.getKey(); ArchiveAsset archiveAsset = nestedArchiveEntry.getValue(); // Check to see if the requested path starts with the nested archive path if (startsWith(path, archivePath)) { Archive<?> nestedArchive = archiveAsset.getArchive(); // Get the asset path from within the nested archive ArchivePath nestedAssetPath = getNestedPath(path, archivePath); // Recurse the call to the nested archive return nestedArchive.contains(nestedAssetPath); } } return false; } /** * Attempt to get the asset from a nested archive. * * @param path * @return */ private Node getNestedNode(ArchivePath path) { // Iterate through nested archives for (Entry<ArchivePath, ArchiveAsset> nestedArchiveEntry : nestedArchives.entrySet()) { ArchivePath archivePath = nestedArchiveEntry.getKey(); ArchiveAsset archiveAsset = nestedArchiveEntry.getValue(); // Check to see if the requested path starts with the nested archive path if (startsWith(path, archivePath)) { Archive<?> nestedArchive = archiveAsset.getArchive(); // Get the asset path from within the nested archive ArchivePath nestedAssetPath = getNestedPath(path, archivePath); // Recurse the call to the nested archive return nestedArchive.get(nestedAssetPath); } } return null; } /** * Check to see if one path starts with another * * @param fullPath * @param startingPath * @return */ private boolean startsWith(ArchivePath fullPath, ArchivePath startingPath) { final String context = fullPath.get(); final String startingContext = startingPath.get(); return context.startsWith(startingContext); } /** * Given a full path and a base path return a new path containing the full path with the base path removed from the * beginning. * * @param fullPath * @param basePath * @return */ private ArchivePath getNestedPath(ArchivePath fullPath, ArchivePath basePath) { final String context = fullPath.get(); final String baseContent = basePath.get(); // Remove the base path from the full path String nestedArchiveContext = context.substring(baseContent.length()); return new BasicPath(nestedArchiveContext); } /** * Used to retrieve a {@link Node} from the content of the {@link Archive}. If the {@link Node} doesn�t exists in * the specified location, it is created and added to the {@link Archive}. The same happens to all its non-existing * parents. However, if the {@link Node} is an asset, an IllegalArchivePathException is thrown. * * @param path * The {@link ArchivePath} from which we are obtaining the {@link Node} * @return The {@link Node} in the specified path * @throws IllegalArchivePathException * if the node is an {@link Asset} */ private NodeImpl obtainParent(ArchivePath path) { if (path == null) { return null; } NodeImpl node = content.get(path); // If the node exists, just return it if (node != null) { // if the node is an asset, throw an exception if (node.getAsset() != null) { throw new IllegalArchivePathException("Could not create node under " + path.getParent() + ". It points to an asset."); } return node; } // If the node doesn't exists, create it. Also create all possible non-existing // parents node = new NodeImpl(path); NodeImpl parentNode = obtainParent(path.getParent()); if (parentNode != null) { parentNode.addChild(node); } // Add the node to the contents of the archive content.put(path, node); return node; } }