/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 1997-2010 Oracle and/or its affiliates. All rights reserved.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License.  You can
 * obtain a copy of the License at
 * https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html
 * or packager/legal/LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 *
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at packager/legal/LICENSE.txt.
 *
 * GPL Classpath Exception:
 * Oracle designates this particular file as subject to the "Classpath"
 * exception as provided by Oracle in the GPL Version 2 section of the License
 * file that accompanied this code.
 *
 * Modifications:
 * If applicable, add the following below the License Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyright [year] [name of copyright owner]"
 *
 * Contributor(s):
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 *
 *
 * This file incorporates work covered by the following copyright and
 * permission notice:
 *
 * Copyright 2004 The Apache Software Foundation
 *
 * 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.apache.jasper.compiler;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FilePermission;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLDecoder;
import java.security.CodeSource;
import java.security.cert.Certificate;
import java.security.PermissionCollection;
import java.security.Policy;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.*;
import java.util.logging.Logger;
import java.util.logging.Level;

import javax.servlet.ServletContext;
import javax.servlet.jsp.JspFactory;
import javax.tools.JavaFileObject;

import org.apache.jasper.Constants;
import org.apache.jasper.JspCompilationContext;
import org.apache.jasper.Options;
import org.apache.jasper.runtime.JspFactoryImpl;
import org.apache.jasper.security.SecurityClassLoad;
import org.apache.jasper.servlet.JspServletWrapper;

/**
 * Class for tracking JSP compile time file dependencies when the
 * &060;%@include file="..."%&062; directive is used.
 *
 * A background thread periodically checks the files a JSP page
 * is dependent upon.  If a dpendent file changes the JSP page
 * which included it is recompiled.
 *
 * Only used if a web application context is a directory.
 *
 * @author Glenn L. Nielsen
 * @version $Revision: 1.13 $
 */
public final class JspRuntimeContext implements Runnable {

    // Logger
    private static Logger log =
            Logger.getLogger(JspRuntimeContext.class.getName());

    /*
     * Counts how many times the webapp's JSPs have been reloaded.
     */
    private AtomicInteger jspReloadCount = new AtomicInteger(0);

    /**
     * Preload classes required at runtime by a JSP servlet so that
     * we don't get a defineClassInPackage security exception.
     */
    static {
        JspFactoryImpl factory = new JspFactoryImpl();
        SecurityClassLoad.securityClassLoad(factory.getClass().getClassLoader());
        if( System.getSecurityManager() != null ) {
            String basePackage = "org.apache.jasper.";
            try {
                factory.getClass().getClassLoader().loadClass(
                    basePackage
                    + "runtime.JspFactoryImpl$PrivilegedGetPageContext");
                factory.getClass().getClassLoader().loadClass(
                    basePackage
                    + "runtime.JspFactoryImpl$PrivilegedReleasePageContext");
                factory.getClass().getClassLoader().loadClass(
                    basePackage
                    + "runtime.JspRuntimeLibrary");
                factory.getClass().getClassLoader().loadClass(
                    basePackage
                    + "runtime.JspRuntimeLibrary$PrivilegedIntrospectHelper");
                factory.getClass().getClassLoader().loadClass(
                    basePackage
                    + "runtime.ServletResponseWrapperInclude");
                factory.getClass().getClassLoader().loadClass(
                    basePackage
                    + "servlet.JspServletWrapper");
            } catch (ClassNotFoundException ex) {
                log.log(Level.SEVERE,
                         "Jasper JspRuntimeContext preload of class failed: "
                          + ex.getMessage(), ex);
            }
        }

        JspFactory.setDefaultFactory(factory);
    }

    // ----------------------------------------------------------- Constructors

    /**
     * Create a JspRuntimeContext for a web application context.
     *
     * Loads in any previously generated dependencies from file.
     *
     * @param context ServletContext for web application
     */
    public JspRuntimeContext(ServletContext context, Options options) {

        this.context = context;
        this.options = options;

        int hashSize = options.getInitialCapacity();
        jsps = new ConcurrentHashMap<String, JspServletWrapper>(hashSize);

        bytecodes = new ConcurrentHashMap<String, byte[]>(hashSize);
        bytecodeBirthTimes = new ConcurrentHashMap<String, Long>(hashSize);
        packageMap = new  ConcurrentHashMap<String, Map<String, JavaFileObject>>();

	if (log.isLoggable(Level.FINEST)) {
            ClassLoader parentClassLoader = getParentClassLoader();
	    if (parentClassLoader != null) {
		log.finest(Localizer.getMessage("jsp.message.parent_class_loader_is",
					       parentClassLoader.toString()));
	    } else {
		log.finest(Localizer.getMessage("jsp.message.parent_class_loader_is",
					       "<none>"));
	    }
        }

        initClassPath();

	if (context instanceof org.apache.jasper.servlet.JspCServletContext) {
	    return;
	}

        if (Constants.IS_SECURITY_ENABLED) {
            initSecurity();
        }

        // If this web application context is running from a
        // directory, start the background compilation thread
        String appBase = context.getRealPath("/");
        if (!options.getDevelopment()
                && appBase != null
                && options.getCheckInterval() > 0
                && !options.getUsePrecompiled()) {
            if (appBase.endsWith(File.separator) ) {
                appBase = appBase.substring(0,appBase.length()-1);
            }
            String directory =
                appBase.substring(appBase.lastIndexOf(File.separator));
            threadName = threadName + "[" + directory + "]";
            threadStart();
        }
    }

