/* * 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.classloader.internal; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.Map; import javax.jcr.Item; import javax.jcr.Node; import javax.jcr.Property; import javax.jcr.RepositoryException; import javax.jcr.Session; import javax.jcr.SimpleCredentials; import org.apache.felix.scr.annotations.Activate; import org.apache.felix.scr.annotations.Component; import org.apache.felix.scr.annotations.Deactivate; import org.apache.felix.scr.annotations.Properties; import org.apache.felix.scr.annotations.Reference; import org.apache.felix.scr.annotations.ReferenceCardinality; import org.apache.felix.scr.annotations.ReferencePolicy; import org.apache.felix.scr.annotations.Service; import org.apache.sling.commons.classloader.ClassLoaderWriter; import org.apache.sling.commons.classloader.DynamicClassLoaderManager; import org.apache.sling.commons.mime.MimeTypeService; import org.apache.sling.commons.osgi.PropertiesUtil; import org.apache.sling.jcr.api.SlingRepository; import org.apache.sling.settings.SlingSettingsService; import org.osgi.framework.Bundle; import org.osgi.framework.ServiceReference; import org.osgi.service.component.ComponentContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * The <code>DynamicClassLoaderProviderImpl</code> provides * a class loader which loads classes from configured paths * in the repository. * It implements the {@link ClassLoaderWriter} interface * for clients to use for writing and reading such * classes and resources. */ @Component(metatype=true, label="%loader.name", description="%loader.description", name="org.apache.sling.jcr.classloader.internal.DynamicClassLoaderProviderImpl") @Service(value = ClassLoaderWriter.class, serviceFactory = true) @Properties({ @org.apache.felix.scr.annotations.Property(name="service.vendor", value="The Apache Software Foundation"), @org.apache.felix.scr.annotations.Property(name="service.description", value="Repository based classloader writer") }) public class ClassLoaderWriterImpl implements ClassLoaderWriter { /** Logger */ private final Logger logger = LoggerFactory.getLogger(ClassLoaderWriterImpl.class); private static final String CLASS_PATH_DEFAULT = "/var/classes"; @org.apache.felix.scr.annotations.Property(value=CLASS_PATH_DEFAULT) private static final String CLASS_PATH_PROP = "classpath"; private static final boolean APPEND_ID_DEFAULT = true; @org.apache.felix.scr.annotations.Property(boolValue=APPEND_ID_DEFAULT) private static final String APPEND_ID_PROP = "appendId"; /** Node type for packages/folders. */ private static final String NT_FOLDER = "nt:folder"; /** Default class loader owner. */ private static final String OWNER_DEFAULT = "admin"; @org.apache.felix.scr.annotations.Property(value=OWNER_DEFAULT) private static final String OWNER_PROP = "owner"; @Reference private SlingSettingsService settings; /** The owner of the class loader / JCR user. */ private String classLoaderOwner; /** The configured class path. */ private String classPath; @Reference private SlingRepository repository; @Reference(policy=ReferencePolicy.DYNAMIC, cardinality=ReferenceCardinality.OPTIONAL_UNARY) private volatile MimeTypeService mimeTypeService; @Reference( referenceInterface = DynamicClassLoaderManager.class, bind = "bindDynamicClassLoaderManager", unbind = "unbindDynamicClassLoaderManager") private volatile ServiceReference dynamicClassLoaderManager; /** The bundle asking for this service instance */ private Bundle callerBundle; /** Cached repository class loader. */ private volatile RepositoryClassLoader repositoryClassLoader; /** * Activate this component. * @param componentContext The component context * @param properties The configuration properties */ @Activate protected void activate(final ComponentContext componentContext, final Map<String, Object> properties) { this.classPath = PropertiesUtil.toString(properties.get(CLASS_PATH_PROP), CLASS_PATH_DEFAULT); if ( this.classPath.endsWith("/") ) { this.classPath = this.classPath.substring(0, this.classPath.length() - 1); } if ( PropertiesUtil.toBoolean(properties.get(APPEND_ID_PROP), APPEND_ID_DEFAULT) ) { this.classPath = this.classPath + '/' + this.settings.getSlingId(); } this.classLoaderOwner = PropertiesUtil.toString(properties.get(OWNER_PROP), OWNER_DEFAULT); this.callerBundle = componentContext.getUsingBundle(); } /** * Deactivate this component. */ @Deactivate protected synchronized void deactivate() { this.destroyRepositoryClassLoader(); this.callerBundle = null; } /** * Called to handle binding the DynamicClassLoaderManager service * reference */ @SuppressWarnings("unused") private void bindDynamicClassLoaderManager(final ServiceReference ref) { this.dynamicClassLoaderManager = ref; } /** * Called to handle unbinding of the DynamicClassLoaderManager service * reference */ @SuppressWarnings("unused") private void unbindDynamicClassLoaderManager(final ServiceReference ref) { if (this.dynamicClassLoaderManager == ref) { this.dynamicClassLoaderManager = null; } } /** * Destroys the repository class loader if existing and ungets the * DynamicClassLoaderManager service if a dynamic class loader is * being used. */ private void destroyRepositoryClassLoader() { final RepositoryClassLoader rcl = this.repositoryClassLoader; if (rcl != null) { this.repositoryClassLoader = null; rcl.destroy(); final ServiceReference localDynamicClassLoaderManager = this.dynamicClassLoaderManager; final Bundle localCallerBundle = this.callerBundle; if ( localDynamicClassLoaderManager != null && localCallerBundle != null ) { localCallerBundle.getBundleContext().ungetService(localDynamicClassLoaderManager); } } } /** * Return a new session. */ public Session createSession() throws RepositoryException { // get an administrative session for potential impersonation final Session admin = this.repository.loginAdministrative(null); // do use the admin session, if the admin's user id is the same as owner if (admin.getUserID().equals(this.classLoaderOwner)) { return admin; } // else impersonate as the owner and logout the admin session again try { return admin.impersonate(new SimpleCredentials(this.classLoaderOwner, new char[0])); } finally { admin.logout(); } } /** * Is this still active? */ public boolean isActivate() { return this.repository != null; } private synchronized RepositoryClassLoader getOrCreateClassLoader() { if ( this.repositoryClassLoader == null || !this.repositoryClassLoader.isLive() ) { // make sure to cleanup any existing class loader this.destroyRepositoryClassLoader(); // get the dynamic class loader for the bundle using this // class loader writer final DynamicClassLoaderManager dclm = (DynamicClassLoaderManager) this.callerBundle.getBundleContext().getService( this.dynamicClassLoaderManager); this.repositoryClassLoader = new RepositoryClassLoader( this.classPath, this, dclm.getDynamicClassLoader()); } return this.repositoryClassLoader; } private synchronized void handleChangeEvent(final String path) { final RepositoryClassLoader rcl = this.repositoryClassLoader; if ( rcl != null ) { rcl.handleEvent(path); } } /** * @see org.apache.sling.commons.classloader.ClassLoaderWriter#delete(java.lang.String) */ public boolean delete(final String name) { final String path = cleanPath(name); this.handleChangeEvent(path); Session session = null; try { session = createSession(); if (session.itemExists(path)) { Item fileItem = session.getItem(path); fileItem.remove(); session.save(); return true; } } catch (final RepositoryException re) { logger.error("Cannot remove " + path, re); } finally { if ( session != null ) { session.logout(); } } // fall back to false if item does not exist or in case of error return false; } /** * @see org.apache.sling.commons.classloader.ClassLoaderWriter#getOutputStream(java.lang.String) */ public OutputStream getOutputStream(final String name) { final String path = cleanPath(name); return new RepositoryOutputStream(this, path); } /** * @see org.apache.sling.commons.classloader.ClassLoaderWriter#rename(java.lang.String, java.lang.String) */ public boolean rename(final String oldName, final String newName) { final String oldPath = cleanPath(oldName); final String newPath = cleanPath(newName); Session session = null; try { session = this.createSession(); session.move(oldPath, newPath); session.save(); this.handleChangeEvent(oldName); this.handleChangeEvent(newName); return true; } catch (final RepositoryException re) { logger.error("Cannot rename " + oldName + " to " + newName, re); } finally { if ( session != null ) { session.logout(); } } // fall back to false in case of error or non-existence of oldFileName return false; } /** * Creates a folder hierarchy in the repository. * We synchronize this method to reduce potential conflicts. * Although each write uses its own session it might occur * that more than one session tries to create the same path * (or parent path) at the same time. By synchronizing this * we avoid this situation - however this method is written * in a fail safe manner anyway. */ private synchronized boolean mkdirs(final Session session, final String path) { try { // quick test if (session.itemExists(path) && session.getItem(path).isNode()) { return true; } // check path walking it down Node current = session.getRootNode(); final String[] names = path.split("/"); for (int i = 0; i < names.length; i++) { if (names[i] == null || names[i].length() == 0) { continue; } else if (current.hasNode(names[i])) { current = current.getNode(names[i]); } else { final Node parentNode = current; try { // adding the node could cause an exception // for example if another thread tries to // create the node "at the same time" current = parentNode.addNode(names[i], NT_FOLDER); session.save(); } catch (final RepositoryException re) { // let's first refresh the session // we don't catch an exception here, because if // session refresh fails, we might have a serious problem! session.refresh(false); // let's check if the node is available now if ( parentNode.hasNode(names[i]) ) { current = parentNode.getNode(names[i]); } else { // we try it one more time to create the node - and fail otherwise current = parentNode.addNode(names[i], NT_FOLDER); session.save(); } } } } return true; } catch (final RepositoryException re) { logger.error("Cannot create folder path:" + path, re); // discard changes try { session.refresh(false); } catch (final RepositoryException e) { // we simply ignore this } } // false in case of error or no need to create return false; } /** * Helper method to clean the path. * It replaces backslashes with slashes and cuts off trailing spaces. * It uses the first configured class path to access the path. */ private String cleanPath(String path) { // replace backslash by slash path = path.replace('\\', '/'); // cut off trailing slash while (path.endsWith("/")) { path = path.substring(0, path.length() - 1); } return this.classPath + path; } private static class RepositoryOutputStream extends ByteArrayOutputStream { private final ClassLoaderWriterImpl repositoryOutputProvider; private final String fileName; RepositoryOutputStream(ClassLoaderWriterImpl repositoryOutputProvider, String fileName) { this.repositoryOutputProvider = repositoryOutputProvider; this.fileName = fileName; } /** * @see java.io.ByteArrayOutputStream#close() */ @Override public void close() throws IOException { super.close(); Session session = null; try { // get an own session for writing session = repositoryOutputProvider.createSession(); final int lastPos = fileName.lastIndexOf('/'); final String path = (lastPos == -1 ? null : fileName.substring(0, lastPos)); final String name = (lastPos == -1 ? fileName : fileName.substring(lastPos + 1)); if ( lastPos != -1 ) { if ( !repositoryOutputProvider.mkdirs(session, path) ) { throw new IOException("Unable to create path for " + path); } } Node fileNode = null; Node contentNode = null; Node parentNode = null; if (session.itemExists(fileName)) { final Item item = session.getItem(fileName); if (item.isNode()) { final Node node = item.isNode() ? (Node) item : item.getParent(); if ("jcr:content".equals(node.getName())) { // replace the content properties of the jcr:content // node parentNode = node; contentNode = node; } else if (node.isNodeType("nt:file")) { // try to set the content properties of jcr:content // node parentNode = node; contentNode = node.getNode("jcr:content"); } else { // fileName is a node // try to set the content properties of the node parentNode = node; contentNode = node; } } else { // replace property with an nt:file node (if possible) parentNode = item.getParent(); item.remove(); session.save(); fileNode = parentNode.addNode(name, "nt:file"); } } else { if (lastPos <= 0) { parentNode = session.getRootNode(); } else { Item parent = session.getItem(path); if (!parent.isNode()) { throw new IOException("Parent at " + path + " is not a node."); } parentNode = (Node) parent; } fileNode = parentNode.addNode(name, "nt:file"); } // if we have a file node, create the contentNode if (fileNode != null) { contentNode = fileNode.addNode("jcr:content", "nt:resource"); } final MimeTypeService mtService = this.repositoryOutputProvider.mimeTypeService; String mimeType = (mtService == null ? null : mtService.getMimeType(fileName)); if (mimeType == null) { mimeType = "application/octet-stream"; } contentNode.setProperty("jcr:lastModified", System.currentTimeMillis()); contentNode.setProperty("jcr:data", new ByteArrayInputStream(buf, 0, size())); contentNode.setProperty("jcr:mimeType", mimeType); session.save(); this.repositoryOutputProvider.handleChangeEvent(fileName); } catch (final RepositoryException re) { throw (IOException)new IOException("Cannot write file " + fileName + ", reason: " + re.toString()).initCause(re); } finally { if ( session != null ) { session.logout(); } } } } /** * @see org.apache.sling.commons.classloader.ClassLoaderWriter#getInputStream(java.lang.String) */ public InputStream getInputStream(final String name) throws IOException { final String path = cleanPath(name) + "/jcr:content/jcr:data"; Session session = null; try { session = this.createSession(); if ( session.itemExists(path) ) { final Property prop = (Property)session.getItem(path); final InputStream is = prop.getStream(); final ByteArrayOutputStream baos = new ByteArrayOutputStream(); int l = 0; final byte[] buf = new byte[2048]; while ( (l = is.read(buf)) > -1 ) { if ( l > 0 ) { baos.write(buf, 0, l); } } return new ByteArrayInputStream(baos.toByteArray()); } throw new FileNotFoundException("Unable to find " + name); } catch (final RepositoryException re) { throw (IOException) new IOException( "Failed to get InputStream for " + name).initCause(re); } finally { if ( session != null ) { session.logout(); } } } /** * @see org.apache.sling.commons.classloader.ClassLoaderWriter#getLastModified(java.lang.String) */ public long getLastModified(final String name) { final String path = cleanPath(name) + "/jcr:content/jcr:lastModified"; Session session = null; try { session = this.createSession(); if ( session.itemExists(path) ) { final Property prop = (Property)session.getItem(path); return prop.getLong(); } } catch (final RepositoryException se) { logger.error("Cannot get last modification time for " + name, se); } finally { if ( session != null ) { session.logout(); } } // fall back to "non-existent" in case of problems return -1; } /** * @see org.apache.sling.commons.classloader.ClassLoaderWriter#getClassLoader() */ public ClassLoader getClassLoader() { return this.getOrCreateClassLoader(); } }