/*******************************************************************************
 * Copyright (c) 2015 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
 *     Peter Jeschke/Gemtec GmbH - correction of incorrect exception messages
 *******************************************************************************/
package org.eclipse.packagedrone.storage.apm;

import java.io.IOException;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Deque;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;

import org.eclipse.packagedrone.repo.MetaKey;

public class StorageManager
{
    private static final ThreadLocal<Deque<State>> lockStates = ThreadLocal.withInitial ( LinkedList::new );

    private long counter;

    private final ReadWriteLock modelLock = new ReentrantReadWriteLock ( false );

    private final Map<Long, Entry> modelIdMap = new HashMap<> ();

    private final Map<MetaKey, Entry> modelKeyMap = new HashMap<> ();

    private final StorageContext context;

    private boolean closed;

    private static enum LockType
    {
        READ,
        WRITE;
    }

    private static class State
    {
        /**
         * The parent state, may be null
         */
        private final State parent;

        /**
         * This will hold the first parent (nearest to the root) with the same
         * key, may be null
         */
        private final State sameKeyParent;

        private final long priority;

        private final MetaKey key;

        private final State highestState;

        private final LockType type;

        private Object writeModel;

        private List<Runnable> afterTasks;

        public State ( final State parent, final LockType type, final long priority, final MetaKey key )
        {
            this.parent = parent;
            this.priority = priority;
            this.key = key;
            this.type = type;

            final State sameParent = getSameParent ( parent, key );
            if ( sameParent != null && sameParent.sameKeyParent != null )
            {
                this.sameKeyParent = sameParent.sameKeyParent;
            }
            else
            {
                this.sameKeyParent = sameParent;
            }

            if ( parent == null )
            {
                this.highestState = this;
            }
            else
            {
                this.highestState = compareTwo ( priority, key, parent.priority, parent.key ) > 0 ? this : parent;
            }
        }

        public void setWriteModel ( final Object writeModel )
        {
            this.writeModel = writeModel;
        }

        public Object getWriteModel ()
        {
            return this.writeModel;
        }

        public static int compareTo ( final State state1, final State state2 )
        {
            return compareTwo ( state1.priority, state1.key, state2.priority, state2.key );
        }

        private static int compareTwo ( final long priority1, final MetaKey key1, final long priority2, final MetaKey key2 )
        {
            final int rc = Long.compare ( priority1, priority2 );
            if ( rc != 0 )
            {
                return rc;
            }

            return key1.compareTo ( key2 );
        }

        public State getHighestState ()
        {
            return this.highestState;
        }

        @Override
        public String toString ()
        {
            return String.format ( "%s -> %s|%s", this.parent != null ? this.parent : "ROOT", this.priority, this.key );
        }

        public boolean isLocked ( final MetaKey key )
        {
            return getSameParent ( this, key ) != null;
        }

        public boolean isReadLocked ( final MetaKey key )
        {
            State current = this;

            while ( current != null )
            {
                if ( current.key.equals ( key ) && current.type == LockType.READ )
                {
                    return true;
                }

                current = current.parent;
            }
            return false;
        }

        public void addAfterTask ( final Runnable runnable )
        {
            if ( this.afterTasks == null )
            {
                this.afterTasks = new LinkedList<> ();
            }

            this.afterTasks.add ( runnable );
        }

        public void runAfterTasks ()
        {
            if ( this.afterTasks == null )
            {
                return;
            }

            LinkedList<Exception> errors = null;
            for ( final Runnable runnable : this.afterTasks )
            {
                try
                {
                    runnable.run ();
                }
                catch ( final Exception e )
                {
                    if ( errors == null )
                    {
                        errors = new LinkedList<> ();
                    }
                    errors.add ( e );
                }
            }

            this.afterTasks.clear ();

            handleErrors ( "Failed to run 'after' tasks", errors );
        }
    }

    private static class StateWrapper
    {
        private final Deque<State> stack;

        private final State state;

        public StateWrapper ( final Entry entry, final LockType type )
        {
            this.stack = lockStates.get ();

            final State current = this.stack.peekLast ();
            final State next = new State ( current, type, entry.lockPriority, entry.key );

            if ( current != null )
            {
                if ( type == LockType.WRITE && current.isReadLocked ( entry.key ) )
                {
                    // lock upgrade is not allowed -> fail
                    throw new IllegalStateException ( String.format ( "%s is already read locked. Upgrading read locks to write locks is not supported! (State: %s)", entry.key, current ) );
                }

                if ( !current.isLocked ( entry.key ) )
                {
                    if ( State.compareTo ( current.getHighestState (), next ) >= 0 )
                    {
                        // last is higher -> fail
                        throw new IllegalStateException ( String.format ( "Lock is lower or equal in priority than previous lock: %s -> %s", current, next ) );
                    }
                }
            }

            this.stack.addLast ( next );

            this.state = next;
        }

        public void dispose ()
        {
            this.stack.removeLast ();
        }
    }

    /**
     * Traverse up from the provided parent and find the first which uses the
     * same key
     *
     * @param parent
     *            the first parent candidate
     * @param key
     *            the key to look for
     * @return the first parent (which may be the input parameter), which uses
     *         the same key
     */
    private static State getSameParent ( final State parent, final MetaKey key )
    {
        State current = parent;
        while ( current != null )
        {
            if ( current.key.equals ( key ) )
            {
                return current;
            }

            current = current.parent;
        }
        return null;
    }

    private static class Entry
    {
        @SuppressWarnings ( "unused" )
        long id;

        long lockPriority;

        MetaKey key;

        StorageModelProvider<?, ?> storageProvider;

        ReadWriteLock lock = new ReentrantReadWriteLock ( false );

        public Entry ( final long id, final long lockPriority, final MetaKey key, final StorageModelProvider<?, ?> storageProvider )
        {
            this.id = id;
            this.lockPriority = lockPriority;
            this.key = key;
            this.storageProvider = storageProvider;
        }
    }

    public StorageManager ( final Path basePath )
    {
        validateBasePath ( basePath );

        this.context = new StorageContext () {

            @Override
            public Path getBasePath ()
            {
                return basePath;
            }
        };
    }

    private void validateBasePath ( final Path basePath )
    {
        if ( Files.exists ( basePath ) )
        {
            if ( !Files.isDirectory ( basePath ) )
            {
                throw new IllegalStateException ( String.format ( "Base path '%s' already exists but is not a directory", basePath.toString () ) );
            }
            if ( !Files.isWritable ( basePath ) )
            {
                throw new IllegalStateException ( String.format ( "Base path '%s' already exists but is not writable", basePath.toString () ) );
            }
        }
        else
        {
            try
            {
                Files.createDirectories ( basePath );
            }
            catch ( final FileAlreadyExistsException e )
            {
                // silently ignore
            }
            catch ( final IOException e )
            {
                throw new IllegalStateException ( "Failed to create base path", e );
            }
        }
    }

    /**
     * Register a new model with the storage manager
     *
     * @param lockPriority
     *            the priority of the lock, lower numbers must be locked before
     *            higher numbers
     * @param key
     *            the model key
     * @param storageProvider
     *            the storage provider
     * @return a handle for unregistering the model
     * @throws ModelInitializationException
     *             if the {@link StorageModelProvider#start(StorageContext)}
     *             method fails
     */
    public StorageRegistration registerModel ( final long lockPriority, final MetaKey key, final StorageModelProvider<?, ?> storageProvider ) throws ModelInitializationException
    {
        this.modelLock.writeLock ().lock ();
        try
        {
            testClosed ();

            if ( this.modelKeyMap.containsKey ( key ) )
            {
                throw new IllegalArgumentException ( String.format ( "A provider for '%s' is already registered", key ) );
            }

            try
            {
                storageProvider.start ( this.context );
            }
            catch ( final Exception e )
            {
                throw new ModelInitializationException ( "Failed to start model provider: " + key, e );
            }

            final long id = this.counter++;

            final Entry entry = new Entry ( id, lockPriority, key, storageProvider );

            this.modelIdMap.put ( id, entry );
            this.modelKeyMap.put ( key, entry );

            return new StorageRegistration () {

                @Override
                public void unregister ()
                {
                    unregisterModel ( id );
                }
            };
        }
        finally
        {
            this.modelLock.writeLock ().unlock ();
        }
    }

    protected void unregisterModel ( final long id )
    {
        final Entry entry;

        this.modelLock.writeLock ().lock ();
        try
        {
            if ( this.closed )
            {
                // ignore when closing
                return;
            }

            entry = this.modelIdMap.remove ( id );
            if ( entry != null )
            {
                this.modelKeyMap.remove ( entry.key );
            }
        }
        finally
        {
            this.modelLock.writeLock ().unlock ();
        }

        if ( entry == null )
        {
            // nothing to do
            return;
        }

        // get the write lock so we don't stop the model while it is active

        entry.lock.writeLock ().lock ();
        try
        {
            entry.storageProvider.stop ();
        }
        finally
        {
            entry.lock.writeLock ().unlock ();
        }
    }