    // ----------------------------------------------------- Instance Variables

    /**
     * This web applications ServletContext
     */
    private ServletContext context;
    private Options options;
    private PermissionCollection permissionCollection;
    private CodeSource codeSource;
    private String classpath;

    /**
     * Maps JSP pages to their JspServletWrapper's
     */
    private Map <String, JspServletWrapper> jsps;

    /**
     * Maps class names to in-memory bytecodes
     */
    private Map<String, byte[]> bytecodes;
    private Map<String, Long> bytecodeBirthTimes;

    /**
     * Maps classes in packages compiled by the JSP compiler.
     * Used only by Jsr199Compiler.
     */
    private Map<String, Map<String, JavaFileObject>> packageMap;

    /**
     * The background thread.
     */
    private Thread thread = null;

    /**
     * The background thread completion semaphore.
     */
    private boolean threadDone = false;

    /**
     * Name to register for the background thread.
     */
    private String threadName = "JspRuntimeContext";

    // ------------------------------------------------------ Public Methods

    /**
     * Add a new JspServletWrapper.
     *
     * @param jspUri JSP URI
     * @param jsw Servlet wrapper for JSP
     */
    public void addWrapper(String jspUri, JspServletWrapper jsw) {
        jsps.remove(jspUri);
        jsps.put(jspUri,jsw);
    }

    /**
     * Get an already existing JspServletWrapper.
     *
     * @param jspUri JSP URI
     * @return JspServletWrapper for JSP
     */
    public JspServletWrapper getWrapper(String jspUri) {
        return jsps.get(jspUri);
    }

    /**
     * Remove a  JspServletWrapper.
     *
     * @param jspUri JSP URI of JspServletWrapper to remove
     */
    public void removeWrapper(String jspUri) {
        jsps.remove(jspUri);
    }

    /**
     * Returns the number of JSPs for which JspServletWrappers exist, i.e.,
     * the number of JSPs that have been loaded into the webapp.
     *
     * @return The number of JSPs that have been loaded into the webapp
     */
    public int getJspCount() {
        return jsps.size();
    }

    /**
     * Get the SecurityManager Policy CodeSource for this web
     * applicaiton context.
     *
     * @return CodeSource for JSP
     */
    public CodeSource getCodeSource() {
        return codeSource;
    }

    /**
     * Get the parent class loader.
     *
     * @return ClassLoader parent
     */
    public ClassLoader getParentClassLoader() {
        ClassLoader parentClassLoader =
                Thread.currentThread().getContextClassLoader();
        if (parentClassLoader == null) {
            parentClassLoader = this.getClass().getClassLoader();
        }
        return parentClassLoader;
    }

    /**
     * Get the SecurityManager PermissionCollection for this
     * web application context.
     *
     * @return PermissionCollection permissions
     */
    public PermissionCollection getPermissionCollection() {
        return permissionCollection;
    }

    /**
     * Process a "destory" event for this web application context.
     */
    public void destroy() {

        threadStop();

        for (JspServletWrapper jsw: jsps.values()) {
            jsw.destroy();
        }
    }

    /**
     * Increments the JSP reload counter.
     */
    public void incrementJspReloadCount() {
        jspReloadCount.incrementAndGet();
    }

    /**
     * Resets the JSP reload counter.
     *
     * @param count Value to which to reset the JSP reload counter
     */
    public void setJspReloadCount(int count) {
        jspReloadCount.set(count);
    }

    /**
     * Gets the current value of the JSP reload counter.
     *
     * @return The current value of the JSP reload counter
     */
    public int getJspReloadCount() {
        return jspReloadCount.get();
    }

    /**
     * Save the bytecode for the class in a map.  The current time is noted.
     * @param name The name of the class
     * @param bytecode The bytecode in byte array
     */
    public void setBytecode(String name, byte[] bytecode) {
        if (bytecode == null) {
            bytecodes.remove(name);
            bytecodeBirthTimes.remove(name);
            return;
        }
        bytecodes.put(name, bytecode);
        bytecodeBirthTimes.put(name, Long.valueOf(System.currentTimeMillis()));
    }

    public void adjustBytecodeTime(String name, long reference) {
        Long time = bytecodeBirthTimes.get(name);
        if (time == null)
            return;

        if (time.longValue() < reference) {
            bytecodeBirthTimes.put(name, Long.valueOf(reference));
        }
    }

    /**
     * Get the class-name to bytecode map
     */
    public Map<String, byte[]> getBytecodes() {
        return bytecodes;
    }

    /**
     * Retrieve the bytecode associated with the class
     */
    public byte[] getBytecode(String name) {
        return bytecodes.get(name);
    }

    /**
     * Retrieve the time the bytecode for a class was created
     */
    public long getBytecodeBirthTime(String name) {
        Long time = bytecodeBirthTimes.get(name);
        return (time != null? time.longValue(): 0);
    }

