/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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.apache.sling.jcr.contentloader.internal; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.net.URL; import java.net.URLConnection; import java.net.URLDecoder; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.StringTokenizer; import javax.jcr.Item; import javax.jcr.NoSuchWorkspaceException; import javax.jcr.Node; import javax.jcr.RepositoryException; import javax.jcr.Session; import org.apache.commons.io.IOUtils; import org.apache.sling.jcr.contentloader.ContentReader; import org.osgi.framework.Bundle; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * The <code>BundleContentLoader</code> loads initial content from the bundle. */ public class BundleContentLoader extends BaseImportLoader { public static final String PARENT_DESCRIPTOR = "ROOT"; private final Logger log = LoggerFactory.getLogger(BundleContentLoader.class); private BundleHelper bundleHelper; // bundles whose registration failed and should be retried private List<Bundle> delayedBundles; public BundleContentLoader(BundleHelper bundleHelper, ContentReaderWhiteboard contentReaderWhiteboard) { super(contentReaderWhiteboard); this.bundleHelper = bundleHelper; this.delayedBundles = new LinkedList<Bundle>(); } public void dispose() { if (delayedBundles != null) { delayedBundles.clear(); delayedBundles = null; } bundleHelper = null; } /** * Register a bundle and install its content. * * @param metadataSession * @param bundle * @throws RepositoryException */ public void registerBundle(final Session metadataSession, final Bundle bundle, final boolean isUpdate) throws RepositoryException { // if this is an update, we have to uninstall the old content first if (isUpdate) { this.unregisterBundle(metadataSession, bundle); } log.debug("Registering bundle {} for content loading.", bundle.getSymbolicName()); if (registerBundleInternal(metadataSession, bundle, false, isUpdate)) { // handle delayed bundles, might help now int currentSize = -1; for (int i = delayedBundles.size(); i > 0 && currentSize != delayedBundles.size() && !delayedBundles.isEmpty(); i--) { for (Iterator<Bundle> di = delayedBundles.iterator(); di.hasNext(); ) { Bundle delayed = di.next(); if (registerBundleInternal(metadataSession, delayed, true, false)) { di.remove(); } } currentSize = delayedBundles.size(); } } else if (!isUpdate) { // add to delayed bundles - if this is not an update! delayedBundles.add(bundle); } } private boolean registerBundleInternal(final Session metadataSession, final Bundle bundle, final boolean isRetry, final boolean isUpdate) { // check if bundle has initial content final Iterator<PathEntry> pathIter = PathEntry.getContentPaths(bundle); if (pathIter == null) { log.debug("Bundle {} has no initial content", bundle.getSymbolicName()); return true; } try { bundleHelper.createRepositoryPath(metadataSession, ContentLoaderService.BUNDLE_CONTENT_NODE); // check if the content has already been loaded final Map<String, Object> bundleContentInfo = bundleHelper.getBundleContentInfo(metadataSession, bundle, true); // if we don't get an info, someone else is currently loading if (bundleContentInfo == null) { return false; } boolean success = false; List<String> createdNodes = null; try { final boolean contentAlreadyLoaded = ((Boolean) bundleContentInfo.get(ContentLoaderService.PROPERTY_CONTENT_LOADED)).booleanValue(); boolean isBundleUpdated = false; Calendar lastLoadedAt = (Calendar) bundleContentInfo.get(ContentLoaderService.PROPERTY_CONTENT_LOADED_AT); if (lastLoadedAt != null) { // this assumes that the bundle has been installed or updated after the content has been loaded if (lastLoadedAt.getTimeInMillis() < bundle.getLastModified()) { isBundleUpdated = true; } } if (!isUpdate && !isBundleUpdated && contentAlreadyLoaded) { log.info("Content of bundle already loaded {}.", bundle.getSymbolicName()); } else { createdNodes = installContent(metadataSession, bundle, pathIter, contentAlreadyLoaded && !isBundleUpdated); if (isRetry) { // log success of retry log.info("Retrying to load initial content for bundle {} succeeded.", bundle.getSymbolicName()); } } success = true; return true; } finally { bundleHelper.unlockBundleContentInfo(metadataSession, bundle, success, createdNodes); } } catch (RepositoryException re) { // if we are retrying we already logged this message once, so we // won't log it again if (!isRetry) { log.error("Cannot load initial content for bundle " + bundle.getSymbolicName() + " : " + re.getMessage(), re); } } return false; } /** * Unregister a bundle. Remove installed content. * * @param bundle The bundle. */ public void unregisterBundle(final Session session, final Bundle bundle) { if (delayedBundles.contains(bundle)) { delayedBundles.remove(bundle); } else { try { bundleHelper.createRepositoryPath(session, ContentLoaderService.BUNDLE_CONTENT_NODE); final Map<String, Object> bundleContentInfo = bundleHelper.getBundleContentInfo(session, bundle, false); // if we don't get an info, someone else is currently loading or unloading // or the bundle is already uninstalled if (bundleContentInfo == null) { return; } try { uninstallContent(session, bundle, (String[]) bundleContentInfo.get(ContentLoaderService.PROPERTY_UNINSTALL_PATHS)); bundleHelper.contentIsUninstalled(session, bundle); } finally { bundleHelper.unlockBundleContentInfo(session, bundle, false, null); } } catch (RepositoryException re) { log.error("Cannot remove initial content for bundle " + bundle.getSymbolicName() + " : " + re.getMessage(), re); } } } // ---------- internal ----------------------------------------------------- /** * Install the content from the bundle. * * @return If the content should be removed on uninstall, a list of top nodes */ private List<String> installContent(final Session defaultSession, final Bundle bundle, final Iterator<PathEntry> pathIter, final boolean contentAlreadyLoaded) throws RepositoryException { final List<String> createdNodes = new ArrayList<String>(); final Map<String, Session> createdSessions = new HashMap<String, Session>(); log.debug("Installing initial content from bundle {}", bundle.getSymbolicName()); final DefaultContentCreator contentCreator = new DefaultContentCreator(this.bundleHelper); try { while (pathIter.hasNext()) { final PathEntry pathEntry = pathIter.next(); if (!contentAlreadyLoaded || pathEntry.isOverwrite()) { String workspace = pathEntry.getWorkspace(); final Session targetSession; if (workspace != null) { if (createdSessions.containsKey(workspace)) { targetSession = createdSessions.get(workspace); } else { targetSession = createSession(workspace); createdSessions.put(workspace, targetSession); } } else { targetSession = defaultSession; } final Node targetNode = getTargetNode(targetSession, pathEntry.getTarget()); if (targetNode != null) { installFromPath(bundle, pathEntry.getPath(), pathEntry, targetNode, pathEntry.isUninstall() ? createdNodes : null, contentCreator); } } } // now optimize created nodes list Collections.sort(createdNodes); if (createdNodes.size() > 1) { final Iterator<String> i = createdNodes.iterator(); String previous = i.next() + '/'; while (i.hasNext()) { final String current = i.next(); if (current.startsWith(previous)) { i.remove(); } else { previous = current + '/'; } } } // persist modifications now defaultSession.refresh(true); defaultSession.save(); for (Session session : createdSessions.values()) { session.refresh(true); session.save(); } // finally check in versionable nodes for (final Node versionable : contentCreator.getVersionables()) { versionable.checkin(); } } finally { try { if (defaultSession.hasPendingChanges()) { defaultSession.refresh(false); } for (Session session : createdSessions.values()) { if (session.hasPendingChanges()) { session.refresh(false); } } } catch (RepositoryException re) { log.warn("Failure to rollback partial initial content for bundle {}", bundle.getSymbolicName(), re); } contentCreator.clear(); for (Session session : createdSessions.values()) { session.logout(); } } log.debug("Done installing initial content from bundle {}", bundle.getSymbolicName()); return createdNodes; } /** * Handle content installation for a single path. * * @param bundle The bundle containing the content. * @param path The path * @param configuration * @param parent The parent node. * @param createdNodes An optional list to store all new nodes. This list is used for an uninstall * @throws RepositoryException */ private void installFromPath(final Bundle bundle, final String path, final PathEntry configuration, final Node parent, final List<String> createdNodes, final DefaultContentCreator contentCreator) throws RepositoryException { // init content creator contentCreator.init(configuration, getContentReaders(), createdNodes, null); final Map<String, Node> processedEntries = new HashMap<String, Node>(); @SuppressWarnings("unchecked") Enumeration<String> entries = bundle.getEntryPaths(path); if (entries == null) { // check for single content final URL u = bundle.getEntry(path); if (u == null) { log.info("install: No initial content entries at {} in bundle {}", path, bundle.getSymbolicName()); return; } // we have a single file content, let's check if this has an content reader extension for (String ext : contentCreator.getContentReaders().keySet()) { if (path.endsWith(ext)) { } } handleFile(path, bundle, processedEntries, configuration, parent, createdNodes, contentCreator); return; } // potential parent node import/extension URL parentNodeDescriptor = importParentNode(bundle, path, parent, contentCreator); if (parentNodeDescriptor != null) { processedEntries.put(parentNodeDescriptor.toString(), parent); } while (entries.hasMoreElements()) { final String entry = entries.nextElement(); log.debug("Processing initial content entry {} in bundle {}", entry, bundle.getSymbolicName()); if (entry.endsWith("/")) { // dir, check for node descriptor, else create dir final String base = entry.substring(0, entry.length() - 1); URL nodeDescriptor = null; for (String ext : contentCreator.getContentReaders().keySet()) { nodeDescriptor = bundle.getEntry(base + ext); if (nodeDescriptor != null) { break; } } // if we have a descriptor, which has not been processed yet, // otherwise call createFolder, which creates an nt:folder or // returns an existing node (created by a descriptor) final String name = getName(base); Node node = null; if (nodeDescriptor != null) { node = processedEntries.get(nodeDescriptor.toString()); if (node == null) { node = createNode(parent, name, nodeDescriptor, contentCreator, configuration); processedEntries.put(nodeDescriptor.toString(), node); } } else { node = createFolder(parent, name, configuration.isOverwrite()); } // walk down the line if (node != null) { installFromPath(bundle, entry, configuration, node, createdNodes, contentCreator); } } else { // file => create file handleFile(entry, bundle, processedEntries, configuration, parent, createdNodes, contentCreator); } } } /** * Handle a file entry. * * @param entry * @param bundle * @param processedEntries * @param configuration * @param parent * @param createdNodes * @throws RepositoryException */ private void handleFile(final String entry, final Bundle bundle, final Map<String, Node> processedEntries, final PathEntry configuration, final Node parent, final List<String> createdNodes, final DefaultContentCreator contentCreator) throws RepositoryException { final URL file = bundle.getEntry(entry); final String name = getName(entry); try { if (processedEntries.containsKey(file.toString())) { // this is a consumed node descriptor return; } // check for node descriptor URL nodeDescriptor = null; for (String ext : contentCreator.getContentReaders().keySet()) { nodeDescriptor = bundle.getEntry(entry + ext); if (nodeDescriptor != null) { break; } } // install if it is a descriptor boolean foundReader = getContentReader(entry, configuration) != null; Node node = null; if (foundReader) { node = createNode(parent, name, file, contentCreator, configuration); if (node != null) { log.debug("Created node as {} {}", node.getPath(), name); processedEntries.put(file.toString(), node); } else { log.warn("No node created for file {} {}", file, name); } } else { log.debug("Can't find content reader for entry {} at {}", entry, name); } // otherwise just place as file if (node == null) { try { createFile(configuration, parent, file, createdNodes, contentCreator); node = parent.getNode(name); } catch (IOException ioe) { log.warn("Cannot create file node for {}", file, ioe); } } // if we have a descriptor, which has not been processed yet, // process it if (nodeDescriptor != null && !processedEntries.containsKey(nodeDescriptor.toString())) { try { contentCreator.setIgnoreOverwriteFlag(true); node = createNode(parent, name, nodeDescriptor, contentCreator, configuration); processedEntries.put(nodeDescriptor.toString(), node); } finally { contentCreator.setIgnoreOverwriteFlag(false); } } } catch (RepositoryException e) { log.error("Failed to process file {} from {}", file, name); throw e; } } /** * Create a new node from a content resource found in the bundle. * * @param parent The parent node * @param name The name of the new content node * @param resourceUrl The resource url. * @param contentCreator the content creator * @param configuration the configuration for the node that needs to be created * @return * @throws RepositoryException */ private Node createNode(Node parent, String name, URL resourceUrl, final DefaultContentCreator contentCreator, PathEntry configuration) throws RepositoryException { final String resourcePath = resourceUrl.getPath().toLowerCase(); InputStream contentStream = null; try { // special treatment for system view imports if (resourcePath.endsWith(EXT_JCR_XML)) { contentStream = resourceUrl.openStream(); return importJcrXml(parent, name, contentStream, false); } // get the node reader for this resource final ContentReader nodeReader = getContentReader(resourcePath, configuration); // cannot find out the type if (nodeReader == null) { return null; } final String contentReaderExtension = getContentReaderExtension(name); contentCreator.prepareParsing(parent, toPlainName(name, contentReaderExtension)); nodeReader.parse(resourceUrl, contentCreator); return contentCreator.getCreatedRootNode(); } catch (RepositoryException re) { throw re; } catch (Throwable t) { throw new RepositoryException(t.getMessage(), t); } finally { IOUtils.closeQuietly(contentStream); } } /** * Create a folder * * @param parent The parent node. * @param name The name of the folder * @param overwrite If set to true, an existing folder is removed first. * @return The node pointing to the folder. * @throws RepositoryException */ private Node createFolder(Node parent, String name, final boolean overwrite) throws RepositoryException { if (parent.hasNode(name)) { if (overwrite) { parent.getNode(name).remove(); } else { return parent.getNode(name); } } return parent.addNode(name, "sling:Folder"); } /** * Create a file from the given url. * * @param configuration * @param parent * @param source * @param createdNodes * @param contentCreator * @throws IOException * @throws RepositoryException */ private void createFile(PathEntry configuration, Node parent, URL source, List<String> createdNodes, final DefaultContentCreator contentCreator) throws IOException, RepositoryException { final String srcPath = source.getPath(); int pos = srcPath.lastIndexOf("/"); final String name = getName(source.getPath()); final String path; if (pos == -1) { path = name; } else { path = srcPath.substring(0, pos + 1) + name; } contentCreator.init(configuration, getContentReaders(), createdNodes, null); contentCreator.prepareParsing(parent, name); final URLConnection conn = source.openConnection(); final long lastModified = Math.min(conn.getLastModified(), configuration.getLastModified()); final String type = conn.getContentType(); final InputStream data = conn.getInputStream(); contentCreator.createFileAndResourceNode(path, data, type, lastModified); contentCreator.finishNode(); contentCreator.finishNode(); } /** * Gets and decodes the name part of the <code>path</code>. The name is * the part of the path after the last slash (or the complete path if no * slash is contained). To support names containing unsupported characters * such as colon (<code>:</code>), names may be URL encoded (see * <code>java.net.URLEncoder</code>) using the <i>UTF-8</i> character * encoding. In this case, this method decodes the name using the * <code>java.net.URLDecoder</code> class with the <i>UTF-8</i> character * encoding. * * @param path The path from which to extract the name part. * @return The URL decoded name part. */ private String getName(String path) { int lastSlash = path.lastIndexOf('/'); String name = (lastSlash < 0) ? path : path.substring(lastSlash + 1); // check for encoded characters (%xx) // has encoded characters, need to decode if (name.indexOf('%') >= 0) { try { return URLDecoder.decode(name, "UTF-8"); } catch (UnsupportedEncodingException uee) { // actually unexpected because UTF-8 is required by the spec log.error("Cannot decode " + name + " because the platform has no support for UTF-8, using undecoded"); } catch (Exception e) { // IllegalArgumentException or failure to decode log.error("Cannot decode " + name + ", using undecoded", e); } } // not encoded or problems decoding, return the name unmodified return name; } private Node getTargetNode(Session session, String path) throws RepositoryException { // not specified path directive if (path == null) { return session.getRootNode(); } if (!path.startsWith("/")) { // make relative path absolute path = "/" + path; } if (!session.itemExists(path)) { Node currentNode = session.getRootNode(); final StringTokenizer st = new StringTokenizer(path.substring(1), "/"); while (st.hasMoreTokens()) { final String name = st.nextToken(); if (!currentNode.hasNode(name)) { currentNode.addNode(name, "sling:Folder"); } currentNode = currentNode.getNode(name); } return currentNode; } Item item = session.getItem(path); return (item.isNode()) ? (Node) item : null; } private void uninstallContent(final Session defaultSession, final Bundle bundle, final String[] uninstallPaths) { final Map<String, Session> createdSessions = new HashMap<String, Session>(); try { log.debug("Uninstalling initial content from bundle {}", bundle.getSymbolicName()); if (uninstallPaths != null && uninstallPaths.length > 0) { for (String path : uninstallPaths) { final Session targetSession; final int wsSepPos = path.indexOf(":/"); if (wsSepPos != -1) { final String workspaceName = path.substring(0, wsSepPos); path = path.substring(wsSepPos + 1); if (workspaceName.equals(defaultSession.getWorkspace().getName())) { targetSession = defaultSession; } else if (createdSessions.containsKey(workspaceName)) { targetSession = createdSessions.get(workspaceName); } else { targetSession = createSession(workspaceName); createdSessions.put(workspaceName, targetSession); } } else { targetSession = defaultSession; } if (targetSession.itemExists(path)) { targetSession.getItem(path).remove(); } } // persist modifications now defaultSession.save(); for (Session session : createdSessions.values()) { session.save(); } } log.debug("Done uninstalling initial content from bundle {}", bundle.getSymbolicName()); } catch (RepositoryException re) { log.error("Unable to uninstall initial content from bundle " + bundle.getSymbolicName(), re); } finally { try { if (defaultSession.hasPendingChanges()) { defaultSession.refresh(false); } for (Session session : createdSessions.values()) { if (session.hasPendingChanges()) { session.refresh(false); } } } catch (RepositoryException re) { log.warn("Failure to rollback uninstalling initial content for bundle {}", bundle.getSymbolicName(), re); } for (Session session : createdSessions.values()) { session.logout(); } } } protected static final class Descriptor { public URL url; public ContentReader contentReader; } /** * Return the parent node descriptor (ROOT). */ private Descriptor getParentNodeDescriptor(final Bundle bundle, final String path, final DefaultContentCreator contentCreator) { for (Map.Entry<String, ContentReader> entry : contentCreator.getContentReaders().entrySet()) { if (entry.getValue() != null) { final StringBuilder filePath = new StringBuilder(path); if (!path.endsWith("/")) { filePath.append("/"); } filePath.append(PARENT_DESCRIPTOR); // add file extension, e.g. .jcr.xml, .xml, .zip (see BaseImportLoader) filePath.append(entry.getKey()); URL url = bundle.getEntry(filePath.toString()); if (url != null) { final Descriptor descriptor = new Descriptor(); descriptor.url = url; descriptor.contentReader = entry.getValue(); return descriptor; } } } return null; } /** * Imports mixin nodes and properties (and optionally child nodes) of the * parent node. */ private URL importParentNode(Bundle bundle, String path, Node parent, final DefaultContentCreator contentCreator) throws RepositoryException { final Descriptor descriptor = getParentNodeDescriptor(bundle, path, contentCreator); // no parent descriptor (ROOT) found if (descriptor == null) { return null; } try { contentCreator.prepareParsing(parent, null); descriptor.contentReader.parse(descriptor.url, contentCreator); return descriptor.url; } catch (RepositoryException re) { throw re; } catch (Throwable t) { throw new RepositoryException(t.getMessage(), t); } } private Session createSession(String workspace) throws RepositoryException { try { return bundleHelper.getSession(workspace); } catch (NoSuchWorkspaceException e) { Session temp = bundleHelper.getSession(); temp.getWorkspace().createWorkspace(workspace); temp.logout(); return bundleHelper.getSession(workspace); } } }