    public <T, M> T accessCall ( final MetaKey modelKey, final Class<M> modelClazz, final Function<M, T> function )
    {
        return doWithModel ( LockType.READ, modelKey, entry -> entry.lock.readLock (), ( entry, state ) -> {
            return performAccessOperation ( entry, state, modelClazz, modelKey, function );
        } );

    }

    public <T, M> T modifyCall ( final MetaKey modelKey, final Class<M> modelClazz, final Function<M, T> function )
    {
        return doWithModel ( LockType.WRITE, modelKey, entry -> entry.lock.writeLock (), ( entry, state ) -> {
            return performModifyOperation ( entry, modelClazz, function );
        } );
    }

    private <T, M> T performAccessOperation ( final Entry entry, final State state, final Class<M> modelClazz, final MetaKey modelKey, final Function<M, T> function )
    {
        final State same = state.sameKeyParent;

        final Object viewModel;
        if ( same != null && same.writeModel != null )
        {
            viewModel = entry.storageProvider.makeViewModel ( same.writeModel );
        }
        else
        {
            viewModel = entry.storageProvider.getViewModel ();
        }

        if ( viewModel == null || modelClazz.isAssignableFrom ( viewModel.getClass () ) )
        {
            return function.apply ( modelClazz.cast ( viewModel ) );
        }
        else
        {
            throw new IllegalStateException ( String.format ( "View model of '%s' is not of type '%s'", modelKey, modelClazz.getName () ) );
        }
    }

    private <T, M> T performModifyOperation ( final Entry entry, final Class<M> modelClazz, final Function<M, T> function )
    {
        @SuppressWarnings ( "unchecked" )
        final StorageModelProvider<?, M> sp = (StorageModelProvider<?, M>)entry.storageProvider;

        final State current = getCurrent ();

        // try to get same from parent of current
        final State same = current.sameKeyParent;

        final M writeModel;

        if ( same == null )
        {
            writeModel = cast ( entry, modelClazz, sp.cloneWriteModel () );
        }
        else
        {
            writeModel = cast ( entry, modelClazz, same.getWriteModel () );
        }
        current.setWriteModel ( writeModel );

        try
        {
            // call user code

            final T result = function.apply ( writeModel );

            if ( same == null )
            {
                // we are the one that cloned the model
                try
                {
                    // we received the same model type from cloneWriteModel ()
                    sp.persistWriteModel ( writeModel );
                }
                catch ( final Exception e )
                {
                    throw new RuntimeException ( String.format ( "Failed to persist model of %s", entry.key ), e );
                }

                current.runAfterTasks ();
            }

            // otherwise: -> save later

            return result;
        }
        finally
        {
            if ( same == null )
            {
                // close write model
                sp.closeWriteModel ( writeModel );
            }
        }
    }

    private static <T> T cast ( final Entry entry, final Class<T> clazz, final Object o )
    {
        if ( o == null || clazz.isAssignableFrom ( o.getClass () ) )
        {
            return clazz.cast ( o );
        }

        throw new IllegalStateException ( String.format ( "Model of '%s' is not of type '%s'", entry.key, clazz.getName () ) );
    }

    protected <T> T doWithModel ( final LockType type, final MetaKey modelKey, final Function<Entry, Lock> lockSupplier, final BiFunction<Entry, State, T> function )
    {
        final Lock modelWithModel = this.modelLock.readLock ();
        final Lock lock;

        final Entry entry;

        modelWithModel.lock ();

        final StateWrapper wrapper;

        try
        {
            testClosed ();

            entry = this.modelKeyMap.get ( modelKey );

            if ( entry == null )
            {
                // the model lock will be releases in the finally
                throw new IllegalArgumentException ( String.format ( "Model '%s' could not be found", modelKey ) );
            }

            lock = lockSupplier.apply ( entry );

            wrapper = new StateWrapper ( entry, type );

            lock.lock ();
        }
        finally
        {
            this.modelLock.readLock ().unlock ();
        }

        // we do have the entry lock and released the model lock

        try
        {
            try
            {
                return function.apply ( entry, wrapper.state );
            }
            finally
            {
                wrapper.dispose ();
            }
        }
        finally
        {
            lock.unlock ();
        }
    }

    protected static State getCurrent ()
    {
        final Deque<State> stack = lockStates.get ();
        return stack.peekLast ();
    }

    public StorageContext getContext ()
    {
        testClosed ();

        return this.context;
    }

    public <M> void accessRun ( final MetaKey modelKey, final Class<M> modelClazz, final Consumer<M> consumer )
    {
        accessCall ( modelKey, modelClazz, ( model ) -> {
            consumer.accept ( model );
            return null;
        } );
    }

    public <M> void modifyRun ( final MetaKey modelKey, final Class<M> modelClazz, final Consumer<M> consumer )
    {
        modifyCall ( modelKey, modelClazz, ( model ) -> {
            consumer.accept ( model );
            return null;
        } );
    }

    /**
     * Execute a task after the current state has been persisted
     *

     * If the current state will not persist the model (because a parent opened
     * the model before and is required to perform persisting the model), then
     * the task will be executed at the time the parent state causes persisting
     * the model.
     *

     *

     * If there is no open state or the parent state is read-lock, the runnable
     * will be executed immediately.
     *

     *
     * @param runnable
     *            the runnable to execute
     */
    public static void executeAfterPersist ( final Runnable runnable )
    {
        final State state = getCurrent ();
        if ( state == null )
        {
            runnable.run ();
            return;
        }

        final MetaKey key = state.key;
        if ( key == null )
        {
            runnable.run ();
            return;
        }

        final State parent = state.sameKeyParent == null ? state : state.sameKeyParent;

        if ( parent.type == LockType.READ )
        {
            runnable.run ();
            return;
        }

        parent.addAfterTask ( runnable );
    }

    public void close ()
    {
        List<Entry> providers;
        this.modelLock.writeLock ().lock ();
        try
        {
            if ( this.closed )
            {
                return;
            }

            providers = new ArrayList<> ( this.modelIdMap.values () );

            this.closed = true;

            this.modelIdMap.clear ();
            this.modelKeyMap.clear ();
        }
        finally
        {
            this.modelLock.writeLock ().unlock ();
        }

        // we closed it, so close all storages

        final LinkedList<Exception> allErrors = new LinkedList<> ();

        for ( final Entry entry : providers )
        {
            try
            {
                // nobody else can hold a lock here, we had been in write lock before
                // and cleared the maps
                entry.storageProvider.stop ();
            }
            catch ( final Exception e )
            {
                allErrors.add ( e );
            }
        }

        // handle errors

        handleErrors ( "Failed to close provider", allErrors );
    }

    private static void handleErrors ( final String message, final LinkedList<Exception> allErrors )
    {
        if ( allErrors == null )
        {
            return;
        }

        if ( !allErrors.isEmpty () )
        {
            final RuntimeException e = new RuntimeException ( message, allErrors.poll () );

            // add remaining
            allErrors.forEach ( e::addSuppressed );

            throw e;
        }
    }

    protected void testClosed ()
    {
        if ( this.closed )
        {
            throw new IllegalStateException ( "Storage is already closed" );
        }
    }

}