    /**
     * The packageMap keeps track of the bytecode files in a package generated
     * by a java compiler.  This is in turn loaded by the java compiler during
     * compilation.  This is gets around the fact that JSR199 API does not
     * provide a way for the compiler use current classloader.
     */
    public Map<String, Map<String, JavaFileObject>> getPackageMap() {
        return packageMap;
    }

    /**
     * Save the bytecode for a class to disk.
     */
    public void saveBytecode(String className, String classFileName) {
        byte[] bytecode = getBytecode(className);
        if (bytecode != null) {
            try {
                FileOutputStream fos = new FileOutputStream(classFileName);
                fos.write(bytecode);
                fos.close();
            } catch (IOException ex) {
                context.log("Error in saving bytecode for " + className +
                    " to " + classFileName, ex);
            }
        }
    }

    // -------------------------------------------------------- Private Methods

    /**
     * Method used by background thread to check the JSP dependencies
     * registered with this class for JSP's.
     */
    private void checkCompile() {
        for (JspServletWrapper jsw: jsps.values()) {
            if (jsw.isTagFile()) {
                // Skip tag files in background compiliations, since modified
                // tag files will be recompiled anyway when their client JSP
                // pages are compiled.  This also avoids problems when the
                // tag files and their clients are not modified simultaneously.
                continue;
            }

            JspCompilationContext ctxt = jsw.getJspEngineContext();
            // JspServletWrapper also synchronizes on this when
            // it detects it has to do a reload
            synchronized(jsw) {
                try {
                    ctxt.compile();
                } catch (FileNotFoundException ex) {
                    ctxt.incrementRemoved();
                } catch (Throwable t) {
                    jsw.getServletContext().log(
                        Localizer.getMessage("jsp.error.background.compile"),
                        t);
                }
            }
        }
    }

    /**
     * The classpath that is passed off to the Java compiler.
     */
    public String getClassPath() {
        return classpath;
    }

    /**
     * Method used to initialize classpath for compiles.
     */
    private void initClassPath() {

        /* Classpath can be specified in one of two ways, depending on
           whether the compilation is embedded or invoked from Jspc.
           1. Calculated by the web container, and passed to Jasper in the
              context attribute.
           2. Jspc directly invoke JspCompilationContext.setClassPath, in
              case the classPath initialzed here is ignored.
        */

        StringBuilder cpath = new StringBuilder();
        String sep = System.getProperty("path.separator");

	cpath.append(options.getScratchDir() + sep);

        String cp = (String) context.getAttribute(Constants.SERVLET_CLASSPATH);
        if (cp == null || cp.equals("")) {
            cp = options.getClassPath();
        }

        if (cp != null) {
            classpath = cpath.toString() + cp;
        }

        // START GlassFish Issue 845
        if (classpath != null) {
            try {
                classpath = URLDecoder.decode(classpath, "UTF-8");
            } catch (UnsupportedEncodingException e) {
                if (log.isLoggable(Level.FINE))
                    log.log(Level.FINE,
                            "Exception decoding classpath : " + classpath, e);
            }
        }
        // END GlassFish Issue 845
    }

    /**
     * Method used to initialize SecurityManager data.
     */
    private void initSecurity() {

        // Setup the PermissionCollection for this web app context
        // based on the permissions configured for the root of the
        // web app context directory, then add a file read permission
        // for that directory.
        Policy policy = Policy.getPolicy();
        if( policy != null ) {
            try {
                // Get the permissions for the web app context
                String docBase = context.getRealPath("/");
                if( docBase == null ) {
                    docBase = options.getScratchDir().toString();
                }
                String codeBase = docBase;
                if (!codeBase.endsWith(File.separator)){
                    codeBase = codeBase + File.separator;
                }
                File contextDir = new File(codeBase);
                URL url = contextDir.getCanonicalFile().toURL();
                codeSource = new CodeSource(url,(Certificate[])null);
                permissionCollection = policy.getPermissions(codeSource);

                // Create a file read permission for web app context directory
                if (!docBase.endsWith(File.separator)){
                    permissionCollection.add
                        (new FilePermission(docBase,"read"));
                    docBase = docBase + File.separator;
                } else {
                    permissionCollection.add
                        (new FilePermission
                            (docBase.substring(0,docBase.length() - 1),"read"));
                }
                docBase = docBase + "-";
                permissionCollection.add(new FilePermission(docBase,"read"));

                // Create a file read permission for web app tempdir (work)
                // directory
                String workDir = options.getScratchDir().toString();
                if (!workDir.endsWith(File.separator)){
                    permissionCollection.add
                        (new FilePermission(workDir,"read"));
                    workDir = workDir + File.separator;
                }
                workDir = workDir + "-";
                permissionCollection.add(new FilePermission(workDir,"read"));

                // Allow the JSP to access org.apache.jasper.runtime.HttpJspBase
                permissionCollection.add( new RuntimePermission(
                    "accessClassInPackage.org.apache.jasper.runtime") );

                ClassLoader parentClassLoader = getParentClassLoader();
                if (parentClassLoader instanceof URLClassLoader) {
                    URL [] urls = ((URLClassLoader)parentClassLoader).getURLs();
                    String jarUrl = null;
                    String jndiUrl = null;
                    for (int i=0; i<urls.length; i++) {
                        if (jndiUrl == null
                                && urls[i].toString().startsWith("jndi:") ) {
                            jndiUrl = urls[i].toString() + "-";
                        }
                        if (jarUrl == null
                                && urls[i].toString().startsWith("jar:jndi:")
                                ) {
                            jarUrl = urls[i].toString();
                            jarUrl = jarUrl.substring(0,jarUrl.length() - 2);
                            jarUrl = jarUrl.substring(0,
                                     jarUrl.lastIndexOf('/')) + "/-";
                        }
                    }
                    if (jarUrl != null) {
                        permissionCollection.add(
                                new FilePermission(jarUrl,"read"));
                        permissionCollection.add(
                                new FilePermission(jarUrl.substring(4),"read"));
                    }
                    if (jndiUrl != null)
                        permissionCollection.add(
                                new FilePermission(jndiUrl,"read") );
                }
            } catch(Exception e) {
                context.log("Security Init for context failed",e);
            }
        }
    }