/*******************************************************************************
 * Copyright (c) 2015 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.apm.store;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;

import org.eclipse.packagedrone.repo.MetaKey;
import org.eclipse.packagedrone.utils.io.IOConsumer;
import org.eclipse.scada.utils.io.RecursiveDeleteVisitor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class CacheStore implements AutoCloseable
{
    private final static Logger logger = LoggerFactory.getLogger ( CacheStore.TransactionImpl.class );

    public class TransactionImpl implements Transaction
    {
        private Path tmp;

        public TransactionImpl ()
        {
        }

        @Override
        public long put ( final MetaKey key, final IOConsumer<OutputStream> source ) throws IOException
        {
            makeTemp ();

            final Path path = makePath ( this.tmp, key );
            Files.createDirectories ( path.getParent () );

            try ( OutputStream stream = new BufferedOutputStream ( Files.newOutputStream ( path ) ) )
            {
                source.accept ( stream );
            }

            return Files.size ( path );
        }

        @Override
        public void clear () throws IOException
        {
            synchronized ( CacheStore.this )
            {
                // if we have a "next" dir
                if ( this.tmp != null )
                {
                    // delete it
                    Files.walkFileTree ( this.tmp, new RecursiveDeleteVisitor () );
                }

                // and ensure we always have one
                makeTemp ();
            }
        }

        private void makeTemp () throws IOException
        {
            if ( this.tmp == null )
            {
                this.tmp = Files.createTempDirectory ( CacheStore.this.tmpPath, "trans" );
            }
        }

        @Override
        public boolean stream ( final MetaKey key, final IOConsumer<InputStream> consumer ) throws IOException
        {
            if ( streamFrom ( this.tmp, key, consumer ) )
            {
                return true;
            }

            return streamFrom ( CacheStore.this.dataPath, key, consumer );
        }

        @Override
        public void commit ()
        {
            synchronized ( CacheStore.this )
            {
                markFinished ( this );

                if ( this.tmp == null )
                {
                    // no change
                    return;
                }

                try
                {
                    // first delete the swap dir
                    if ( Files.exists ( CacheStore.this.swapPath ) )
                    {
                        Files.walkFileTree ( CacheStore.this.swapPath, new RecursiveDeleteVisitor () );
                    }

                    // move away old data path
                    if ( Files.exists ( CacheStore.this.dataPath ) )
                    {
                        Files.createDirectories ( CacheStore.this.swapPath.getParent () );
                        Files.move ( CacheStore.this.dataPath, CacheStore.this.swapPath, StandardCopyOption.ATOMIC_MOVE );
                    }

                    Files.move ( this.tmp, CacheStore.this.dataPath, StandardCopyOption.ATOMIC_MOVE );
                }
                catch ( final IOException e )
                {
                    throw new RuntimeException ( e );
                }

                if ( Files.exists ( CacheStore.this.swapPath ) )
                {
                    try
                    {
                        Files.walkFileTree ( CacheStore.this.swapPath, new RecursiveDeleteVisitor () );
                    }
                    catch ( final IOException e )
                    {
                        // we can ignore this one
                    }
                }
            }
        }

        @Override
        public void rollback ()
        {
            synchronized ( CacheStore.this )
            {
                markFinished ( this );

                if ( this.tmp == null )
                {
                    return;
                }

                try
                {
                    Files.walkFileTree ( this.tmp, new RecursiveDeleteVisitor () );
                }
                catch ( final IOException e )
                {
                    throw new RuntimeException ( "Failed to roll back", e );
                }
            }
        }
    }

    public interface Transaction
    {
        public long put ( MetaKey key, IOConsumer<OutputStream> data ) throws IOException;

        public boolean stream ( final MetaKey key, final IOConsumer<InputStream> consumer ) throws IOException;

        public void clear () throws IOException;

        public void commit ();

        public void rollback ();
    }

    private Transaction transaction;

    private final Path dataPath;

    private final Path tmpPath;

    private final Path swapPath;

    public CacheStore ( final Path path ) throws IOException
    {
        this.dataPath = path.resolve ( "data" );
        this.tmpPath = path.resolve ( "tmp" );
        this.swapPath = path.resolve ( "swap" );

        Files.createDirectories ( this.dataPath );
        Files.createDirectories ( this.tmpPath );
    }

    protected void markFinished ( final TransactionImpl transaction )
    {
        if ( transaction != this.transaction )
        {
            throw new IllegalStateException ( "Wrong transaction tries to finish" );
        }
        this.transaction = null;
    }

    @Override
    public synchronized void close ()
    {
        if ( this.transaction != null )
        {
            this.transaction.rollback ();
            this.transaction = null;
        }
    }

    public synchronized Transaction startTransaction ()
    {
        if ( this.transaction != null )
        {
            throw new IllegalStateException ( "Another transaction is already in progress" );
        }

        this.transaction = new TransactionImpl ();
        return this.transaction;
    }

    /**
     * Stream directly from the storage
     */
    public boolean stream ( final MetaKey key, final IOConsumer<InputStream> consumer ) throws IOException
    {
        return streamFrom ( this.dataPath, key, consumer );
    }

    private boolean streamFrom ( final Path basePath, final MetaKey key, final IOConsumer<InputStream> consumer ) throws IOException
    {
        final Path path = makePath ( basePath, key );

        logger.trace ( "streamFrom - key: %s, path: %s", key, path );

        try ( InputStream stream = new BufferedInputStream ( Files.newInputStream ( path ) ) )
        {
            consumer.accept ( stream );
            return true;
        }
        catch ( final NoSuchFileException e )
        {
            return false;
        }
    }

    private static Path makePath ( final Path basePath, final MetaKey key )
    {
        return basePath.resolve ( key.getNamespace () ).resolve ( key.getKey () );
    }

}


/*******************************************************************************
 * Copyright (c) 2015 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.job.web.internal;

import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.annotation.HttpConstraint;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.eclipse.packagedrone.job.JobFactoryDescriptor;
import org.eclipse.packagedrone.job.JobHandle;
import org.eclipse.packagedrone.job.JobManager;
import org.eclipse.packagedrone.sec.web.controller.HttpContraintControllerInterceptor;
import org.eclipse.packagedrone.sec.web.controller.Secured;
import org.eclipse.packagedrone.sec.web.controller.SecuredControllerInterceptor;
import org.eclipse.packagedrone.web.Controller;
import org.eclipse.packagedrone.web.LinkTarget;
import org.eclipse.packagedrone.web.ModelAndView;
import org.eclipse.packagedrone.web.RequestMapping;
import org.eclipse.packagedrone.web.RequestMethod;
import org.eclipse.packagedrone.web.ViewResolver;
import org.eclipse.packagedrone.web.common.CommonController;
import org.eclipse.packagedrone.web.controller.ControllerInterceptor;
import org.eclipse.packagedrone.web.controller.binding.PathVariable;
import org.eclipse.packagedrone.web.controller.binding.RequestParameter;
import org.eclipse.packagedrone.web.util.Requests;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Controller
@ViewResolver ( "/WEB-INF/views/%s.jsp" )
@Secured
@RequestMapping ( "/job" )
@ControllerInterceptor ( SecuredControllerInterceptor.class )
@HttpConstraint ( rolesAllowed = "MANAGER" )
@ControllerInterceptor ( HttpContraintControllerInterceptor.class )
public class JobController
{
    private final static Logger logger = LoggerFactory.getLogger ( JobController.class );

    private JobManager manager;

    public void setManager ( final JobManager manager )
    {
        this.manager = manager;
    }

    @RequestMapping ( value = "/{factoryId}/create", method = RequestMethod.POST )
    @HttpConstraint ( rolesAllowed = { "MANAGER", "ADMIN" } )
    public ModelAndView create ( @PathVariable ( "factoryId" ) final String factoryId, @RequestParameter (
            required = false, value = "data" ) final String data)
    {
        final JobHandle job = this.manager.startJob ( factoryId, data );

        // forward to get loose of the POST request, so that we can reload the status page
        return new ModelAndView ( String.format ( "redirect:/job/%s/view", job.getId () ) );
    }

    @RequestMapping ( "/{id}/view" )
    public ModelAndView view ( @PathVariable ( "id" ) final String id)
    {
        final JobHandle job = this.manager.getJob ( id );

        final Map<String, Object> model = new HashMap<> ( 1 );
        model.put ( "job", job );
        return new ModelAndView ( "view", model );
    }

    /**
     * Monitor the job, only produces an HTML fragment of the current job state
     *
     * @param id
     *            the job id
     * @return the view
     */
    @RequestMapping ( "/{id}/monitor" )
    public ModelAndView monitor ( @PathVariable ( "id" ) final String id)
    {
        final JobHandle job = this.manager.getJob ( id );

        if ( job != null )
        {
            logger.debug ( "Job: {} - {}", job.getId (), job.getState () );
        }
        else
        {
            logger.debug ( "No job: {}", id );
        }

        final Map<String, Object> model = new HashMap<> ( 1 );
        model.put ( "job", job );
        return new ModelAndView ( "monitor", model );
    }

    @RequestMapping ( "/{id}/result" )
    public ModelAndView result ( @PathVariable ( "id" ) final String id, final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException
    {
        final JobHandle job = this.manager.getJob ( id );

        if ( job == null )
        {
            return CommonController.createNotFound ( "job", id );
        }

        if ( job != null && job.isComplete () && job.getError () != null )
        {
            // show default error page
            return defaultResult ( job );
        }

        final String factoryId = job.getRequest ().getFactoryId ();
        final JobFactoryDescriptor desc = this.manager.getFactory ( factoryId );

        if ( desc == null )
        {
            return defaultResult ( job );
        }

        final LinkTarget target = desc.getResultTarget ();
        if ( target == null )
        {
            return defaultResult ( job );
        }

        final LinkTarget url = target.expand ( Collections.singletonMap ( "id", id ) );

        logger.debug ( "Forwarding to job result view: {}", url );

        if ( url.getUrl ().equals ( Requests.getOriginalPath ( request ) ) )
        {
            throw new IllegalStateException ( String.format ( "Illegal redirect to same URL: %s", url.getUrl () ) );
        }

        final RequestDispatcher rd = request.getRequestDispatcher ( url.getUrl () );
        rd.forward ( request, response );

        return null;
    }

    protected ModelAndView defaultResult ( final JobHandle job )
    {
        final Map<String, Object> model = new HashMap<> ( 1 );
        model.put ( "job", job );
        return new ModelAndView ( "defaultResult", model );
    }
}


/*******************************************************************************
 * Copyright (c) 2014, 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
 *     M-Ezzat - code cleanup - squid:S2131
 *******************************************************************************/
package org.eclipse.packagedrone.utils.deb.build;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.StringWriter;
import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.time.Instant;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.function.Supplier;
import java.util.zip.GZIPOutputStream;

import org.apache.commons.compress.archivers.ar.ArArchiveEntry;
import org.apache.commons.compress.archivers.ar.ArArchiveOutputStream;
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
import org.apache.commons.io.IOUtils;
import org.bouncycastle.util.encoders.Hex;
import org.eclipse.packagedrone.utils.deb.ControlFileWriter;
import org.eclipse.packagedrone.utils.deb.control.BinaryPackageControlFile;
import org.eclipse.packagedrone.utils.deb.internal.ChecksumInputStream;

public class DebianPackageWriter implements AutoCloseable, BinaryPackageBuilder
{
    public static final Charset CHARSET = Charset.forName ( "UTF-8" );

    private static final int AR_ARCHIVE_DEFAULT_MODE = 33188; // see ArArchive

    private final ArArchiveOutputStream ar;

    private final byte[] binaryHeader = "2.0\n".getBytes ();

    private final Supplier<Instant> timestampSupplier;

    private final File dataTemp;

    private final TarArchiveOutputStream dataStream;

    private final BinaryPackageControlFile packageControlFile;

    private long installedSize = 0;

    private final Map<String, String> checkSums = new TreeMap<> ();

    private final Set<String> confFiles = new TreeSet<> ();

    private final Set<String> paths = new HashSet<> ();

    private ContentProvider preinstScript;

    private ContentProvider postinstScript;

    private ContentProvider prermScript;

    private ContentProvider postrmScript;

    public DebianPackageWriter ( final OutputStream stream, final BinaryPackageControlFile packageControlFile ) throws IOException
    {
        this ( stream, packageControlFile, Instant::now );
    }

    public DebianPackageWriter ( final OutputStream stream, final BinaryPackageControlFile packageControlFile, final Supplier<Instant> timestampSupplier ) throws IOException
    {
        Objects.requireNonNull ( timestampSupplier );

        this.timestampSupplier = timestampSupplier;
        this.packageControlFile = packageControlFile;
        BinaryPackageControlFile.validate ( packageControlFile );

        this.ar = new ArArchiveOutputStream ( stream );

        this.ar.putArchiveEntry ( new ArArchiveEntry ( "debian-binary", this.binaryHeader.length, 0, 0, AR_ARCHIVE_DEFAULT_MODE, timestampSupplier.get ().getEpochSecond () ) );
        this.ar.write ( this.binaryHeader );
        this.ar.closeArchiveEntry ();

        this.dataTemp = File.createTempFile ( "data", null );

        this.dataStream = new TarArchiveOutputStream ( new GZIPOutputStream ( new FileOutputStream ( this.dataTemp ) ) );
        this.dataStream.setLongFileMode ( TarArchiveOutputStream.LONGFILE_GNU );
    }

    public void addFile ( final File file, final String fileName, final EntryInformation entryInformation ) throws IOException
    {
        addFile ( new FileContentProvider ( file ), fileName, entryInformation, Optional.of ( (Supplier<Instant>) () -> {
            return file == null || !file.canRead () ? null : Instant.ofEpochMilli ( file.lastModified () );
        } ) );
    }

    public void addFile ( final File file, final String fileName, final EntryInformation entryInformation, final Optional<Supplier<Instant>> timestampSupplier ) throws IOException
    {
        addFile ( new FileContentProvider ( file ), fileName, entryInformation, timestampSupplier );
    }

    public void addFile ( final byte[] content, final String fileName, final EntryInformation entryInformation ) throws IOException
    {
        addFile ( new StaticContentProvider ( content ), fileName, entryInformation );
    }

    public void addFile ( final byte[] content, final String fileName, final EntryInformation entryInformation, final Optional<Supplier<Instant>> timestampSupplier ) throws IOException
    {
        addFile ( new StaticContentProvider ( content ), fileName, entryInformation, timestampSupplier );
    }

    public void addFile ( final String content, final String fileName, final EntryInformation entryInformation ) throws IOException
    {
        addFile ( new StaticContentProvider ( content ), fileName, entryInformation );
    }

    public void addFile ( final String content, final String fileName, final EntryInformation entryInformation, final Optional<Supplier<Instant>> timestampSupplier ) throws IOException
    {
        addFile ( new StaticContentProvider ( content ), fileName, entryInformation, timestampSupplier );
    }

    @Override
    public void addFile ( final ContentProvider contentProvider, String fileName, EntryInformation entryInformation, final Optional<Supplier<Instant>> timestampSupplier ) throws IOException
    {
        Objects.requireNonNull ( timestampSupplier );

        if ( entryInformation == null )
        {
            entryInformation = EntryInformation.DEFAULT_FILE;
        }

        try
        {
            fileName = cleanupPath ( fileName );

            if ( entryInformation.isConfigurationFile () )
            {
                this.confFiles.add ( fileName.substring ( 1 ) ); // without the leading dot
            }

            final TarArchiveEntry entry = new TarArchiveEntry ( fileName );
            entry.setSize ( contentProvider.getSize () );
            // in case the content provider supplies a modification time itself, use that one
            applyInfo ( entry, entryInformation );
            // if the modification time should be overridden, then do it here
            applyTimestamp ( entry, timestampSupplier );

            checkCreateParents ( fileName, timestampSupplier );

            this.dataStream.putArchiveEntry ( entry );

            final Map<String, byte[]> results = new HashMap<> ();
            try ( final ChecksumInputStream in = new ChecksumInputStream ( contentProvider.createInputStream (), results, MessageDigest.getInstance ( "MD5" ) ) )
            {
                this.installedSize += IOUtils.copyLarge ( in, this.dataStream );
            }

            this.dataStream.closeArchiveEntry ();

            // record the checksum
            recordChecksum ( fileName, results.get ( "MD5" ) );
        }
        catch ( final Exception e )
        {
            throw new IOException ( e );
        }
    }

    /**
     * clean up the path so that is looks like "./usr/local/file"
     */
    private String cleanupPath ( String fileName )
    {
        if ( fileName == null )
        {
            return null;
        }

        fileName = fileName.replace ( "\\", "/" ); // just in case we get windows paths
        fileName = fileName.replace ( "/+", "/" );

        if ( fileName.startsWith ( "./" ) )
        {
            return fileName;
        }
        if ( fileName.startsWith ( "/" ) )
        {
            return "." + fileName;
        }
        return "./" + fileName;
    }

    @Override
    public void addDirectory ( String directory, final EntryInformation entryInformation, final Optional<Supplier<Instant>> timestampSupplier ) throws IOException
    {
        directory = cleanupPath ( directory );
        if ( !directory.endsWith ( "/" ) )
        {
            directory += Character.toString ( '/' );
        }
        checkCreateParents ( directory, timestampSupplier );
        internalAddDirectory ( directory, entryInformation, timestampSupplier );
    }

    protected void internalAddDirectory ( final String path, final EntryInformation entryInformation, final Optional<Supplier<Instant>> timestampSupplier ) throws IOException
    {
        final TarArchiveEntry entry = new TarArchiveEntry ( path );
        applyInfo ( entry, entryInformation );
        applyTimestamp ( entry, timestampSupplier );

        this.dataStream.putArchiveEntry ( entry );
        this.dataStream.closeArchiveEntry ();

        this.paths.add ( path );
    }

    private static void applyTimestamp ( final TarArchiveEntry entry, final Optional<Supplier<Instant>> timestampSupplier )
    {
        timestampSupplier.map ( Supplier::get ).map ( Instant::toEpochMilli ).ifPresent ( entry::setModTime );
    }

    private static void applyInfo ( final TarArchiveEntry entry, final EntryInformation entryInformation )
    {
        if ( entryInformation == null )
        {
            return;
        }

        if ( entryInformation.getUser () != null )
        {
            entry.setUserName ( entryInformation.getUser () );
        }
        if ( entryInformation.getGroup () != null )
        {
            entry.setGroupName ( entryInformation.getGroup () );
        }
        entry.setMode ( entryInformation.getMode () );
    }

    private void checkCreateParents ( final String fileName, final Optional<Supplier<Instant>> timestampSupplier ) throws IOException
    {
        final String toks[] = fileName.split ( "/+" );

        String current = "";

        for ( int i = 0; i < toks.length - 1; i++ )
        {
            if ( toks[i].isEmpty () )
            {
                continue;
            }

            current += toks[i] + "/";
            if ( !this.paths.contains ( current ) )
            {
                internalAddDirectory ( current, EntryInformation.DEFAULT_DIRECTORY, timestampSupplier );
            }
        }
    }

    private void recordChecksum ( final String fileName, final byte[] bs )
    {
        this.checkSums.put ( fileName, Hex.toHexString ( bs ) );
    }

    @Override
    public void close () throws IOException
    {
        try
        {
            try
            {
                buildAndAddControlFile ( this.timestampSupplier );
                this.dataStream.close ();
                addArFile ( this.dataTemp, "data.tar.gz", this.timestampSupplier );
            }
            finally
            {
                this.ar.close ();
            }
        }
        finally
        {
            this.dataTemp.delete ();
        }
    }