    // -------------------------------------------------------- Thread Support

    /**
     * Start the background thread that will periodically check for
     * changes to compile time included files in a JSP.
     *
     * @exception IllegalStateException if we should not be starting
     *  a background thread now
     */
    protected void threadStart() {

        // Has the background thread already been started?
        if (thread != null) {
            return;
        }

        // Start the background thread
        threadDone = false;
        thread = new Thread(this, threadName);
        thread.setDaemon(true);
        thread.start();

    }

    /**
     * Stop the background thread that is periodically checking for
     * changes to compile time included files in a JSP.
     */
    protected void threadStop() {

        if (thread == null) {
            return;
        }

        threadDone = true;
        thread.interrupt();
        try {
            thread.join();
        } catch (InterruptedException e) {
            ;
        }

        thread = null;

    }

    /**
     * Sleep for the duration specified by the <code>checkInterval</code>
     * property.
     */
    protected void threadSleep() {

        try {
            Thread.sleep(options.getCheckInterval() * 1000L);
        } catch (InterruptedException e) {
            ;
        }

    }

    // ------------------------------------------------------ Background Thread

    /**
     * The background thread that checks for changes to files
     * included by a JSP and flags that a recompile is required.
     */
    public void run() {

        // Loop until the termination semaphore is set
        while (!threadDone) {

            // Wait for our check interval
            threadSleep();

            // Check for included files which are newer than the
            // JSP which uses them.
            try {
                checkCompile();
            } catch (Throwable t) {
                t.printStackTrace();
                log.log(Level.SEVERE, Localizer
                        .getMessage("jsp.error.recompile"), t);
            }
        }

    }

}


/*******************************************************************************
 * Copyright (c) 2015, 2016 IBH SYSTEMS GmbH.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBH SYSTEMS GmbH - initial API and implementation
 *******************************************************************************/
package org.eclipse.packagedrone.repo.channel.impl;

import static java.util.Optional.empty;
import static java.util.Optional.ofNullable;
import static org.eclipse.packagedrone.repo.utils.Splits.split;
import static org.eclipse.packagedrone.utils.Locks.lock;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.function.Function;
import java.util.stream.Collectors;

import org.eclipse.packagedrone.repo.MetaKey;
import org.eclipse.packagedrone.repo.aspect.ChannelAspectProcessor;
import org.eclipse.packagedrone.repo.channel.ChannelDetails;
import org.eclipse.packagedrone.repo.channel.ChannelId;
import org.eclipse.packagedrone.repo.channel.ChannelInformation;
import org.eclipse.packagedrone.repo.channel.ChannelNotFoundException;
import org.eclipse.packagedrone.repo.channel.ChannelService;
import org.eclipse.packagedrone.repo.channel.DeployKeysChannelAdapter;
import org.eclipse.packagedrone.repo.channel.DescriptorAdapter;
import org.eclipse.packagedrone.repo.channel.ReadableChannel;
import org.eclipse.packagedrone.repo.channel.deploy.DeployAuthService;
import org.eclipse.packagedrone.repo.channel.deploy.DeployGroup;
import org.eclipse.packagedrone.repo.channel.deploy.DeployKey;
import org.eclipse.packagedrone.repo.channel.impl.model.ChannelConfiguration;
import org.eclipse.packagedrone.repo.channel.provider.Channel;
import org.eclipse.packagedrone.repo.channel.provider.ChannelProvider;
import org.eclipse.packagedrone.repo.channel.stats.ChannelStatistics;
import org.eclipse.packagedrone.repo.trigger.ConfiguredTriggerFactoryTracker;
import org.eclipse.packagedrone.repo.trigger.ProcessorFactoryTracker;
import org.eclipse.packagedrone.storage.apm.StorageManager;
import org.eclipse.packagedrone.storage.apm.StorageRegistration;
import org.eclipse.packagedrone.utils.Locks.Locked;
import org.osgi.framework.BundleContext;
import org.osgi.framework.FrameworkUtil;
import org.osgi.service.event.EventAdmin;

import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;

public class ChannelServiceImpl implements ChannelService, DeployAuthService
{
    private static final MetaKey KEY_STORAGE = new MetaKey ( "channels", "service" );

    private final BundleContext context;

    /**
     * Used to read-lock the channel and provider map
     */
    private final Lock readLock;

    /**
     * Used to write-lock the channel and provider map
     */
    private final Lock writeLock;

    private StorageManager manager;

    private StorageRegistration handle;

    /**
     * Map channel ids to deploy groups, cache
     */
    private final Multimap<String, DeployGroup> deployKeysMap = HashMultimap.create ();

    private ChannelAspectProcessor aspectProcessor;

    private ChannelProviderTracker providerTracker;

    private final Map<String, ChannelInstance> channels = new HashMap<> ();

    private EventAdmin eventAdmin;

    private ProcessorFactoryTracker processorFactoryTracker;

    private ConfiguredTriggerFactoryTracker triggerFactoryTracker;

    public ChannelServiceImpl ()
    {
        this.context = FrameworkUtil.getBundle ( ChannelServiceImpl.class ).getBundleContext ();

        final ReentrantReadWriteLock lock = new ReentrantReadWriteLock ();

        this.readLock = lock.readLock ();
        this.writeLock = lock.writeLock ();
    }

    public void setEventAdmin ( final EventAdmin eventAdmin )
    {
        this.eventAdmin = eventAdmin;
    }

    public void setStorageManager ( final StorageManager manager )
    {
        this.manager = manager;
    }

    public void start ()
    {
        this.aspectProcessor = new ChannelAspectProcessor ( this.context );

        this.providerTracker = new ChannelProviderTracker ( this.context );
        this.providerTracker.start ();

        this.processorFactoryTracker = new ProcessorFactoryTracker ( this.context );
        this.triggerFactoryTracker = new ConfiguredTriggerFactoryTracker ( this.context );

        this.handle = this.manager.registerModel ( 1_000, KEY_STORAGE, new ChannelServiceModelProvider () );

        try
        {
            this.manager.accessRun ( KEY_STORAGE, ChannelServiceAccess.class, model -> {
                updateDeployGroupCache ( model );

                for ( final Map.Entry<String, ChannelConfiguration> entry : model.getChannels ().entrySet () )
                {
                    loadChannel ( entry.getKey (), entry.getValue () );
                }
            } );
        }
        catch ( final Exception e )
        {
            dispose ();
            throw e;
        }
    }

    public void stop ()
    {
        dispose ();
    }

    private void dispose ()
    {
        try ( Locked l = lock ( this.writeLock ) )
        {
            this.channels.values ().stream ().forEach ( ChannelInstance::dispose );
            this.channels.clear ();
        }

        if ( this.handle != null )
        {
            this.handle.unregister ();
            this.handle = null;
        }

        if ( this.aspectProcessor != null )
        {
            this.aspectProcessor.close ();
            this.aspectProcessor = null;
        }

        if ( this.providerTracker != null )
        {
            this.providerTracker.stop ();
            this.providerTracker = null;
        }

        if ( this.processorFactoryTracker != null )
        {
            this.processorFactoryTracker.close ();
            this.processorFactoryTracker = null;
        }

        if ( this.triggerFactoryTracker != null )
        {
            this.triggerFactoryTracker.close ();
            this.triggerFactoryTracker = null;
        }
    }

    private void loadChannel ( final String channelId, final ChannelConfiguration configuration )
    {
        this.channels.put ( channelId, new ChannelInstance ( channelId, configuration.getProviderId (), configuration.getConfiguration (), this.providerTracker, this.aspectProcessor, this.eventAdmin, this.manager, this.processorFactoryTracker, this.triggerFactoryTracker ) );
    }

    private ChannelInformation accessState ( final ChannelServiceAccess channels, final ChannelInstance channel )
    {
        return channel.access ( buildId ( channel.getChannelId (), channels ), ReadableChannel.class, ReadableChannel::getInformation );
    }

    @Override
    public Collection<ChannelInformation> list ()
    {
        return accessChannels ( channels -> {
            return this.channels.values ().stream ().map ( instance -> accessState ( channels, instance ) ).collect ( Collectors.toList () );
        } );
    }

    /**
     * Find by the locator
     * <p>
     * This method does not acquire the read lock, this has to be done by the
     * caller
     * </p>
     *
     * @param by
     *            the locator
     * @return the result
     */
    protected Optional<ChannelInstance> find ( final By by )
    {
        switch ( by.getType () )
        {
            case ID:
                return findById ( (String)by.getQualifier () );
            case NAME:
                return findByName ( (String)by.getQualifier () );
            case COMPOSITE:
            {
                final By[] bys = (By[])by.getQualifier ();
                for ( final By oneBy : bys )
                {
                    final Optional<ChannelInstance> result = find ( oneBy );
                    if ( result.isPresent () )
                    {
                        return result;
                    }
                }
                return Optional.empty ();
            }
            default:
                throw new IllegalArgumentException ( String.format ( "Unknown locator type: %s", by.getType () ) );
        }
    }

    private Optional<ChannelInstance> findById ( final String id )
    {
        if ( id == null )
        {
            return empty ();
        }

        return ofNullable ( this.channels.get ( id ) );
    }

    /**
     * Find a channel by name
     *
     * @param name
     *            the channel name to look for
     * @return the optional channel entry, never returns {@code null} but my
     *         return {@link Optional#empty()}.
     */
    private Optional<ChannelInstance> findByName ( final String name )
    {
        if ( name == null )
        {
            return empty ();
        }

        final String id = this.manager.accessCall ( KEY_STORAGE, ChannelServiceAccess.class, channels -> {
            return channels.mapToId ( name );
        } );

        return findById ( id );
    }