    private void buildAndAddControlFile ( final Supplier<Instant> timestampSupplier ) throws IOException, FileNotFoundException
    {
        final File controlFile = File.createTempFile ( "control", null );
        try
        {
            try ( GZIPOutputStream gout = new GZIPOutputStream ( new FileOutputStream ( controlFile ) );
                  TarArchiveOutputStream tout = new TarArchiveOutputStream ( gout ) )
            {
                tout.setLongFileMode ( TarArchiveOutputStream.LONGFILE_GNU );

                addControlContent ( tout, "control", createControlContent (), -1, timestampSupplier );
                addControlContent ( tout, "md5sums", createChecksumContent (), -1, timestampSupplier );
                addControlContent ( tout, "conffiles", createConfFilesContent (), -1, timestampSupplier );
                addControlContent ( tout, "preinst", this.preinstScript, EntryInformation.DEFAULT_FILE_EXEC.getMode (), timestampSupplier );
                addControlContent ( tout, "prerm", this.prermScript, EntryInformation.DEFAULT_FILE_EXEC.getMode (), timestampSupplier );
                addControlContent ( tout, "postinst", this.postinstScript, EntryInformation.DEFAULT_FILE_EXEC.getMode (), timestampSupplier );
                addControlContent ( tout, "postrm", this.postrmScript, EntryInformation.DEFAULT_FILE_EXEC.getMode (), timestampSupplier );
            }
            addArFile ( controlFile, "control.tar.gz", timestampSupplier );
        }
        finally
        {
            controlFile.delete ();
        }
    }

    private void addControlContent ( final TarArchiveOutputStream out, final String name, final ContentProvider content, final int mode, final Supplier<Instant> timestampSupplier ) throws IOException
    {
        if ( content == null || !content.hasContent () )
        {
            return;
        }

        final TarArchiveEntry entry = new TarArchiveEntry ( name );
        if ( mode >= 0 )
        {
            entry.setMode ( mode );
        }

        entry.setUserName ( "root" );
        entry.setGroupName ( "root" );
        entry.setSize ( content.getSize () );
        entry.setModTime ( timestampSupplier.get ().toEpochMilli () );
        out.putArchiveEntry ( entry );
        try ( InputStream stream = content.createInputStream () )
        {
            IOUtils.copy ( stream, out );
        }
        out.closeArchiveEntry ();
    }

    protected ContentProvider createControlContent () throws IOException
    {
        this.packageControlFile.set ( BinaryPackageControlFile.Fields.INSTALLED_SIZE, Long.toString ( this.installedSize ) );

        final StringWriter sw = new StringWriter ();
        final ControlFileWriter writer = new ControlFileWriter ( sw, BinaryPackageControlFile.FORMATTERS );
        writer.writeEntries ( this.packageControlFile.getValues () );
        sw.close ();

        return new StaticContentProvider ( sw.toString () );
    }

    protected ContentProvider createChecksumContent () throws IOException
    {
        if ( this.checkSums.isEmpty () )
        {
            return ContentProvider.NULL_CONTENT;
        }

        final StringWriter sw = new StringWriter ();

        for ( final Map.Entry<String, String> entry : this.checkSums.entrySet () )
        {
            final String filename = entry.getKey ().substring ( 2 ); // without the leading dot and slash

            sw.append ( entry.getValue () );
            sw.append ( "  " );
            sw.append ( filename );
            sw.append ( '\n' );
        }

        sw.close ();

        return new StaticContentProvider ( sw.toString () );
    }

    protected ContentProvider createConfFilesContent () throws IOException
    {
        if ( this.confFiles.isEmpty () )
        {
            return ContentProvider.NULL_CONTENT;
        }

        final StringWriter sw = new StringWriter ();

        for ( final String confFile : this.confFiles )
        {
            sw.append ( confFile ).append ( '\n' );
        }

        sw.close ();
        return new StaticContentProvider ( sw.toString () );
    }

    private void addArFile ( final File file, final String entryName, final Supplier<Instant> timestampSupplier ) throws IOException
    {
        final ArArchiveEntry entry = new ArArchiveEntry ( entryName, file.length (), 0, 0, AR_ARCHIVE_DEFAULT_MODE, timestampSupplier.get ().getEpochSecond () );
        this.ar.putArchiveEntry ( entry );

        IOUtils.copy ( new FileInputStream ( file ), this.ar );

        this.ar.closeArchiveEntry ();
    }

    public void setPostinstScript ( final ContentProvider postinstScript )
    {
        this.postinstScript = postinstScript;
    }

    public void setPostrmScript ( final ContentProvider postrmScript )
    {
        this.postrmScript = postrmScript;
    }

    public void setPreinstScript ( final ContentProvider preinstScript )
    {
        this.preinstScript = preinstScript;
    }

    public void setPrermScript ( final ContentProvider prermScript )
    {
        this.prermScript = prermScript;
    }

}


/*******************************************************************************
 * 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.servlet;

import static org.eclipse.packagedrone.web.util.BasicAuthentication.parseAuthorization;

import java.io.IOException;
import java.util.Collections;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.eclipse.packagedrone.repo.channel.ChannelService;
import org.eclipse.packagedrone.repo.channel.ChannelService.By;
import org.eclipse.packagedrone.web.util.BasicAuthentication;
import org.osgi.framework.BundleContext;
import org.osgi.framework.FrameworkUtil;
import org.osgi.util.tracker.ServiceTracker;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * This is an abstract implementation for implementing servlets which require
 * the {@link StorageService}.
 *

 * The servlet ensures that the service methods (GET, POST, ... ) only get
 * called when there is a storage service present. The service can then be
 * fetched using {@link #getService(HttpServletRequest)}.
 * </p<
 */
public abstract class AbstractChannelServiceServlet extends HttpServlet
{
    private final static Logger logger = LoggerFactory.getLogger ( AbstractChannelServiceServlet.class );

    private static final long serialVersionUID = 1L;

    private static final String ATTR_CHANNEL_SERVICE = AbstractChannelServiceServlet.class.getName () + ".channelService";

    private ServiceTracker<ChannelService, ChannelService> tracker;

    public AbstractChannelServiceServlet ()
    {
        super ();
    }

    @Override
    public void init () throws ServletException
    {
        super.init ();

        final BundleContext context = FrameworkUtil.getBundle ( getClass () ).getBundleContext ();
        this.tracker = new ServiceTracker<> ( context, ChannelService.class, null );
        this.tracker.open ();
    }

    protected ChannelService getService ( final HttpServletRequest request )
    {
        return (ChannelService)request.getAttribute ( ATTR_CHANNEL_SERVICE );
    }

    @Override
    protected void service ( final HttpServletRequest request, final HttpServletResponse response ) throws ServletException, IOException
    {
        logger.trace ( "Request: {} / {}", request.getMethod (), request.getPathInfo () );

        final ChannelService service = this.tracker.getService ();

        if ( service == null )
        {
            handleNoService ( request, response );
        }
        else
        {
            request.setAttribute ( ATTR_CHANNEL_SERVICE, service );
            super.service ( request, response );
        }
    }

    protected void handleNoService ( final HttpServletRequest request, final HttpServletResponse response ) throws IOException
    {
        response.setStatus ( HttpServletResponse.SC_SERVICE_UNAVAILABLE );
        response.setContentType ( "text/plain" );
        response.getWriter ().write ( "Channel service unavailable" );
    }

    @Override
    public void destroy ()
    {
        this.tracker.close ();
        super.destroy ();
    }

    /**
     * Authenticate the request is authenticated against the deploy keys
     *

     * If the request could not be authenticated a basic authentication request
     * is sent back and the {@link HttpServletResponse} will be committed.
     *

     *
     * @param by
     *            the channel locator
     * @param request
     *            the request
     * @param response
     *            the response
     * @return false if the request was not authenticated and the
     *         response got committed, true otherwise
     * @throws IOException
     *             in case on a IO error
     */
    protected boolean authenticate ( final By by, final HttpServletRequest request, final HttpServletResponse response ) throws IOException
    {
        if ( isAuthenticated ( by, request ) )
        {
            return true;
        }

        BasicAuthentication.request ( response, "channel", "Please authenticate" );

        return false;
    }

    /**
     * Simply test if the request is authenticated against the channels deploy
     * keys
     *
     * @param by
     *            the channel locator
     * @param request
     *            the request
     * @return true if the request could be authenticated against
     *         the channels deploy keys, false otherwise
     */
    protected boolean isAuthenticated ( final By by, final HttpServletRequest request )
    {
        final String[] authToks = parseAuthorization ( request );

        if ( authToks == null )
        {
            return false;
        }

        // we don't enforce the "deploy" user name anymore

        final String deployKey = authToks[1];

        logger.debug ( "Deploy key: '{}'", deployKey );

        final ChannelService service = getService ( request );
        if ( service == null )
        {
            logger.info ( "Called 'isAuthenticated' without service" );
            return false;
        }

        return service.getChannelDeployKeyStrings ( by ).orElse ( Collections.emptySet () ).contains ( deployKey );
    }

}


/*
 * 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.xmlparser;

import java.io.InputStream;
import java.io.IOException;
import java.io.Reader;
import java.util.logging.Logger;
import java.util.logging.Level;

/**
 * Reader for UCS-2 and UCS-4 encodings.
 * (i.e., encodings from ISO-10646-UCS-(2|4)).
 *
 * @author Neil Graham, IBM
 *
 * @version $Id: UCSReader.java,v 1.3 2006/03/12 01:27:12 jluehe Exp $
 */
public class UCSReader extends Reader {

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

    //
    // Constants
    //

    /** Default byte buffer size (8192, larger than that of ASCIIReader
     * since it's reasonable to surmise that the average UCS-4-encoded
     * file should be 4 times as large as the average ASCII-encoded file).
     */
    public static final int DEFAULT_BUFFER_SIZE = 8192;

    public static final short UCS2LE = 1;
    public static final short UCS2BE = 2;
    public static final short UCS4LE = 4;
    public static final short UCS4BE = 8;

    //
    // Data
    //

    /** Input stream. */
    protected InputStream fInputStream;

    /** Byte buffer. */
    protected byte[] fBuffer;

    // what kind of data we're dealing with
    protected short fEncoding;

    //
    // Constructors
    //

    /**
     * Constructs an ASCII reader from the specified input stream
     * using the default buffer size.  The Endian-ness and whether this is
     * UCS-2 or UCS-4 needs also to be known in advance.
     *
     * @param inputStream The input stream.
     * @param encoding One of UCS2LE, UCS2BE, UCS4LE or UCS4BE.
     */
    public UCSReader(InputStream inputStream, short encoding) {
        this(inputStream, DEFAULT_BUFFER_SIZE, encoding);
    } // <init>(InputStream, short)

    /**
     * Constructs an ASCII reader from the specified input stream
     * and buffer size.  The Endian-ness and whether this is
     * UCS-2 or UCS-4 needs also to be known in advance.
     *
     * @param inputStream The input stream.
     * @param size        The initial buffer size.
     * @param encoding One of UCS2LE, UCS2BE, UCS4LE or UCS4BE.
     */
    public UCSReader(InputStream inputStream, int size, short encoding) {
        fInputStream = inputStream;
        fBuffer = new byte[size];
        fEncoding = encoding;
    } // <init>(InputStream,int,short)

    //
    // Reader methods
    //

    /**
     * Read a single character.  This method will block until a character is
     * available, an I/O error occurs, or the end of the stream is reached.
     *
     * <p> Subclasses that intend to support efficient single-character input
     * should override this method.
     *
     * @return     The character read, as an integer in the range 0 to 127
     *             (<tt>0x00-0x7f</tt>), or -1 if the end of the stream has
     *             been reached
     *
     * @exception  IOException  If an I/O error occurs
     */
    public int read() throws IOException {
        int b0 = fInputStream.read() & 0xff;
        if (b0 == 0xff)
            return -1;
        int b1 = fInputStream.read() & 0xff;
        if (b1 == 0xff)
            return -1;
        if(fEncoding >=4) {
            int b2 = fInputStream.read() & 0xff;
            if (b2 == 0xff)
                return -1;
            int b3 = fInputStream.read() & 0xff;
            if (b3 == 0xff)
                return -1;
            if (log.isLoggable(Level.FINE))
                log.fine("b0 is " + (b0 & 0xff) + " b1 " + (b1 & 0xff) + " b2 " + (b2 & 0xff) + " b3 " + (b3 & 0xff));
            if (fEncoding == UCS4BE)
                return (b0<<24)+(b1<<16)+(b2<<8)+b3;
            else
                return (b3<<24)+(b2<<16)+(b1<<8)+b0;
        } else { // UCS-2
            if (fEncoding == UCS2BE)
                return (b0<<8)+b1;
            else
                return (b1<<8)+b0;
        }
    } // read():int

    /**
     * Read characters into a portion of an array.  This method will block
     * until some input is available, an I/O error occurs, or the end of the
     * stream is reached.
     *
     * @param      ch     Destination buffer
     * @param      offset Offset at which to start storing characters
     * @param      length Maximum number of characters to read
     *
     * @return     The number of characters read, or -1 if the end of the
     *             stream has been reached
     *
     * @exception  IOException  If an I/O error occurs
     */
    public int read(char ch[], int offset, int length) throws IOException {
        int byteLength = length << ((fEncoding >= 4)?2:1);
        if (byteLength > fBuffer.length) {
            byteLength = fBuffer.length;
        }
        int count = fInputStream.read(fBuffer, 0, byteLength);
        if(count == -1) return -1;
        // try and make count be a multiple of the number of bytes we're looking for
        if(fEncoding >= 4) { // BigEndian
            // this looks ugly, but it avoids an if at any rate...
            int numToRead = (4 - (count & 3) & 3);
            for(int i=0; i<numToRead; i++) {
                int charRead = fInputStream.read();
                if(charRead == -1) { // end of input; something likely went wrong!A  Pad buffer with nulls.
                    for (int j = i;j<numToRead; j++)
                        fBuffer[count+j] = 0;
                    break;
                } else {
                    fBuffer[count+i] = (byte)charRead;
                }
            }
            count += numToRead;
        } else {
            int numToRead = count & 1;
            if(numToRead != 0) {
                count++;
                int charRead = fInputStream.read();
                if(charRead == -1) { // end of input; something likely went wrong!A  Pad buffer with nulls.
                    fBuffer[count] = 0;
                } else {
                    fBuffer[count] = (byte)charRead;
                }
            }
        }

        // now count is a multiple of the right number of bytes
        int numChars = count >> ((fEncoding >= 4)?2:1);
        int curPos = 0;
        for (int i = 0; i < numChars; i++) {
            int b0 = fBuffer[curPos++] & 0xff;
            int b1 = fBuffer[curPos++] & 0xff;
            if(fEncoding >=4) {
                int b2 = fBuffer[curPos++] & 0xff;
                int b3 = fBuffer[curPos++] & 0xff;
                if (fEncoding == UCS4BE)
                    ch[offset+i] = (char)((b0<<24)+(b1<<16)+(b2<<8)+b3);
                else
                    ch[offset+i] = (char)((b3<<24)+(b2<<16)+(b1<<8)+b0);
            } else { // UCS-2
                if (fEncoding == UCS2BE)
                    ch[offset+i] = (char)((b0<<8)+b1);
                else
                    ch[offset+i] = (char)((b1<<8)+b0);
            }
        }
        return numChars;
    } // read(char[],int,int)

    /**
     * Skip characters.  This method will block until some characters are
     * available, an I/O error occurs, or the end of the stream is reached.
     *
     * @param  n  The number of characters to skip
     *
     * @return    The number of characters actually skipped
     *
     * @exception  IOException  If an I/O error occurs
     */
    public long skip(long n) throws IOException {
        // charWidth will represent the number of bits to move
        // n leftward to get num of bytes to skip, and then move the result rightward
        // to get num of chars effectively skipped.
        // The trick with &'ing, as with elsewhere in this dcode, is
        // intended to avoid an expensive use of / that might not be optimized
        // away.
        int charWidth = (fEncoding >=4)?2:1;
        long bytesSkipped = fInputStream.skip(n<<charWidth);
        if((bytesSkipped & (charWidth | 1)) == 0) return bytesSkipped >> charWidth;
        return (bytesSkipped >> charWidth) + 1;
    } // skip(long):long

    /**
     * Tell whether this stream is ready to be read.
     *
     * @return True if the next read() is guaranteed not to block for input,
     * false otherwise.  Note that returning false does not guarantee that the
     * next read will block.
     *
     * @exception  IOException  If an I/O error occurs
     */
    public boolean ready() throws IOException {
	return false;
    } // ready()

    /**
     * Tell whether this stream supports the mark() operation.
     */
    public boolean markSupported() {
	return fInputStream.markSupported();
    } // markSupported()

    /**
     * Mark the present position in the stream.  Subsequent calls to reset()
     * will attempt to reposition the stream to this point.  Not all
     * character-input streams support the mark() operation.
     *
     * @param  readAheadLimit  Limit on the number of characters that may be
     *                         read while still preserving the mark.  After
     *                         reading this many characters, attempting to
     *                         reset the stream may fail.
     *
     * @exception  IOException  If the stream does not support mark(),
     *                          or if some other I/O error occurs
     */
    public void mark(int readAheadLimit) throws IOException {
	fInputStream.mark(readAheadLimit);
    } // mark(int)

    /**
     * Reset the stream.  If the stream has been marked, then attempt to
     * reposition it at the mark.  If the stream has not been marked, then
     * attempt to reset it in some way appropriate to the particular stream,
     * for example by repositioning it to its starting point.  Not all
     * character-input streams support the reset() operation, and some support
     * reset() without supporting mark().
     *
     * @exception  IOException  If the stream has not been marked,
     *                          or if the mark has been invalidated,
     *                          or if the stream does not support reset(),
     *                          or if some other I/O error occurs
     */
    public void reset() throws IOException {
        fInputStream.reset();
    } // reset()

    /**
     * Close the stream.  Once a stream has been closed, further read(),
     * ready(), mark(), or reset() invocations will throw an IOException.
     * Closing a previously-closed stream, however, has no effect.
     *
     * @exception  IOException  If an I/O error occurs
     */
     public void close() throws IOException {
         fInputStream.close();
     } // close()

} // class UCSReader