    @Override
    public Optional<ChannelInformation> getState ( final By by )
    {
        return accessChannels ( channels -> {
            return find ( by ).map ( instance -> accessState ( channels, instance ) );
        } );
    }

    @Override
    public ChannelId create ( final String providerId, final ChannelDetails details, final Map<MetaKey, String> configuration )
    {
        final String channelId = UUID.randomUUID ().toString ();

        final String actualProviderId;
        if ( providerId != null )
        {
            actualProviderId = providerId;
        }
        else
        {
            actualProviderId = "apm";
        }

        final ChannelConfiguration cfg = new ChannelConfiguration ();
        cfg.setProviderId ( actualProviderId );
        cfg.setConfiguration ( configuration );
        cfg.setDescription ( details.getDescription () );

        this.providerTracker.run ( actualProviderId, p -> {
            final ChannelProvider provider = p.orElseThrow ( () -> new IllegalStateException ( String.format ( "Channel provider '%s' is not registered", actualProviderId ) ) );
            provider.create ( channelId, configuration );
        } );

        this.manager.accessRun ( KEY_STORAGE, ChannelServiceModify.class, channels -> {
            channels.createChannel ( channelId, cfg );
        } );

        // FIXME: ensure that we are the only active call to the storage manager model KEY_STORAGE
        commitCreateChannel ( channelId, actualProviderId, configuration );

        return new ChannelId ( channelId );
    }

    private void commitCreateChannel ( final String channelId, final String providerId, final Map<MetaKey, String> configuration )
    {
        final ChannelInstance channel = new ChannelInstance ( channelId, providerId, configuration, this.providerTracker, this.aspectProcessor, this.eventAdmin, this.manager, this.processorFactoryTracker, this.triggerFactoryTracker );

        try ( Locked l = lock ( this.writeLock ) )
        {
            this.channels.put ( channelId, channel );
        }
    }

    private <R> R accessChannels ( final Function<ChannelServiceAccess, R> operation )
    {
        return this.manager.accessCall ( KEY_STORAGE, ChannelServiceAccess.class, channels -> {
            return operation.apply ( channels );
        } );
    }

    private <R> R modifyChannels ( final Function<ChannelServiceModify, R> operation )
    {
        return this.manager.modifyCall ( KEY_STORAGE, ChannelServiceModify.class, channels -> {
            return operation.apply ( channels );
        } );
    }

    private <R, T> R modifyChannel ( final By by, final Function<ChannelInstance, R> operation )
    {
        return modifyChannels ( channels -> {
            final ChannelInstance channelInstance = findChannel ( by );
            return operation.apply ( channelInstance );
        } );
    }

    @SuppressWarnings ( "unchecked" )
    @Override
    public <R, T> R accessCall ( final By by, final Class<T> clazz, final ChannelOperation<R, T> operation )
    {
        if ( DeployKeysChannelAdapter.class.equals ( clazz ) )
        {
            return modifyChannel ( by, channelInstance -> handleDeployKeys ( channelInstance, (ChannelOperation<R, DeployKeysChannelAdapter>)operation ) );
        }
        else if ( DescriptorAdapter.class.equals ( clazz ) )
        {
            return modifyChannel ( by, channelInstance -> handleDescribe ( channelInstance, (ChannelOperation<R, DescriptorAdapter>)operation ) );
        }

        return accessChannels ( channels -> {
            final ChannelInstance channelInstance = findChannel ( by );
            final ChannelId id = buildId ( channelInstance.getChannelId (), channels );
            return channelInstance.access ( id, clazz, operation );
        } );
    }

    private static ChannelId buildId ( final String channelId, final ChannelServiceAccess channels )
    {
        final Set<String> names = new LinkedHashSet<> ( channels.getNameMappings ( channelId ) );
        final String description = channels.getDescription ( channelId );
        return new ChannelId ( channelId, names, description );
    }

    private <R> R handleDeployKeys ( final ChannelInstance channel, final ChannelOperation<R, DeployKeysChannelAdapter> operation )
    {
        try ( Locked l = lock ( this.writeLock ) )
        {
            return this.manager.modifyCall ( KEY_STORAGE, ChannelServiceModify.class, model -> {

                final DeployKeysChannelAdapterImpl adapter = new DeployKeysChannelAdapterImpl ( channel.getChannelId (), model ) {

                    @Override
                    public void assignDeployGroup ( final String groupId )
                    {
                        super.assignDeployGroup ( groupId );
                        StorageManager.executeAfterPersist ( () -> {
                            updateDeployGroupCache ( this.model );
                        } );
                    }

                    @Override
                    public void unassignDeployGroup ( final String groupId )
                    {
                        super.unassignDeployGroup ( groupId );
                        StorageManager.executeAfterPersist ( () -> {
                            updateDeployGroupCache ( this.model );
                        } );
                    }

                };
                return runDisposing ( operation, DeployKeysChannelAdapter.class, adapter );
            } );
        }
    }