/*******************************************************************************
 * Copyright (c) 2015 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.web.sitemap;

import java.io.IOException;
import java.io.Writer;
import java.net.URI;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
import java.util.Optional;

import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;

public class UrlSetWriter implements AutoCloseable, UrlSetContext
{

    private static final String NS = "http://www.sitemaps.org/schemas/sitemap/0.9";

    private static final String NL = "\n";

    private static final String IN = "  ";

    private static final String IN2 = IN + IN;

    private static DateTimeFormatter FORMAT = DateTimeFormatter.ofPattern ( "yyyy-MM-dd'T'hh:mm:ssXXX", Locale.ROOT );

    private final String urlPrefix;

    private final XMLStreamWriter out;

    private boolean finished;

    private final Writer writer;

    public UrlSetWriter ( final Writer writer, final String urlPrefix, final XMLOutputFactory outputFactory ) throws IOException
    {
        this.writer = writer;
        this.urlPrefix = urlPrefix;

        try
        {
            this.out = outputFactory.createXMLStreamWriter ( writer );
        }
        catch ( final XMLStreamException e )
        {
            throw new IOException ( e );
        }

        writeStart ();
    }

    private void writeEnd () throws IOException
    {
        try
        {
            this.out.writeEndElement ();
            this.out.writeEndDocument ();
        }
        catch ( final XMLStreamException e )
        {
            throw new IOException ( e );
        }
    }

    private void writeStart () throws IOException
    {
        try
        {
            this.out.writeStartDocument ( "UTF-8", "1.0" );
            this.out.writeCharacters ( NL );

            this.out.writeStartElement ( "urlset" );
            this.out.writeDefaultNamespace ( NS );
            this.out.writeCharacters ( NL );
        }
        catch ( final XMLStreamException e )
        {
            throw new IOException ( e );
        }
    }

    @Override
    public void addLocation ( String localUrl, final Optional<Instant> lastModification, final Optional<ChangeFrequency> changeFrequency, final Optional<Double> priority )
    {
        if ( !localUrl.startsWith ( "/" ) )
        {
            localUrl = "/" + localUrl;
        }

        try
        {
            this.out.writeCharacters ( IN );
            this.out.writeStartElement ( "url" );
            this.out.writeCharacters ( NL );

            writeTag ( this.out, "loc", URI.create ( this.urlPrefix + localUrl ).toASCIIString () );

            if ( lastModification.isPresent () )
            {
                writeTag ( this.out, "lastmod", FORMAT.format ( lastModification.get ().atZone ( ZoneId.systemDefault () ) ) );
            }
            if ( changeFrequency.isPresent () )
            {
                writeTag ( this.out, "changefreq", changeFrequency.get ().getValue () );
            }
            if ( priority.isPresent () )
            {
                final double value = Math.min ( Math.max ( 0.0, priority.get () ), 1.0 );
                writeTag ( this.out, "priority", String.format ( "%.1f", value ) );
            }

            this.out.writeCharacters ( IN );
            this.out.writeEndElement ();
        }
        catch ( final Exception e )
        {
            throw new RuntimeException ( e );
        }
    }

    private void writeTag ( final XMLStreamWriter out, final String tagName, final String value ) throws XMLStreamException
    {
        out.writeCharacters ( IN2 );
        out.writeStartElement ( tagName );
        out.writeCharacters ( value );
        out.writeEndElement ();
        out.writeCharacters ( NL );
    }

    /**
     * Complete the document but don't close the underlying writer
     *
     * @throws IOException
     *             if there is an IO error
     */
    public void finish () throws IOException
    {
        if ( !this.finished )
        {
            this.finished = true;
            writeEnd ();
        }
        try
        {
            this.out.close ();
        }
        catch ( final XMLStreamException e )
        {
            throw new IOException ( e );
        }
    }

    /**
     * Complete the document and close the underlying writer
     *
     * @throws IOException
     *             if there is an IO error
     */
    @Override
    public void close () throws IOException
    {
        try
        {
            finish ();
        }
        finally
        {
            this.writer.close ();
        }
    }
}