    private static <R, T> R runDisposing ( final ChannelOperation<R, T> operation, final Class<T> clazz, final T target )
    {
        try ( Disposing<T> adapter = Disposing.proxy ( clazz, target ) )
        {
            return operation.process ( adapter.getTarget () );
        }
        catch ( final Exception e )
        {
            throw new RuntimeException ( e );
        }
    }

    @Override
    public Optional<Collection<DeployGroup>> getChannelDeployGroups ( final By by )
    {
        try ( Locked l = lock ( this.readLock ) )
        {
            final Optional<ChannelInstance> channel = find ( by );

            if ( !channel.isPresent () )
            {
                return Optional.empty ();
            }

            return Optional.ofNullable ( this.deployKeysMap.get ( channel.get ().getChannelId () ) ).map ( ArrayList<DeployGroup>::new );
        }
    }

    private <R> R handleDescribe ( final ChannelInstance channelEntry, final ChannelOperation<R, DescriptorAdapter> operation )
    {
        return this.manager.modifyCall ( KEY_STORAGE, ChannelServiceModify.class, model -> {
            final DescriptorAdapter dai = new DescriptorAdapterImpl ( channelEntry.getChannelId (), model );
            return runDisposing ( operation, DescriptorAdapter.class, dai );
        } );
    }

    /**
     * Find a channel
     *
     * @param by
     *            the search criteria
     * @return the found channel, never returns {@code null}
     * @throws ChannelNotFoundException
     *             if there was no channel found
     */
    private ChannelInstance findChannel ( final By by )
    {
        final Optional<ChannelInstance> channel;
        try ( Locked l = lock ( this.readLock ) )
        {
            channel = find ( by );
        }

        if ( !channel.isPresent () )
        {
            throw new ChannelNotFoundException ( by.toString () );
        }

        return channel.get ();
    }

    @Override
    public boolean delete ( final By by )
    {
        try ( Locked l = lock ( this.writeLock ) )
        {
            final Optional<ChannelInstance> channelInstance = find ( by );
            if ( !channelInstance.isPresent () )
            {
                return false;
            }

            final ChannelInstance instance = channelInstance.get ();

            final Optional<Channel> channel = instance.getChannel ();
            if ( !channel.isPresent () )
            {
                throw new IllegalStateException ( String.format ( "Can only delete channel %s when the provider %s is present", instance.getChannelId (), instance.getProviderId () ) );
            }

            deleteChannel ( instance );

            channel.get ().delete ();

            return true;
        }
    }

    protected void deleteChannel ( final ChannelInstance channel )
    {
        this.manager.modifyRun ( KEY_STORAGE, ChannelServiceModify.class, model -> {

            model.deleteChannel ( channel.getChannelId () );

            StorageManager.executeAfterPersist ( () -> commitDeleteChannel ( channel ) );
        } );
    }

    @Override
    public void wipeClean ()
    {
        // get the current list of channels

        List<ChannelInstance> channels;
        try ( Locked l = lock ( this.writeLock ) )
        {
            channels = new ArrayList<> ( this.channels.values () );
        }

        // delete one by one

        for ( final ChannelInstance instance : channels )
        {
            final Optional<Channel> channel = instance.getChannel ();
            if ( channel.isPresent () )
            {
                // ... only if the provider is present

                deleteChannel ( instance );
                this.channels.remove ( instance.getChannelId () );
                channel.get ().delete ();
            }
        }
    }

    private void commitDeleteChannel ( final ChannelInstance channel )
    {
        // remove the channel from the map
        this.channels.remove ( channel.getChannelId () );

        // remove deploy groups for channel
        this.deployKeysMap.removeAll ( channel.getChannelId () );
    }

    /**
     * Update the channel to deploy group cache map
     *
     * @param model
     *            the model to fill the cache from
     */
    private void updateDeployGroupCache ( final ChannelServiceAccess model )
    {
        // this will simply rebuild the complete map

        // clear first
        this.deployKeysMap.clear ();

        // fill afterwards
        for ( final Map.Entry<String, Set<String>> entry : model.getDeployGroupMap ().entrySet () )
        {
            final String channelId = entry.getKey ();
            final List<DeployGroup> groups = entry.getValue ().stream ().map ( groupId -> model.getDeployGroup ( groupId ) ).collect ( Collectors.toList () );
            this.deployKeysMap.putAll ( channelId, groups );
        }
    }

    // methods of DeployAuthService

    @Override
    public List<DeployGroup> listGroups ( final int position, final int count )
    {
        return this.manager.accessCall ( KEY_STORAGE, ChannelServiceAccess.class, model -> {
            return split ( model.getDeployGroups (), position, count );
        } );
    }

    @Override
    public DeployGroup createGroup ( final String name )
    {
        return this.manager.modifyCall ( KEY_STORAGE, ChannelServiceModify.class, model -> {
            return model.createGroup ( name );
        } );
    }

    @Override
    public void deleteGroup ( final String groupId )
    {
        try ( final Locked l = lock ( this.writeLock ) )
        {
            this.manager.modifyRun ( KEY_STORAGE, ChannelServiceModify.class, model -> {
                model.deleteGroup ( groupId );
                StorageManager.executeAfterPersist ( () -> {
                    updateDeployGroupCache ( model );
                } );
            } );
        }
    }