/*******************************************************************************
 * Copyright (c) 2015 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.adapter.maven;

import static org.eclipse.packagedrone.repo.xml.XmlHelper.addElement;
import static org.eclipse.packagedrone.repo.xml.XmlHelper.addElementFirst;

import java.io.Reader;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.function.BiConsumer;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.eclipse.packagedrone.repo.MetaKey;
import org.eclipse.packagedrone.repo.channel.ArtifactInformation;
import org.eclipse.packagedrone.repo.xml.XmlHelper;
import org.eclipse.scada.utils.str.StringHelper;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

public class ChannelData
{
    protected static final DateFormat DATE_FORMAT = new SimpleDateFormat ( "yyyyMMddHHmmss" );

    private static final Pattern SNAPSHOT_PATTERN = Pattern.compile ( "(?<ts>[0-9]{8}-[0-9]{6})-1(?<bn>[0-9]+)" );

    public static abstract class Node
    {
        public boolean isDirectory ()
        {
            return false;
        }
    }

    public static class DirectoryNode extends Node
    {
        private final Map<String, Node> nodes = new HashMap<> ();

        public Map<String, Node> getNodes ()
        {
            return this.nodes;
        }

        @Override
        public boolean isDirectory ()
        {
            return true;
        }
    }

    public abstract static class ContentNode extends Node
    {
        public abstract String getMimeType ();

        public abstract byte[] getData ();
    }

    public static class DataNode extends ContentNode
    {
        private final byte[] data;

        private final String mimeType;

        public DataNode ( final byte[] data, final String mimeType )
        {
            this.data = data;
            this.mimeType = mimeType;
        }

        public DataNode ( final String data, final String mimeType )
        {
            this.data = data.getBytes ( StandardCharsets.UTF_8 );
            this.mimeType = mimeType;
        }

        @Override
        public String getMimeType ()
        {
            return this.mimeType;
        }

        @Override
        public byte[] getData ()
        {
            return this.data;
        }
    }

    public static class ChecksumNode extends ContentNode
    {

        private final ContentNode node;

        private final String alg;

        public ChecksumNode ( final ContentNode node, final String alg )
        {
            this.node = node;
            this.alg = alg;
        }

        @Override
        public String getMimeType ()
        {
            return "text/plain";
        }

        @Override
        public byte[] getData ()
        {
            try
            {
                final MessageDigest md = MessageDigest.getInstance ( this.alg );
                final byte[] result = md.digest ( this.node.getData () );
                return StringHelper.toHex ( result ).getBytes ( StandardCharsets.UTF_8 );
            }
            catch ( final Exception e )
            {
                throw new RuntimeException ( e );
            }
        }

    }

    public static class ArtifactNode extends Node
    {
        private final String artifactId;

        public ArtifactNode ( final String artifactId )
        {
            this.artifactId = artifactId;
        }

        public String getArtifactId ()
        {
            return this.artifactId;
        }
    }

    public static class VersionMetadataNode extends ContentNode
    {
        private final List<MavenInformation> infos = new LinkedList<> ();

        private final Map<MavenInformation, Date> timestamps = new HashMap<> ();

        private final String groupId;

        private final String artifactId;

        private final String version;

        public VersionMetadataNode ( final String groupId, final String artifactId, final String version )
        {
            this.groupId = groupId;
            this.artifactId = artifactId;
            this.version = version;
        }

        @Override
        public String getMimeType ()
        {
            return "application/xml";
        }

        @Override
        public byte[] getData ()
        {
            final XmlHelper xml = new XmlHelper ();

            final Document doc = createMetaData ( this.groupId, this.artifactId, this.version, this.infos, this.timestamps );
            try
            {
                return xml.toData ( doc );
            }
            catch ( final Exception e )
            {
                throw new RuntimeException ( e );
            }
        }

        public void add ( final MavenInformation info, final Date date )
        {
            this.infos.add ( info );
            this.timestamps.put ( info, date );
        }
    }

    public static class ArtifactMetadataNode extends ContentNode
    {
        private final List<MavenInformation> infos = new LinkedList<> ();

        private final String groupId;

        private final String artifactId;

        public ArtifactMetadataNode ( final String groupId, final String artifactId )
        {
            this.groupId = groupId;
            this.artifactId = artifactId;
        }

        @Override
        public String getMimeType ()
        {
            return "application/xml";
        }

        @Override
        public byte[] getData ()
        {
            final XmlHelper xml = new XmlHelper ();

            final Document doc = createMetaData ( this.groupId, this.artifactId, this.infos );
            try
            {
                return xml.toData ( doc );
            }
            catch ( final Exception e )
            {
                throw new RuntimeException ( e );
            }
        }

        public void add ( final MavenInformation info, final Date date )
        {
            this.infos.add ( info );
        }
    }

    private final DirectoryNode root = new DirectoryNode ();

    public void add ( final MavenInformation info, final ArtifactInformation art )
    {
        final String[] gn = info.getGroupId ().split ( "\\." );
        final DirectoryNode groupNode = getGroup ( gn );

        final DirectoryNode artifactBase = addDirNode ( groupNode, info.getArtifactId () );

        final ArtifactMetadataNode mdNode = addArtifactMetaDataNode ( info, artifactBase );

        final DirectoryNode versionNode = addDirNode ( artifactBase, info.getVersion () );

        addNode ( versionNode, info.makeName (), new ArtifactNode ( art.getId () ) );

        addCheckSum ( versionNode, info.makeName (), art, "md5" );
        addCheckSum ( versionNode, info.makeName (), art, "sha1" );

        mdNode.add ( info, art.getCreationTimestamp () );

        if ( info.isSnapshot () )
        {
            final VersionMetadataNode versionMd = addVersionMetaDataNode ( info, versionNode );
            versionMd.add ( info, art.getCreationTimestamp () );
        }
    }

    protected <T extends ContentNode> T addMetaDataNode ( final MavenInformation info, final DirectoryNode base, final Class<T> clazz, final Supplier<T> supp )
    {
        final Node n = base.getNodes ().get ( "maven-metadata.xml" );
        if ( n == null )
        {
            final T result = addNode ( base, "maven-metadata.xml", supp.get () );

            addNode ( base, "maven-metadata.xml.md5", new ChecksumNode ( result, "MD5" ) );
            addNode ( base, "maven-metadata.xml.sha1", new ChecksumNode ( result, "SHA1" ) );

            return result;
        }
        else if ( clazz.isAssignableFrom ( n.getClass () ) )
        {
            return clazz.cast ( n );
        }
        else
        {
            throw new IllegalStateException ( String.format ( "Invalid hierarchy. Someone blocked meta data entry: 'maven-metadata.xml'" ) );
        }
    }

    protected ArtifactMetadataNode addArtifactMetaDataNode ( final MavenInformation info, final DirectoryNode artifactBase )
    {
        return addMetaDataNode ( info, artifactBase, ArtifactMetadataNode.class, () -> new ArtifactMetadataNode ( info.getGroupId (), info.getArtifactId () ) );
    }

    protected VersionMetadataNode addVersionMetaDataNode ( final MavenInformation info, final DirectoryNode artifactBase )
    {
        return addMetaDataNode ( info, artifactBase, VersionMetadataNode.class, () -> new VersionMetadataNode ( info.getGroupId (), info.getArtifactId (), info.getVersion () ) );
    }

    protected static Document makeMetaData ( final String groupId, final String artifactId, final BiConsumer<Document, Element> cons )
    {
        final XmlHelper xml = new XmlHelper ();
        final Document doc = xml.create ();
        final Element root = doc.createElement ( "metadata" );
        doc.appendChild ( root );

        addElement ( root, "groupId", groupId );
        addElement ( root, "artifactId", artifactId );

        final Element v = addElement ( root, "versioning" );

        cons.accept ( doc, v );

        XmlHelper.addElement ( v, "lastUpdated", DATE_FORMAT.format ( new Date () ) );

        return doc;
    }

    public static Document createMetaData ( final String groupId, final String artifactId, final String version, final List<MavenInformation> infos, final Map<MavenInformation, Date> timestamps )
    {
        return makeMetaData ( groupId, artifactId, ( doc, v ) -> {

            // insert right before "versioning"
            final Element ver = doc.createElement ( "version" );
            ver.setTextContent ( version );
            v.getParentNode ().insertBefore ( ver, v );

            final Map<String, List<MavenInformation>> gi = new TreeMap<> ();
            final TreeSet<String> snapshots = new TreeSet<> ();

            // group by snapshot version
            for ( final MavenInformation info : infos )
            {
                if ( info.isSnapshot () && info.getSnapshotVersion () == null )
                {
                    continue;
                }

                snapshots.add ( info.getSnapshotVersion () );

                List<MavenInformation> list = gi.get ( info.getSnapshotVersion () );
                if ( list == null )
                {
                    list = new LinkedList<> ();
                    gi.put ( info.getSnapshotVersion (), list );
                }
                list.add ( info );
            }

            if ( !gi.isEmpty () )
            {
                final Element svs = addElement ( v, "snapshotVersions" );
                for ( final Map.Entry<String, List<MavenInformation>> entry : gi.entrySet () )
                {
                    for ( final MavenInformation info : entry.getValue () )
                    {
                        final Element sv = addElement ( svs, "snapshotVersion" );

                        addElement ( sv, "extension", info.getExtension () );
                        addElement ( sv, "value", info.getSnapshotVersion () );

                        if ( info.getClassifier () != null && !info.getClassifier ().isEmpty () )
                        {
                            addElement ( sv, "classifier", info.getClassifier () );
                        }

                        final Date ts = timestamps.get ( info );
                        if ( ts != null )
                        {
                            addElement ( sv, "updated", DATE_FORMAT.format ( ts ) ); // FIXME: replace with artifact date
                        }
                    }
                }

                {
                    final String latest = snapshots.last ();

                    final Matcher m = SNAPSHOT_PATTERN.matcher ( latest );
                    if ( m.matches () )
                    {
                        final Element s = addElementFirst ( v, "snapshot" );
                        addElement ( s, "timestamp", m.group ( "ts" ) );
                        addElement ( s, "buildNumber", m.group ( "bn" ) );
                    }
                }
            }
        } );
    }

    public static Document createMetaData ( final String groupId, final String artifactId, final List<MavenInformation> infos )
    {
        return makeMetaData ( groupId, artifactId, ( doc, v ) -> {

            final Set<String> releases = new HashSet<> ();
            final Set<String> all = new HashSet<> ();

            for ( final MavenInformation info : infos )
            {
                all.add ( info.getVersion () );
                if ( info.isSnapshot () )
                {
                    continue;
                }

                releases.add ( info.getVersion () );
            }

            final List<String> allSorted = sorted ( all );
            if ( !all.isEmpty () )
            {
                final Element vs = addElement ( v, "versions" );
                for ( final String release : allSorted )
                {
                    addElement ( vs, "version", release );
                }
            }

            if ( !releases.isEmpty () )
            {
                final List<String> releasesSorted = sorted ( releases );

                final String releaseStr = best ( releasesSorted );
                if ( releaseStr != null )
                {
                    final Element release = addElementFirst ( v, "release" );
                    release.setTextContent ( releaseStr );
                }
            }

            final String latestStr = best ( allSorted );
            if ( latestStr != null )
            {
                final Element latest = addElementFirst ( v, "latest" );
                latest.setTextContent ( latestStr );
            }

        } );
    }

    private static List<String> sorted ( final Set<String> versions )
    {
        final List<String> list = new ArrayList<> ( versions );
        Collections.sort ( list );
        return list;
    }

    private static String best ( final List<String> versions )
    {
        if ( versions.isEmpty () )
        {
            return null;
        }

        return versions.get ( versions.size () - 1 );
    }

    private static void addCheckSum ( final DirectoryNode versionNode, final String name, final ArtifactInformation art, final String string )
    {
        final String data = art.getMetaData ().get ( new MetaKey ( "hasher", string ) );
        if ( data == null )
        {
            return;
        }

        addNode ( versionNode, name + "." + string, new DataNode ( data, "text/plain" ) );
    }

    private DirectoryNode getGroup ( final String[] gn )
    {
        final LinkedList<String> dir = new LinkedList<> ( Arrays.asList ( gn ) );

        DirectoryNode current = this.root;
        while ( !dir.isEmpty () )
        {
            current = addDirNode ( current, dir.pollFirst () );
        }

        return current;
    }

    private static <T extends Node> T addNode ( final DirectoryNode current, final String seg, final T node )
    {
        if ( current.nodes.containsKey ( seg ) )
        {
            throw new IllegalStateException ( String.format ( "Invalid hierarchy. %s is already used.", seg ) );
        }

        current.nodes.put ( seg, node );

        return node;
    }

    private DirectoryNode addDirNode ( final DirectoryNode current, final String seg )
    {
        Node g = current.nodes.get ( seg );

        if ( g == null )
        {
            g = new DirectoryNode ();
            current.nodes.put ( seg, g );
            return (DirectoryNode)g;
        }

        if ( g instanceof DirectoryNode )
        {
            return (DirectoryNode)g;
        }

        throw new IllegalStateException ( String.format ( "Invalid group hierarchy. %s is of type %s.", seg, g.getClass () ) );
    }

    public Node findNode ( final Deque<String> segs )
    {
        Node current = this.root;
        while ( !segs.isEmpty () )
        {
            if ( ! ( current instanceof DirectoryNode ) )
            {
                return null;
            }

            final String n = segs.pollFirst ();
            final Node node = ( (DirectoryNode)current ).nodes.get ( n );
            if ( node == null )
            {
                return null;
            }

            current = node;
        }

        return current;
    }

    public String toJson ()
    {
        final Gson gson = makeGson ( false );
        return gson.toJson ( this );
    }

    @Override
    public String toString ()
    {
        final Gson gson = makeGson ( true );
        return gson.toJson ( this );
    }

    public static ChannelData fromJson ( final String json )
    {
        final Gson gson = makeGson ( false );
        return gson.fromJson ( json, ChannelData.class );
    }

    public static ChannelData fromReader ( final Reader reader )
    {
        final Gson gson = makeGson ( false );
        return gson.fromJson ( reader, ChannelData.class );
    }

    /**
     * Make an appropriate Gson parser to processing ChannelData instances
     *
     * @param pretty
     *            if the gson output should be "pretty printed"
     * @return the new gson instance
     */
    public static Gson makeGson ( final boolean pretty )
    {
        final GsonBuilder gb = new GsonBuilder ();

        if ( pretty )
        {
            gb.setPrettyPrinting ();
        }

        gb.registerTypeAdapter ( Node.class, new NodeAdapter () );
        gb.registerTypeAdapter ( byte[].class, new ByteArrayAdapter () );

        return gb.create ();
    }

}