    @Override
    public void updateGroup ( final String groupId, final String name )
    {
        try ( final Locked l = lock ( this.writeLock ) )
        {
            this.manager.modifyRun ( KEY_STORAGE, ChannelServiceModify.class, model -> {
                model.updateGroup ( groupId, name );
                StorageManager.executeAfterPersist ( () -> {
                    updateDeployGroupCache ( model );
                } );
            } );
        }
    }

    @Override
    public DeployGroup getGroup ( final String groupId )
    {
        return this.manager.accessCall ( KEY_STORAGE, ChannelServiceAccess.class, model -> {
            return model.getDeployGroup ( groupId );
        } );
    }

    @Override
    public DeployKey createDeployKey ( final String groupId, final String name )
    {
        return this.manager.modifyCall ( KEY_STORAGE, ChannelServiceModify.class, model -> {
            StorageManager.executeAfterPersist ( () -> {
                updateDeployGroupCache ( model );
            } );
            return model.createKey ( groupId, name );
        } );
    }

    @Override
    public DeployKey deleteDeployKey ( final String keyId )
    {
        return this.manager.modifyCall ( KEY_STORAGE, ChannelServiceModify.class, model -> {
            StorageManager.executeAfterPersist ( () -> {
                updateDeployGroupCache ( model );
            } );
            return model.deleteKey ( keyId );
        } );
    }

    @Override
    public DeployKey getDeployKey ( final String keyId )
    {
        return this.manager.accessCall ( KEY_STORAGE, ChannelServiceAccess.class, model -> {
            return model.getDeployKey ( keyId );
        } );
    }

    @Override
    public DeployKey updateDeployKey ( final String keyId, final String name )
    {
        return this.manager.modifyCall ( KEY_STORAGE, ChannelServiceModify.class, model -> {
            StorageManager.executeAfterPersist ( () -> {
                updateDeployGroupCache ( model );
            } );
            return model.updateKey ( keyId, name );
        } );
    }

    // statistics

    @Override
    public ChannelStatistics getStatistics ()
    {
        final ChannelStatistics cs = new ChannelStatistics ();

        try ( Locked l = lock ( this.readLock ) )
        {
            final Collection<ChannelInformation> cis = list ();

            cs.setTotalNumberOfArtifacts ( cis.stream ().mapToLong ( ci -> ci.getState ().getNumberOfArtifacts () ).sum () );
            cs.setTotalNumberOfBytes ( cis.stream ().mapToLong ( ci -> ci.getState ().getNumberOfBytes () ).sum () );
        }

        return cs;
    }
}


/*******************************************************************************
 * Copyright (c) 2016 IBH SYSTEMS GmbH and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBH SYSTEMS GmbH - initial API and implementation
 *******************************************************************************/
package org.eclipse.packagedrone.utils.io;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Writer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.util.Objects;

/**
 * Helper class working with {@link InputStream}s and {@link OutputStream}s
 */
public final class Streams
{
    private static final int COPY_BUFFER_SIZE = 8 * 1024;

    private static final InputStream CLOSED_INPUT = new InputStream () {

        @Override
        public int read () throws IOException
        {
            return -1;
        }
    };

    private static final OutputStream CLOSED_OUTPUT = new OutputStream () {

        @Override
        public void write ( final int b ) throws IOException
        {
            throw new IOException ( "Failed to write: stream is closed" );
        }
    };

    private Streams ()
    {
    }

    /**
     * Copy the remaining content of one stream to the other
     *
     * @param in
     *            the input stream
     * @param out
     *            the output stream
     * @return the number of bytes copied
     * @throws IOException
     *             if any I/O error occurs
     */
    public static long copy ( final InputStream in, final OutputStream out ) throws IOException
    {
        Objects.requireNonNull ( in );
        Objects.requireNonNull ( out );

        final byte[] buffer = new byte[COPY_BUFFER_SIZE];
        long result = 0;

        int rc;
        while ( ( rc = in.read ( buffer ) ) >= 0 )
        {
            result += rc;
            out.write ( buffer, 0, rc );
        }

        return result;
    }

    /**
     * Copy the remaining content of one reader to the {@link Appendable} (or
     * {@link Writer}
     *
     * @param readable
     *            the input readable
     * @param appendable
     *            the output appendable
     * @return the number of characters copied
     * @throws IOException
     *             if any I/O error occurs
     */
    public static long copy ( final Readable readable, final Appendable appendable ) throws IOException
    {
        final CharBuffer buffer = CharBuffer.allocate ( COPY_BUFFER_SIZE );
        long total = 0;
        while ( readable.read ( buffer ) >= 0 )
        {
            buffer.flip ();
            appendable.append ( buffer );
            total += buffer.remaining ();
            buffer.clear ();
        }
        return total;
    }

    public static String toString ( final Readable readable ) throws IOException
    {
        final StringBuilder builder = new StringBuilder ();
        copy ( readable, builder );
        return builder.toString ();
    }

    public static String toString ( final InputStream stream, final Charset charset ) throws IOException
    {
        return toString ( new InputStreamReader ( stream, charset ) );
    }

    public static InputStream closedInput ()
    {
        return CLOSED_INPUT;
    }

    public static OutputStream closedOutput ()
    {
        return CLOSED_OUTPUT;
    }
}