/**
 * Copyright (C) 2011-2018 Red Hat, Inc. (https://github.com/Commonjava/indy)
 *
 * 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.commonjava.indy.implrepo.change;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.enterprise.event.Observes;
import javax.inject.Inject;

import org.commonjava.indy.change.event.IndyLifecycleEvent;
import org.commonjava.indy.change.event.ArtifactStorePreUpdateEvent;
import org.commonjava.indy.data.IndyDataException;
import org.commonjava.indy.data.StoreDataManager;
import org.commonjava.indy.implrepo.ImpliedReposException;
import org.commonjava.indy.implrepo.conf.ImpliedRepoConfig;
import org.commonjava.indy.implrepo.data.ImpliedRepoMetadataManager;
import org.commonjava.indy.model.core.ArtifactStore;
import org.commonjava.indy.model.core.Group;
import org.commonjava.indy.model.core.StoreKey;
import org.commonjava.atlas.maven.ident.util.JoinString;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ImpliedRepoMaintainer
{

    private final Logger logger = LoggerFactory.getLogger( getClass() );

    @Inject
    private StoreDataManager storeManager;

    @Inject
    private ImpliedRepoMetadataManager metadataManager;

    @Inject
    private ImpliedRepoConfig config;

    protected ImpliedRepoMaintainer()
    {
    }

    public ImpliedRepoMaintainer( final StoreDataManager storeManager, final ImpliedRepoMetadataManager metadataManager,
                                  final ImpliedRepoConfig config )
    {
        this.storeManager = storeManager;
        this.metadataManager = metadataManager;
        this.config = config;
    }

    public void scanAtStart( @Observes final IndyLifecycleEvent event )
    {
        if ( event.getType() != IndyLifecycleEvent.Type.started )
        {
            return;
        }

        logger.info( "Scanning for unincorporated repository implications." );
        try
        {
            final Map<StoreKey, ArtifactStore> stores = mapStores( storeManager.getAllArtifactStores() );

            if ( stores != null )
            {
                for ( final ArtifactStore store : stores.values() )
                {
                    processStore( store, stores );
                }
            }
        }
        catch ( final IndyDataException e )
        {
            logger.error( "Failed to retrieve all known stores.", e );
        }
        catch ( Throwable error )
        {
            Logger logger = LoggerFactory.getLogger( getClass() );
            logger.error( String.format( "Implied-repository maintenance failed: %s", error.getMessage() ), error );
        }

    }

    // FIXME: How to tell whether a repo that is implied by other repos was added manually???
    // That, vs. just left there after the repo that implied it was removed???
    // We cannot currently remove formerly implied repos because we can't distinguish between the above states.
    public void updateImpliedStores( @Observes final ArtifactStorePreUpdateEvent event )
    {
        if ( !storeManager.isStarted() )
        {
            return;
        }

        if ( !config.isEnabled() )
        {
            logger.debug( "Implied-repository processing is not enabled." );
            return;
        }

        try
        {
            // TODO: Update for changes map.
            final Map<StoreKey, ArtifactStore> currentStores = mapStores( event );

            for ( final ArtifactStore store : event )
            {
                logger.debug( "Processing store: {} for implied repo metadata", store );
                processStore( store, currentStores );
            }
        }
        catch ( Throwable error )
        {
            Logger logger = LoggerFactory.getLogger( getClass() );
            logger.error( String.format( "Implied-repository maintenance failed: %s", error.getMessage() ), error );
        }
    }

    private void processStore( final ArtifactStore store, final Map<StoreKey, ArtifactStore> currentStores )
    {
        final ImpliedRepoMaintJob job = new ImpliedRepoMaintJob( store, currentStores );
        if ( !initJob( job ) )
        {
            return;
        }

        if ( processImpliedRepos( job ) )
        {
            logger.info( "Group: {} updated with {} implied repositories.", job.group.getKey(), job.added.size() );

            // Since we're getting in ahead of persistence, we shouldn't need to store anything in a store that was in the event.
            //                final String message =
            //                    String.format( "On update of group: %s, implied membership was recalculated.\n\nAdded:"
            //                        + "\n  %s\n\nNOTE: This update may have resulted in stores that were previously "
            //                        + "implied by another member persisting as members even after the store that implied "
            //                        + "them was removed.", store.getName(), new JoinString( "\n  ", job.added ) );
            //
            //                final ChangeSummary summary = new ChangeSummary( ChangeSummary.SYSTEM_USER, message );
            //                try
            //                {
            //                    storeManager.storeArtifactStore( job.store, summary );
            //                }
            //                catch ( final IndyDataException e )
            //                {
            //                    logger.error( "Failed to store implied-membership changes to: " + store.getKey(), e );
            //                }
        }
    }

    private Map<StoreKey, ArtifactStore> mapStores( final Iterable<ArtifactStore> stores )
    {
        final Map<StoreKey, ArtifactStore> result = new HashMap<>();
        if ( stores != null )
        {
            for ( final ArtifactStore store : stores )
            {
                result.put( store.getKey(), store );
            }
        }

        return result;
    }

    private boolean processImpliedRepos( final ImpliedRepoMaintJob job )
    {
        final Set<StoreKey> processed = new HashSet<>();

        // pre-load all existing reachable members to processed list, to prevent re-adding them via
        // implications. Reachable means they could be in nested groups.
        for ( final ArtifactStore member : job.reachableMembers )
        {
            processed.add( member.getKey() );
        }

        logger.debug( "Preset processed-implications to reachable members:\n  {}",
                      new JoinString( "\n  ", processed ) );

        int lastLen = 0;
        boolean changed = false;
        job.added = new ArrayList<>();

//        job.group = job.group.copyOf();

        // iterate through group membership looking for implied stores that aren't already members.
        // For each implied store:
        //  1. load the store
        //  2. add the implied store's key to the processed list
        //  3. add the implied store's key to the group's membership
        //  4. add the implied store to the members list
        // As soon as we go an iteration without adding a new member, we've captured everything.
        do
        {
            lastLen = job.members.size();

            for ( final ArtifactStore member : new ArrayList<>( job.members ) )
            {
                logger.debug( "Processing member: {} for implied repos within group: {}", member.getKey(),
                              job.group.getKey() );
                processed.add( member.getKey() );
                List<StoreKey> implied;
                try
                {
                    implied = metadataManager.getStoresImpliedBy( member );
                }
                catch ( final ImpliedReposException e )
                {
                    logger.error( "Failed to retrieve implied-store metadata for: " + member.getKey(), e );
                    continue;
                }

                if ( implied == null || implied.isEmpty() )
                {
                    continue;
                }

                implied.removeAll( processed );

                for ( final StoreKey key : implied )
                {
                    logger.debug( "Found implied store: {} not already in group: {}", key, job.group.getKey() );
                    ArtifactStore impliedStore;
                    try
                    {
                        impliedStore = storeManager.getArtifactStore( key );
                    }
                    catch ( final IndyDataException e )
                    {
                        logger.error( "Failed to retrieve store: " + key + " implied by: " + member.getKey(), e );
                        continue;
                    }

                    logger.info( "Adding: {} to group: {} (implied by POMs in: {})", key, job.group.getKey(),
                                 member.getKey() );

                    processed.add( key );
                    job.added.add( key );

                    job.group.addConstituent( key );

                    job.members.add( impliedStore );
                    changed = true;
                }
            }
        }
        while ( job.members.size() > lastLen );

        return changed;
    }

    private boolean initJob( final ImpliedRepoMaintJob job )
    {
        // TODO: Regardless of whether it's a group, look for implied repo metadata. If found, find the
        // groups this store belongs to and update them. If we use a pre/post event pair like with delete,
        // we might be able to fix the repo-removal problem too.
        if ( !( job.store instanceof Group ) )
        {
            logger.debug( "ImpliedRepoMaint: Ignoring non-group: {}", job.store.getKey() );
            return false;
        }

        if ( !config.isEnabledForGroup( job.store.getName() ) )
        {
            logger.debug( "ImpliedRepoMaint: Implied repositories not enabled for group: {}", job.store.getKey() );
            return false;
        }

        logger.debug( "Processing group: {} for stores implied by membership which are not yet in the membership",
                      job.store.getName() );
        job.group = (Group) job.store;

        try
        {
            // getOrderedStoresInGroup(), but we can't use persisted info...
            job.members = loadMemberStores( job.group, job );

            // getOrderedConcreteStores(), but we can't use the persisted info...
            final LinkedHashSet<ArtifactStore> reachable = new LinkedHashSet<>( job.members.size() );
            for ( final ArtifactStore member : job.members )
            {
                if ( member instanceof Group )
                {
                    reachable.addAll( loadMemberStores( (Group) member, job ) );
                }
                else
                {
                    reachable.add( member );
                }
            }

            job.reachableMembers = new ArrayList<>( reachable );

            logger.debug( "For group: {}\n Members: {}\n  Reachable Concrete Members: {}", job.group.getKey(),
                          job.members, job.reachableMembers );
        }
        catch ( final IndyDataException e )
        {
            logger.error( "Failed to retrieve member stores for group: " + job.group.getName(), e );
        }

        if ( job.members == null )
        {
            logger.debug( "ImpliedRepoMaint: Group: {} has no membership", job.store.getKey() );
            return false;
        }

        return true;
    }

    private List<ArtifactStore> loadMemberStores( final Group group, final ImpliedRepoMaintJob job )
        throws IndyDataException
    {
        final List<StoreKey> constituents = new ArrayList<>( group.getConstituents() );
        final List<ArtifactStore> members = new ArrayList<>( constituents.size() );

        for ( final StoreKey memberKey : constituents )
        {
            ArtifactStore store = job.currentStores.get( memberKey );
            if ( store == null )
            {
                store = storeManager.getArtifactStore( memberKey );
            }

            if ( store == null )
            {
                logger.warn( "Store not found for key: {} (member of: {})", memberKey, group.getKey() );
                continue;
            }

            members.add( store );
        }

        return members;
    }

    public static final class ImpliedRepoMaintJob
    {
        List<StoreKey> added;

        Group group;

        List<ArtifactStore> reachableMembers;

        List<ArtifactStore> members;

        final ArtifactStore store;

        final Map<StoreKey, ArtifactStore> currentStores;

        public ImpliedRepoMaintJob( final ArtifactStore store, final Map<StoreKey, ArtifactStore> currentStores )
        {
            this.store = store;
            this.currentStores = currentStores;
        }
    }

}


/**
 * Copyright (C) 2011-2018 Red Hat, Inc. (https://github.com/Commonjava/indy)
 *
 * 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.commonjava.indy.httprox.util;

//import sun.security.tools.keytool.CertAndKeyGen;

import sun.security.x509.AlgorithmId;
import sun.security.x509.BasicConstraintsExtension;
import sun.security.x509.CertificateAlgorithmId;
import sun.security.x509.CertificateExtensions;
import sun.security.x509.CertificateSerialNumber;
import sun.security.x509.CertificateValidity;
import sun.security.x509.CertificateVersion;
import sun.security.x509.CertificateX509Key;
import sun.security.x509.X500Name;
import sun.security.x509.X509CertImpl;
import sun.security.x509.X509CertInfo;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.math.BigInteger;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.GeneralSecurityException;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.Principal;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Date;
import java.io.IOException;
import java.util.concurrent.TimeUnit;

/**
 * Created by ruhan on 9/18/18.
 */
public class CertUtils
{
    public static final String DEFAULT_SIGN_ALGORITHM = "SHA256withRSA";

    public static final String KEY_TYPE_RSA = "RSA";

    public static final String CERT_TYPE_X509 = "X.509";

    public static final int DEFAULT_CERT_EXPIRATION_DAYS = 365;

    /**
     * Create a self-signed X.509 cert
     *
     * @param dn        the X.509 Distinguished Name, eg "CN=Test, L=London, C=GB"
     * @param days      how many days from now the cert is valid for
     * @param algorithm the signing algorithm, eg "SHA256withRSA"
     */
    public static X509Certificate generateX509Certificate( KeyPair pair, String dn, int days, String algorithm )
                    throws GeneralSecurityException, IOException
    {
        PrivateKey privateKey = pair.getPrivate();
        X509CertInfo info = new X509CertInfo();
        Date from = new Date();
        Date to = new Date( from.getTime() + TimeUnit.DAYS.toMillis( days ) );
        CertificateValidity interval = new CertificateValidity( from, to );
        BigInteger sn = new BigInteger( 64, new SecureRandom() );
        X500Name owner = new X500Name( dn );

        info.set( X509CertInfo.VALIDITY, interval );
        info.set( X509CertInfo.SERIAL_NUMBER, new CertificateSerialNumber( sn ) );
        info.set( X509CertInfo.SUBJECT, owner );
        info.set( X509CertInfo.ISSUER, owner );
        info.set( X509CertInfo.KEY, new CertificateX509Key( pair.getPublic() ) );
        info.set( X509CertInfo.VERSION, new CertificateVersion( CertificateVersion.V3 ) );

        AlgorithmId algo = new AlgorithmId( AlgorithmId.sha256WithRSAEncryption_oid );
        info.set( X509CertInfo.ALGORITHM_ID, new CertificateAlgorithmId( algo ) );

        // Sign the cert to identify the algorithm that's used.
        X509CertImpl cert = new X509CertImpl( info );
        cert.sign( privateKey, algorithm );

        // Update the algorithm, and resign.
        algo = (AlgorithmId) cert.get( X509CertImpl.SIG_ALG );
        info.set( CertificateAlgorithmId.NAME + "." + CertificateAlgorithmId.ALGORITHM, algo );
        cert = new X509CertImpl( info );
        cert.sign( privateKey, algorithm );
        return cert;
    }

    public static X509Certificate loadX509Certificate( File file ) throws CertificateException, IOException
    {
        CertificateFactory cf = CertificateFactory.getInstance( CERT_TYPE_X509 );
        Certificate ca;
        try (InputStream caInput = new BufferedInputStream( new FileInputStream( file ) ))
        {
            ca = cf.generateCertificate( caInput );
        }
        return (X509Certificate) ca;
    }

    public static KeyStore createKeyStore()
                    throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException
    {
        String keyStoreType = KeyStore.getDefaultType();
        KeyStore keyStore = KeyStore.getInstance( keyStoreType );
        keyStore.load( null, null );
        return keyStore;
    }

    public static KeyStore loadKeyStore( File file, String passwd )
                    throws IOException, KeyStoreException, CertificateException, NoSuchAlgorithmException
    {
        String keyStoreType = KeyStore.getDefaultType(); // jks
        KeyStore keyStore = KeyStore.getInstance( keyStoreType );
        keyStore.load( new FileInputStream( file ), passwd.toCharArray() );
        return keyStore;
    }

    public static PrivateKey getPrivateKey( String filename ) throws Exception
    {
        byte[] keyBytes = Files.readAllBytes( Paths.get( filename ) );

        PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec( keyBytes );
        KeyFactory kf = KeyFactory.getInstance( KEY_TYPE_RSA );
        return kf.generatePrivate( spec );
    }

    public static PublicKey getPublicKey( String filename ) throws Exception
    {
        byte[] keyBytes = Files.readAllBytes( Paths.get( filename ) );

        X509EncodedKeySpec spec = new X509EncodedKeySpec( keyBytes );
        KeyFactory kf = KeyFactory.getInstance( KEY_TYPE_RSA );
        return kf.generatePublic( spec );
    }

    public static X509Certificate createSignedCertificate( X509Certificate certificate,
                                                           X509Certificate issuerCertificate,
                                                           PrivateKey issuerPrivateKey, boolean isIntermediate )
                    throws Exception
    {
        Principal issuer = issuerCertificate.getSubjectDN();
        String issuerSigAlg = issuerCertificate.getSigAlgName();

        byte[] inCertBytes = certificate.getTBSCertificate();
        X509CertInfo info = new X509CertInfo( inCertBytes );
        info.set( X509CertInfo.ISSUER, issuer );

        if ( isIntermediate )
        {
            CertificateExtensions exts = new CertificateExtensions();
            BasicConstraintsExtension bce = new BasicConstraintsExtension( true, -1 );
            exts.set( BasicConstraintsExtension.NAME, new BasicConstraintsExtension( false, bce.getExtensionValue() ) );
            info.set( X509CertInfo.EXTENSIONS, exts );
        }

        X509CertImpl outCert = new X509CertImpl( info );
        outCert.sign( issuerPrivateKey, issuerSigAlg );

        return outCert;
    }

    public static CertificateAndKeys createSignedCertificateAndKey( String dn, X509Certificate issuerCertificate,
                                                                    PrivateKey issuerPrivateKey,
                                                                    boolean isIntermediate ) throws Exception
    {
        /*
         * CertAndKeyGen is jre class. Maven compile will fail unless use some additional plugin settings.
         * Although it is neat and nice, ATM we just use old fashioned code to create cert.
         *
        CertAndKeyGen gen = new CertAndKeyGen( KEY_TYPE_RSA, DEFAULT_SIGN_ALGORITHM, null );
        gen.generate( KEY_BITS );
        X509Certificate cert = gen.getSelfCertificate(new X500Name(dn), TimeUnit.DAYS.toMillis( DEFAULT_CERT_EXPIRATION_DAYS ));
        */

        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance( KEY_TYPE_RSA );
        KeyPair pair = keyPairGenerator.generateKeyPair();

        X509Certificate cert = generateX509Certificate( pair, dn, DEFAULT_CERT_EXPIRATION_DAYS, DEFAULT_SIGN_ALGORITHM );

        X509Certificate signedCertificate =
                        createSignedCertificate( cert, issuerCertificate, issuerPrivateKey, isIntermediate );
        PublicKey publicKey = pair.getPublic();
        PrivateKey privateKey = pair.getPrivate();
        return new CertificateAndKeys( signedCertificate, privateKey, publicKey );
    }

}


/**
 * Copyright (C) 2011-2018 Red Hat, Inc. (https://github.com/Commonjava/indy)
 *
 * 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.commonjava.indy.db.common;

import org.commonjava.cdi.util.weft.Locker;
import org.commonjava.indy.audit.ChangeSummary;
import org.commonjava.indy.change.event.ArtifactStoreUpdateType;
import org.commonjava.indy.data.ArtifactStoreQuery;
import org.commonjava.indy.data.IndyDataException;
import org.commonjava.indy.data.StoreDataManager;
import org.commonjava.indy.data.StoreEventDispatcher;
import org.commonjava.indy.measure.annotation.Measure;
import org.commonjava.indy.model.core.ArtifactStore;
import org.commonjava.indy.model.core.HostedRepository;
import org.commonjava.indy.model.core.StoreKey;
import org.commonjava.indy.util.ApplicationStatus;
import org.commonjava.maven.galley.event.EventMetadata;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.Stream;

import static org.commonjava.indy.model.core.StoreType.hosted;

public abstract class AbstractStoreDataManager
        implements StoreDataManager
{
    protected static final long LOCK_TIMEOUT_SECONDS = 10;

    private final Logger logger = LoggerFactory.getLogger( getClass() );

    protected final Locker<StoreKey> opLocks = new Locker<>(); // used internally

    abstract protected StoreEventDispatcher getStoreEventDispatcher();

    @Override
    public ArtifactStoreQuery<ArtifactStore> query()
    {
        return new DefaultArtifactStoreQuery<>( this );
    }

    protected abstract ArtifactStore getArtifactStoreInternal( final StoreKey key );

    @Override
    public ArtifactStore getArtifactStore( final StoreKey key )
            throws IndyDataException
    {
        return getArtifactStoreInternal( key );
    }

    @Override
    @Measure
    public boolean storeArtifactStore( final ArtifactStore store, final ChangeSummary summary,
                                       final boolean skipIfExists, final boolean fireEvents,
                                       final EventMetadata eventMetadata )
            throws IndyDataException
    {
        return store( store, summary, skipIfExists, fireEvents, eventMetadata );
    }

    protected void preStore( final ArtifactStore store, final ArtifactStore original, final ChangeSummary summary,
                             final boolean exists, final boolean fireEvents, final EventMetadata eventMetadata )
            throws IndyDataException
    {
        StoreEventDispatcher dispatcher = getStoreEventDispatcher();
        if ( dispatcher != null && isStarted() && fireEvents )
        {
            logger.debug( "Firing store pre-update event for: {} (originally: {})", store, original );
            dispatcher.updating( exists ? ArtifactStoreUpdateType.UPDATE : ArtifactStoreUpdateType.ADD, eventMetadata,
                                 Collections.singletonMap( store, original ) );

            if ( exists )
            {
                if ( store.isDisabled() && !original.isDisabled() )
                {
                    dispatcher.disabling( eventMetadata, store );
                }
                else if ( !store.isDisabled() && original.isDisabled() )
                {
                    dispatcher.enabling( eventMetadata, store );
                }
            }
        }
    }

    protected void postStore( final ArtifactStore store, final ArtifactStore original, final ChangeSummary summary,
                              final boolean exists, final boolean fireEvents, final EventMetadata eventMetadata )
            throws IndyDataException
    {
        StoreEventDispatcher dispatcher = getStoreEventDispatcher();
        if ( dispatcher != null && isStarted() && fireEvents )
        {
            logger.debug( "Firing store post-update event for: {} (originally: {})", store, original );
            dispatcher.updated( exists ? ArtifactStoreUpdateType.UPDATE : ArtifactStoreUpdateType.ADD, eventMetadata,
                                Collections.singletonMap( store, original ) );

            if ( exists )
            {
                if ( store.isDisabled() && !original.isDisabled() )
                {
                    dispatcher.disabled( eventMetadata, store );
                }
                else if ( !store.isDisabled() && original.isDisabled() )
                {
                    dispatcher.enabled( eventMetadata, store );
                }
            }
        }
    }

    protected void preDelete( final ArtifactStore store, final ChangeSummary summary, final boolean fireEvents,
                              final EventMetadata eventMetadata )
            throws IndyDataException
    {
        StoreEventDispatcher dispatcher = getStoreEventDispatcher();
        if ( dispatcher != null && isStarted() && fireEvents )
        {
            dispatcher.deleting( eventMetadata, store );
        }
    }

    protected void postDelete( final ArtifactStore store, final ChangeSummary summary, final boolean fireEvents,
                               final EventMetadata eventMetadata )
            throws IndyDataException
    {
        StoreEventDispatcher dispatcher = getStoreEventDispatcher();
        if ( dispatcher != null && isStarted() && fireEvents )
        {
            dispatcher.deleted( eventMetadata, store );
        }
    }

    protected abstract ArtifactStore removeArtifactStoreInternal( StoreKey key );

    @Override
    public void deleteArtifactStore( final StoreKey key, final ChangeSummary summary,
                                     final EventMetadata eventMetadata )
            throws IndyDataException
    {
        AtomicReference<IndyDataException> error = new AtomicReference<>();
        opLocks.lockAnd( key, LOCK_TIMEOUT_SECONDS, k->{
            try
            {
                final ArtifactStore store = getArtifactStoreInternal( k );
                if ( store == null )
                {
                    logger.warn( "No store found for: {}", k );
                    return null;
                }

                if ( isReadonly( store ) )
                {
                    throw new IndyDataException( ApplicationStatus.METHOD_NOT_ALLOWED.code(),
                                                 "The store {} is readonly. If you want to delete this store, please modify it to non-readonly",
                                                 store.getKey() );
                }

                preDelete( store, summary, true, eventMetadata );

                ArtifactStore removed = removeArtifactStoreInternal( k );
                logger.info( "REMOVED store: {}", removed );

                postDelete( store, summary, true, eventMetadata );
            }
            catch ( IndyDataException e )
            {
                error.set( e );
            }

            return null;
        }, (k,lock)->{
            error.set( new IndyDataException( "Failed to lock: %s for DELETE after %d seconds.", k,
                                              LOCK_TIMEOUT_SECONDS ) );
            return false;
        } );

        IndyDataException ex = error.get();
        if ( ex != null )
        {
            throw ex;
        }
    }

    /**
     * TODO: currently we only check hosted readonly to prevent unexpected removing of both files and repo itself.
     * We may expand to remote or group in the future to support functions like remote repo "deploy-through".
     */
    @Override
    public boolean isReadonly( final ArtifactStore store )
    {
        if ( store != null )
        {
            if ( store.getKey().getType() == hosted && ( (HostedRepository) store ).isReadonly() )
            {
                return true;
            }
        }
        return false;
    }

    @Override
    public void install()
            throws IndyDataException
    {
    }

    @Override
    public abstract void clear( final ChangeSummary summary )
            throws IndyDataException;

    @Override
    public abstract Set<ArtifactStore> getAllArtifactStores()
            throws IndyDataException;

    @Override
    public Stream<ArtifactStore> streamArtifactStores()
            throws IndyDataException
    {
        return getAllArtifactStores().stream();
    }

    @Override
    public abstract Map<StoreKey, ArtifactStore> getArtifactStoresByKey();

    @Override
    public abstract boolean hasArtifactStore( final StoreKey key );

    @Override
    public void reload()
            throws IndyDataException
    {
    }

    @Override
    public abstract boolean isStarted();

    protected abstract ArtifactStore putArtifactStoreInternal(final StoreKey storeKey, ArtifactStore store);

    protected boolean store( final ArtifactStore store, final ChangeSummary summary, final boolean skipIfExists,
                           final boolean fireEvents, final EventMetadata eventMetadata )
            throws IndyDataException
    {
        if ( store == null )
        {
            logger.warn( "Tried to store null ArtifactStore!" );
            return false;
        }

        AtomicReference<IndyDataException> error = new AtomicReference<>();
        logger.trace( "Storing {} using operation lock: {}", store, opLocks );

        final StoreKey storeKey = store.getKey();

        Function<StoreKey, Boolean> lockHandler = k -> {
            try
            {
                ArtifactStore original = getArtifactStoreInternal( k );
                if ( original == store )
                {
                    // if they're the same instance, preUpdate events may not work correctly!
                    logger.warn( "Storing changes on existing instance of: {}! You forgot to call {}.copyOf().", store,
                                 store.getClass().getSimpleName() );
                }

                if ( skipIfExists && original != null )
                {
                    logger.info( "Skip storing for {} (repo exists)", original );
                    return true;
                }

                try
                {
                    if ( eventMetadata != null && summary != null )
                    {
                        eventMetadata.set( StoreDataManager.CHANGE_SUMMARY, summary );
                    }
                    logger.debug( "Starting pre-store actions for {}", k );
                    preStore( store, original, summary, original != null, fireEvents, eventMetadata );
                    logger.debug( "Pre-store actions complete for {}", k );
                }
                catch ( IndyDataException e )
                {
                    error.set( e );
                    return false;
                }

                logger.debug( "Put {} to stores map", k );
                final ArtifactStore old = putArtifactStoreInternal( store.getKey(), store );

                try
                {
                    logger.debug( "Starting post-store actions for {}", k );
                    postStore( store, original, summary, original != null, fireEvents, eventMetadata );
                    logger.debug( "Post-store actions complete for {}", k );
                }
                catch ( final IndyDataException e )
                {
                    if ( old != null )
                    {
                        logger.error( "postStore() failed for {}. Rollback to old value: {}", store, old );
                        putArtifactStoreInternal( old.getKey(), old );
                    }
                    error.set( e );
                    return false;
                }

                return true;
            }
            catch ( RuntimeException e )
            {
                //logger.error( "Runtime exception trying to store: " + k, e );
                error.set( new IndyDataException( "Runtime exception trying to store: " + k, e ) );
            }

            return false;
        };

        BiFunction<StoreKey, ReentrantLock, Boolean> lockFailedHandler = (k,lock) -> {
            error.set( new IndyDataException( "Failed to lock: %s for STORE after %d seconds.", k,
                                              LOCK_TIMEOUT_SECONDS ) );
            return false;
        };

        boolean result = opLocks.lockAnd( storeKey, LOCK_TIMEOUT_SECONDS, lockHandler, lockFailedHandler );

        IndyDataException ex = error.get();
        if ( ex != null )
        {
            throw ex;
        }

        return result;
    }

}


/**
 * Copyright (C) 2011-2018 Red Hat, Inc. (https://github.com/Commonjava/indy)
 *
 * 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.commonjava.indy.metrics;

import org.apache.commons.lang3.ClassUtils;

import static com.codahale.metrics.MetricRegistry.name;
import static org.apache.commons.lang.StringUtils.isBlank;

public class IndyMetricsConstants
{
    public static final String DEFAULT = "default";

    public static final String EXCEPTION = "exception";

    public static final String METER = "meter";

    public static final String TIMER = "timer";

    public static final String SKIP_METRIC = "skip-this-metric";

    /**
     * Get default metric name. Use abbreviated package name, e.g., foo.bar.ClassA.methodB -> f.b.ClassA.methodB
     */
    public static String getDefaultName( Class<?> declaringClass, String method )
    {
        // minimum len 1 shortens the package name and keeps class name
        String cls = ClassUtils.getAbbreviatedName( declaringClass.getName(), 1 );
        return name( cls, method );
    }

    /**
     * Get default metric name. Use abbreviated package name, e.g., foo.bar.ClassA.methodB -> f.b.ClassA.methodB
     */
    public static String getDefaultName( String declaringClass, String method )
    {
        // minimum len 1 shortens the package name and keeps class name
        String cls = ClassUtils.getAbbreviatedName( declaringClass, 1 );
        return name( cls, method );
    }

    /**
     * Get the metric fullname with no default value.
     * @param nameParts user specified name parts
     */
    public static String getSupername( String nodePrefix, String... nameParts )
    {
        return name( nodePrefix, nameParts );
    }

    /**
     * Get the metric fullname.
     * @param name user specified name
     * @param defaultName 'class name + method name', not null.
     */
    public static String getName( String nodePrefix, String name, String defaultName, String... suffix )
    {
        if ( isBlank( name ) || name.equals( DEFAULT ) )
        {
            name = defaultName;
        }
        return name( name( nodePrefix, name ), suffix );
    }

}


/**
 * Copyright (C) 2011-2018 Red Hat, Inc. (https://github.com/Commonjava/indy)
 *
 * 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.commonjava.indy.relate.util;

import org.commonjava.indy.data.IndyDataException;
import org.commonjava.indy.data.StoreDataManager;
import org.commonjava.indy.model.core.ArtifactStore;
import org.commonjava.indy.model.core.Group;
import org.commonjava.indy.model.core.StoreKey;
import org.commonjava.indy.model.core.io.IndyObjectMapper;
import org.commonjava.indy.relate.conf.RelateConfig;
import org.commonjava.indy.util.LocationUtils;
import org.commonjava.atlas.maven.graph.model.EProjectDirectRelationships;
import org.commonjava.atlas.maven.ident.ref.ProjectVersionRef;
import org.commonjava.atlas.maven.ident.util.ArtifactPathInfo;
import org.commonjava.maven.galley.maven.model.view.MavenPomView;
import org.commonjava.maven.galley.maven.parse.MavenPomReader;
import org.commonjava.maven.galley.maven.rel.MavenModelProcessor;
import org.commonjava.maven.galley.maven.rel.ModelProcessorConfig;
import org.commonjava.maven.galley.model.ConcreteResource;
import org.commonjava.maven.galley.model.Location;
import org.commonjava.maven.galley.model.Transfer;
import org.commonjava.maven.galley.model.TransferOperation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URI;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import static org.commonjava.indy.model.core.StoreType.group;
import static org.commonjava.maven.galley.maven.model.view.MavenPomView.ALL_PROFILES;

/**
 * Created by ruhan on 3/16/17.
 */
@ApplicationScoped
public class RelateGenerationManager
{
    public static final String POM_SUFFIX = ".pom";

    public static final String REL_SUFFIX = ".rel";

    public static final String REL_DIRECT_GENERATING = "rel-direct-generating";

    @Inject
    private MavenModelProcessor mavenModelProcessor;

    @Inject
    private StoreDataManager storeManager;

    @Inject
    private MavenPomReader mavenPomReader;

    @Inject
    private IndyObjectMapper indyObjectMapper;

    @Inject
    private RelateConfig config;

    /**
     * Generate relationship file for pom transfer.
     * @param transfer
     * @param op
     * @return transfer pointing to the generated rel file.
     */
    public Transfer generateRelationshipFile( Transfer transfer, TransferOperation op )
    {
        final Logger logger = LoggerFactory.getLogger( getClass() );
        if ( !config.isEnabled() )
        {
            logger.debug( "Relate Add-on is not enabled." );
            return null;
        }

        logger.debug( "Relate generation for {}", transfer );

        if ( transfer == null )
        {
            logger.debug( "No transfer. No .rel generation performed." );
            return null;
        }

        String txfrPath = transfer.getPath();
        if ( !txfrPath.endsWith( ".pom" ) )
        {
            logger.debug( "This is not a pom transfer." );
            return null;
        }

        ArtifactPathInfo artPathInfo = ArtifactPathInfo.parse( txfrPath );
        if ( artPathInfo == null )
        {
            logger.debug( "Not an artifact download ({}). No .rel generation performed.", txfrPath );
            return null;
        }

        ConcreteResource pomResource = transfer.getResource();

        StoreKey storeKey = StoreKey.fromString( transfer.getLocation().getName() );
        ArtifactStore store;
        try
        {
            store = storeManager.getArtifactStore( storeKey );
        }
        catch ( final IndyDataException ex )
        {
            logger.error( "Error retrieving artifactStore with key " + storeKey, ex );
            return null;
        }

        logger.debug( "Generate .rel corresponding to associated POM download: {}/{}", storeKey,
                      pomResource.getPath() );

        try
        {
            URI source = new URI( pomResource.getLocation().getUri() + REL_SUFFIX );

            ProjectVersionRef ref = artPathInfo.getProjectId();

            // get all groups that this store is a member of
            Set<ArtifactStore> stores = new HashSet<>();
            stores.add( store );
            stores.addAll( storeManager.query().getGroupsContaining( store.getKey() ) );

            List<? extends Location> supplementalLocations =
                            LocationUtils.toLocations( stores.toArray( new ArtifactStore[0] ) );

            MavenPomView pomView = mavenPomReader.read( ref, transfer, supplementalLocations, ALL_PROFILES );

            EProjectDirectRelationships rel =
                            mavenModelProcessor.readRelationships( pomView, source, new ModelProcessorConfig() );

            Transfer transferRel = transfer.getSiblingMeta( REL_SUFFIX );
            writeRelationships( rel, transferRel, op );

            return transferRel;
        }
        catch ( Exception e )
        {
            logger.error( "Error generating .rel file for " + txfrPath + " from store " + store, e );
            return null;
        }
    }

    private void writeRelationships( final EProjectDirectRelationships rels, Transfer transfer,
                                            TransferOperation op ) throws IOException
    {
        try (OutputStream out = transfer.openOutputStream( op, true ))
        {
            String json = indyObjectMapper.writeValueAsString( rels );
            out.write( json.getBytes() );
        }
    }
}


/**
 * Copyright (C) 2011-2018 Red Hat, Inc. (https://github.com/Commonjava/indy)
 *
 * 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.commonjava.indy.koji.content;

import com.redhat.red.build.koji.KojiClient;
import com.redhat.red.build.koji.KojiClientException;
import com.redhat.red.build.koji.model.xmlrpc.KojiArchiveInfo;
import com.redhat.red.build.koji.model.xmlrpc.KojiBuildArchiveCollection;
import com.redhat.red.build.koji.model.xmlrpc.KojiBuildInfo;
import com.redhat.red.build.koji.model.xmlrpc.KojiSessionInfo;
import org.apache.commons.io.IOUtils;
import org.commonjava.indy.IndyWorkflowException;
import org.commonjava.indy.content.ContentDigester;
import org.commonjava.indy.content.DirectContentAccess;
import org.commonjava.indy.data.IndyDataException;
import org.commonjava.indy.data.StoreDataManager;
import org.commonjava.indy.koji.conf.IndyKojiConfig;
import org.commonjava.indy.measure.annotation.Measure;
import org.commonjava.indy.measure.annotation.MetricNamed;
import org.commonjava.indy.model.core.ArtifactStore;
import org.commonjava.indy.model.core.StoreKey;
import org.commonjava.indy.subsys.infinispan.CacheProducer;
import org.commonjava.atlas.maven.ident.ref.ProjectRef;
import org.commonjava.maven.galley.event.EventMetadata;
import org.commonjava.maven.galley.io.checksum.ContentDigest;
import org.commonjava.maven.galley.io.checksum.TransferMetadata;
import org.commonjava.maven.galley.maven.spi.type.TypeMapper;
import org.commonjava.maven.galley.maven.util.ArtifactPathUtils;
import org.commonjava.maven.galley.model.Transfer;
import org.infinispan.manager.DefaultCacheManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import static java.lang.Boolean.TRUE;
import static org.apache.commons.lang.StringUtils.contains;
import static org.apache.commons.lang.StringUtils.isNotBlank;
import static org.apache.commons.lang.StringUtils.trimToEmpty;
import static org.commonjava.indy.measure.annotation.MetricNamed.DEFAULT;
import static org.commonjava.maven.galley.io.ChecksummingTransferDecorator.FORCE_CHECKSUM;

/**
 * Created by jdcasey on 1/4/17.
 *
 * Implements authoritative artifact checking using checksums against some authoritative repository. If no authoritative
 * store is configured, all builds are treated as authoritative and cleared for use.
 */
@ApplicationScoped
public class KojiBuildAuthority
{

    private static final List<String> EXCLUDED_FILE_ENDINGS = Collections.unmodifiableList(
            Arrays.asList( "-sources.zip", "-patches.zip", "-sources.jar", "-javadoc.jar" ) );

    @Inject
    private IndyKojiConfig config;

    @Inject
    private TypeMapper typeMapper;

    @Inject
    private IndyKojiContentProvider kojiContentProvider;

    @Inject
    private StoreDataManager storeDataManager;

    @Inject
    private ContentDigester contentDigester;

    @Inject
    private DirectContentAccess directContentAccess;

    protected KojiBuildAuthority(){}

    public KojiBuildAuthority( IndyKojiConfig config, TypeMapper typeMapper, KojiClient kojiClient,
                               StoreDataManager storeDataManager, ContentDigester contentDigester,
                               DirectContentAccess directContentAccess, DefaultCacheManager cacheManager )
    {
        this.config = config;
        this.typeMapper = typeMapper;
        this.kojiContentProvider = new IndyKojiContentProvider( kojiClient, new CacheProducer( null, cacheManager, null ) );
        this.storeDataManager = storeDataManager;
        this.contentDigester = contentDigester;
        this.directContentAccess = directContentAccess;
    }

    /**
     * Enum used to sort the order of preference for archives coming from Koji for a give build.
     */
    public enum TypePriority
    {
        jar,
        other,
        xml,
        pom;

        public static TypePriority get( String type )
        {
            for ( TypePriority tp : values() )
            {
                if ( tp.name().equalsIgnoreCase( type ) )
                {
                    return tp;
                }
            }

            return other;
        }
    }

    public boolean isAuthorized( String path, EventMetadata eventMetadata, ProjectRef ref, KojiBuildInfo build,
                                 KojiSessionInfo session )
            throws KojiClientException
    {
        return isAuthorized( path, eventMetadata, ref, build, session, new HashMap<>() );
    }

    @Measure( timers = @MetricNamed( DEFAULT ) )
    public boolean isAuthorized( String path, EventMetadata eventMetadata, ProjectRef ref, KojiBuildInfo build,
                                 KojiSessionInfo session, Map<Integer, KojiBuildArchiveCollection> seenBuildArchives )
            throws KojiClientException
    {
        ArtifactStore authoritativeStore = getAuthoritativeStore();

        if ( authoritativeStore != null )
        {
            KojiBuildArchiveCollection archiveCollection = seenBuildArchives.get( build.getId() );
            if ( archiveCollection == null )
            {
                List<KojiArchiveInfo> archiveList = kojiContentProvider.listArchivesForBuild( build.getId(), session );
                archiveCollection = new KojiBuildArchiveCollection( build, archiveList );
                seenBuildArchives.put( build.getId(), archiveCollection );
            }

            if ( archiveCollection == null )
            {
                throw new KojiClientException( "Failed to retrieve archives for build: %s", build );
            }

            // @formatter:off
            Predicate<KojiArchiveInfo> archiveInfoFilter = ( archive ) -> EXCLUDED_FILE_ENDINGS.stream()
                                                                                .allMatch( ending -> !archive.getFilename().endsWith( ending ) );
            List<KojiArchiveInfo> sortedArchives = archiveCollection.getArchives()
                                                                    .stream()
                                                                    // filter out excluded filename endings.
                                                                    .filter( archiveInfoFilter )
                                                                    // sort so jars are favored first, then poms, then everything else.
                                                                    .sorted( ( a1, a2 ) -> {
                                                                        TypePriority t1 = TypePriority.get( a1.getExtension() );
                                                                        TypePriority t2 = TypePriority.get( a2.getExtension() );

                                                                        return Integer.valueOf( t1.ordinal() )
                                                                                .compareTo( t2.ordinal() );
                                                                    } )
                                                                    // make a list of the result
                                                                    .collect( Collectors.toList() );
            // @formatter:on

            for ( KojiArchiveInfo archive : sortedArchives )
            {
                try
                {
                    if ( isMavenArtifact( archive ) )
                    {
                        // skip non-Maven artifacts
                        continue;
                    }

                    if ( containsPlaceholders( archive ) )
                    {
                        return false;
                    }

                    String artifactPath = ArtifactPathUtils.formatArtifactPath( archive.asArtifact(), typeMapper );
                    String md5 = checksumArtifact( authoritativeStore, artifactPath, eventMetadata );
                    if ( isNotBlank( md5 ) )
                    {
                        //FIXME: not sure if all koji archives are using md5 as checksum type for maven build
                        String kojiMd5 = archive.getChecksum();

                        Logger logger = LoggerFactory.getLogger( getClass() );
                        logger.info(
                                "Checking checksum for {} (path: {}) in auth store {}, auth store checksum:{}, koji build check sum:{}",
                                ref, path, authoritativeStore, md5, kojiMd5 );

                        if ( !md5.equals( kojiMd5 ) )
                        {
                            // if checksum is not the same, it means the artifact in koji is DIFFERENT from the one in the authoritative store. Reject this.
                            return false;
                        }
                    }
                }
                catch ( Exception e )
                {
                    Logger logger = LoggerFactory.getLogger( getClass() );
                    logger.error( "SHOULD NEVER HAPPEN: Failed to transform artifact to path: " + e.getMessage(), e );
                }
            }
        }

        return true;
    }

    /**
     * Checks if the given archive is a Maven artifact, i.e. has artifactId and groupId set. If one of those is
     * missing, it is not considered to be a Maven artifact.
     */
    private boolean isMavenArtifact( final KojiArchiveInfo archive )
    {
        return isNotBlank( archive.getArtifactId() ) && isNotBlank( archive.getGroupId() );
    }

    /**
     * Checks if the given archive GAV contains unreplaced placeholders, e.g. "${parent.version}" in the version field.
     */
    private boolean containsPlaceholders( final KojiArchiveInfo archive )
    {
        return contains( trimToEmpty( archive.getArtifactId() ), "${" )
                || contains( trimToEmpty( archive.getGroupId() ), "${" )
                || contains( trimToEmpty( archive.getVersion() ), "${" );
    }

    private ArtifactStore getAuthoritativeStore()
    {
        if ( config.getArtifactAuthorityStore() != null )
        {
            final StoreKey authStoreKey = StoreKey.fromString( config.getArtifactAuthorityStore() );
            try
            {
                return storeDataManager.getArtifactStore( authStoreKey );
            }
            catch ( IndyDataException e )
            {
                Logger logger = LoggerFactory.getLogger( getClass() );
                logger.warn( "Error occurred when finding authoritative store for {}, error message: {}", authStoreKey,
                             e.getMessage() );
            }
        }
        return null;
    }

    private String checksumArtifact( ArtifactStore store, String path, EventMetadata eventMetadata )
    {
        final Logger logger = LoggerFactory.getLogger( getClass() );
        try
        {
            if ( directContentAccess.exists( store, path ) )
            {
                String md5Path = path + ".md5";
                Transfer md5 = directContentAccess.retrieveRaw( store, md5Path, eventMetadata );
                if ( md5 != null && md5.exists() )
                {
                    try (InputStream in = md5.openInputStream( true ))
                    {
                        return IOUtils.toString( in ).trim();
                    }
                    catch ( IOException e )
                    {
                        logger.warn( "Error reading MD5 checksum for transfer of path {} in store {}, error is {}",
                                     md5Path, store, e.getMessage() );
                    }
                }
                else
                {
                    EventMetadata forcedEventMetadata = new EventMetadata( eventMetadata ).set(FORCE_CHECKSUM, TRUE);
                    final TransferMetadata artifactData = contentDigester.digest( store.getKey(), path, forcedEventMetadata );
                    if ( artifactData != null )
                    {
                        return artifactData.getDigests().get( ContentDigest.MD5 );
                    }
                }
            }
        }
        catch ( IndyWorkflowException e )
        {
            logger.warn( "Error happened when calculate md5 checksum for transfer of path {} in store {}, error is {}",
                         path, store, e.getMessage() );
        }

        return null;
    }
}


/**
 * Copyright (C) 2011-2018 Red Hat, Inc. (https://github.com/Commonjava/indy)
 *
 * 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.commonjava.indy.subsys.template;

import groovy.lang.Writable;
import groovy.text.GStringTemplateEngine;
import groovy.text.Template;

import java.io.IOException;
import java.io.StringWriter;
import java.net.URL;
import java.util.Map;

import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;

import org.codehaus.groovy.control.CompilationFailedException;
import org.commonjava.indy.subsys.datafile.DataFile;
import org.commonjava.indy.subsys.datafile.DataFileManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@ApplicationScoped
public class TemplatingEngine
{
    private final Logger logger = LoggerFactory.getLogger( getClass() );

    public static final String TEMPLATES = "templates";

    @Inject
    private DataFileManager manager;

    private final GStringTemplateEngine engine;

    protected TemplatingEngine()
    {
        engine = new GStringTemplateEngine();
    }

    public TemplatingEngine( final GStringTemplateEngine engine, final DataFileManager manager )
    {
        this.engine = engine;
        this.manager = manager;
    }

    public String render( final String templateKey, final Map<String, Object> params )
        throws IndyGroovyException
    {
        return render( null, templateKey, params );
    }

    public String render( final String acceptHeader, final String templateKey, final Map<String, Object> params )
        throws IndyGroovyException
    {
        final Template template = getTemplate( acceptHeader, templateKey );

        final Writable output = template.make( params );

        final StringWriter writer = new StringWriter();
        try
        {
            output.writeTo( writer );
        }
        catch ( final IOException e )
        {
            throw new IndyGroovyException( "Failed to render template: %s for addMetadata: %s. Reason: %s", e, templateKey,
                                            acceptHeader, e.getMessage() );
        }

        return writer.toString();
    }

    // TODO Cache these...though this will hurt hot-reloading. Perhaps a debug mode configuration?
    private Template getTemplate( final String acceptHeader, final String templateKey )
        throws IndyGroovyException
    {
        final String accept = ( acceptHeader == null ? "" : acceptHeader.replace( '/', '_' ) + "/" );
        try
        {
            final String filename = accept + templateKey + ".groovy";
            final DataFile templateFile = manager.getDataFile( TEMPLATES, filename );
            logger.info( "Looking for template: {} for ACCEPT header: {} in: {}", templateKey, acceptHeader,
                         templateFile );

            Template template;
            if ( templateFile.exists() && !templateFile.isDirectory() )
            {
                template = engine.createTemplate( templateFile.readString() );
            }
            else
            {
                final String urlpath = TEMPLATES + "/" + accept + templateKey + ".groovy";
                logger.info( "Looking for template: {} for ACCEPT header: {} in: {}", templateKey, acceptHeader,
                             urlpath );

                final URL u = Thread.currentThread()
                                    .getContextClassLoader()
                                    .getResource( urlpath );

                template = u == null ? null : engine.createTemplate( u );
            }

            if ( template == null )
            {
                throw new IndyGroovyException( "Failed to locate template: %s (with ACCEPT header: %s)", templateKey,
                                                acceptHeader );
            }

            return template;
        }
        catch ( final CompilationFailedException e )
        {
            throw new IndyGroovyException( "Failed to compile template: %s. Reason: %s", e, templateKey, e.getMessage() );
        }
        catch ( final ClassNotFoundException e )
        {
            throw new IndyGroovyException( "Failed to compile template: %s. Reason: %s", e, templateKey, e.getMessage() );
        }
        catch ( final IOException e )
        {
            throw new IndyGroovyException( "Failed to read template: %s. Reason: %s", e, templateKey, e.getMessage() );
        }
    }
}


/**
 * Copyright (C) 2011-2018 Red Hat, Inc. (https://github.com/Commonjava/indy)
 *
 * 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.commonjava.indy.util;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.commonjava.indy.content.StoreResource;

public final class ContentUtils
{

    private ContentUtils()
    {
    }

    /**
     * Attempt to remove duplicates, even "fuzzy" ones where a directory is
     * listed with trailing '/' in some cases but not others.
     */
    public static List<StoreResource> dedupeListing( final List<StoreResource> listing )
    {
        final List<StoreResource> result = new ArrayList<StoreResource>();
        final Map<String, StoreResource> mapping = new LinkedHashMap<String, StoreResource>();
        for ( final StoreResource res : listing )
        {
            final String path = res.getPath();
            if ( mapping.containsKey( path ) )
            {
                continue;
            }

            if ( mapping.containsKey( path + "/" ) )
            {
                continue;
            }

            mapping.put( path, res );
            result.add( res );
        }

        return result;
    }

}


/**
 * Copyright (C) 2011-2018 Red Hat, Inc. (https://github.com/Commonjava/indy)
 *
 * 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.commonjava.indy.core.change;

import org.commonjava.cdi.util.weft.DrainingExecutorCompletionService;
import org.commonjava.cdi.util.weft.ExecutorConfig;
import org.commonjava.cdi.util.weft.WeftExecutorService;
import org.commonjava.cdi.util.weft.WeftManaged;
import org.commonjava.indy.IndyWorkflowException;
import org.commonjava.indy.change.event.ArtifactStoreDeletePreEvent;
import org.commonjava.indy.change.event.ArtifactStoreEnablementEvent;
import org.commonjava.indy.change.event.ArtifactStorePreUpdateEvent;
import org.commonjava.indy.change.event.ArtifactStoreUpdateType;
import org.commonjava.indy.change.event.IndyStoreEvent;
import org.commonjava.indy.content.DirectContentAccess;
import org.commonjava.indy.content.StoreContentAction;
import org.commonjava.indy.data.IndyDataException;
import org.commonjava.indy.data.StoreDataManager;
import org.commonjava.indy.measure.annotation.Measure;
import org.commonjava.indy.measure.annotation.MetricNamed;
import org.commonjava.indy.model.core.ArtifactStore;
import org.commonjava.indy.model.core.Group;
import org.commonjava.indy.model.core.StoreKey;
import org.commonjava.indy.util.LocationUtils;
import org.commonjava.maven.galley.model.SpecialPathInfo;
import org.commonjava.maven.galley.model.Transfer;
import org.commonjava.maven.galley.spi.io.SpecialPathManager;
import org.commonjava.maven.galley.spi.nfc.NotFoundCache;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.event.Observes;
import javax.enterprise.inject.Instance;
import javax.inject.Inject;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

import static org.commonjava.indy.measure.annotation.MetricNamed.DEFAULT;
import static org.commonjava.indy.model.core.StoreType.group;
import static org.commonjava.maven.galley.util.PathUtils.ROOT;

/**
 * Created by jdcasey on 1/27/17.
 */
@ApplicationScoped
public class StoreContentListener
{

    private final Logger logger = LoggerFactory.getLogger( getClass() );

    @Inject
    private Instance<StoreContentAction> storeContentActions;

    @Inject
    private StoreDataManager storeDataManager;

    @Inject
    private SpecialPathManager specialPathManager;

    @Inject
    private DirectContentAccess directContentAccess;

    @Inject
    private NotFoundCache nfc;

    @Inject
    @WeftManaged
    @ExecutorConfig( threads=20, priority=7, named="content-cleanup" )
    private WeftExecutorService cleanupExecutor;

    /**
     * Handles store disable/enablement.
     */
    @Measure
    public void onStoreEnablement( @Observes final ArtifactStoreEnablementEvent event )
    {
        Logger logger = LoggerFactory.getLogger( getClass() );
        logger.trace( "Got store-enablement event: {}", event );

        if ( event.isPreprocessing() )
        {
            if ( event.isDisabling() )
            {
                /* For disablement, we remove the merged metadata files by paths via listing mergable files in the target repo's local cache.
                 * We do not cache pom/jar files in affected groups so don't worry about them.
                 */
                processAllPaths( event, mergablePathStrings(), false );
            }
            else
            {
                /* When enabling a repo, we have to clean all (mergable) content from containing groups' cache because
                 * we don't have complete listing of the enabled repo and listing it is very slow. The only option how to keep
                 * the cache consistent is to drop everything from it.
                 */
                processAllPathsExt( event, mergablePathStrings() );
            }
        }
    }

    @Measure( timers = @MetricNamed( DEFAULT ) )
    public void onStoreDeletion( @Observes final ArtifactStoreDeletePreEvent event )
    {
        Logger logger = LoggerFactory.getLogger( getClass() );
        logger.trace( "Got store-delete event: {}", event );

        processAllPaths( event, p->true, true );
    }

    @Measure( timers = @MetricNamed( DEFAULT ) )
    public void onStoreUpdate( @Observes final ArtifactStorePreUpdateEvent event )
    {
        Logger logger = LoggerFactory.getLogger( getClass() );
        logger.trace( "Got store-update event: {}", event );

        // we're only interested in existing stores, since new stores cannot have indexed keys
        if ( ArtifactStoreUpdateType.UPDATE == event.getType() )
        {
            for ( ArtifactStore store : event )
            {
                removeAllSupercededMemberContent( store, event.getChangeMap() );
            }
        }
    }

    // TODO: If we find points where a new HostedRepository is added, we should be using its comprehensive index to minimize the index damage to the group.
    private void removeAllSupercededMemberContent( final ArtifactStore store,
                                                   final Map<ArtifactStore, ArtifactStore> changeMap )
    {
        StoreKey key = store.getKey();
        // we're only interested in groups, since only adjustments to group memberships can invalidate indexed content.
        if ( group == key.getType() )
        {
            List<StoreKey> newMembers = ( (Group) store ).getConstituents();
            logger.debug( "New members of: {} are: {}", store, newMembers );

            Group group = (Group) changeMap.get( store );
            List<StoreKey> oldMembers = group.getConstituents();
            logger.debug( "Old members of: {} are: {}", group, oldMembers );

            int commonSize = Math.min( newMembers.size(), oldMembers.size() );
            int divergencePoint;

            // look in the members that overlap in new/old groups and see if there are changes that would
            // indicate member reordering. If so, it might lead previously suppressed results to be prioritized,
            // which would invalidate part of the content index for the group.
            boolean foundDivergence = false;
            for ( divergencePoint = 0; divergencePoint < commonSize; divergencePoint++ )
            {
                logger.debug( "Checking for common member at index: {}", divergencePoint );
                if ( !oldMembers.get( divergencePoint ).equals( newMembers.get( divergencePoint ) ) )
                {
                    foundDivergence = true;
                    break;
                }
            }

            // [NOS-128]
            // 1. If membership has shrunk, we can remove origin-indexed paths, which will remove merged group content
            //      based on the removed member's content.
            // 2. If membership has grown, we should iterate new members' indexed content looking for mergable paths.
            //      For each of these, we need to removeIndexedStorePaths using the group and the mergable path.
            // [addendum]
            // 3. If membership is the same size but has been reordered, we need to iterate from the divergence point
            //    and invalidate the non-mergable files. This is because the reordering may change what artifacts
            //    should obscure which other physical artifacts.
            //
            // NOTE: In any case, once we isolate the changes, we need to handle matches in two ways:
            // 1. deleteTransfers()
            // 2. add the indexedStorePath to the removed Set so we can propagage their removal through any groups
            //      that include the one we're affecting directly here...using clearIndexedPathFrom() to do this.
            if ( !foundDivergence )
            {
                if ( newMembers.size() < oldMembers.size() )
                {
                    divergencePoint = commonSize;
                }
                else
                {
                    divergencePoint = newMembers.size();
                }
            }

            logger.debug( "group membership divergence point: {}", divergencePoint );

            Set<StoreKey> affectedMembers = new HashSet<>();
            boolean removeMergableOnly = divergencePoint >= oldMembers.size();

            // if we can iterate some old members that have been removed or reordered, invalidate the
            // group content index entries for those.
            if ( divergencePoint < oldMembers.size() )
            {
                for ( int i = divergencePoint; i < oldMembers.size(); i++ )
                {
                    affectedMembers.add( oldMembers.get( i ) );
                }
            }
            else
            {
                // for new added members, need to clear the indexed path with this group store for repo metadata merging
                // See [NOS-128]
                for ( int i = divergencePoint - 1; i >= commonSize; i-- )
                {
                    affectedMembers.add( newMembers.get( i ) );
                }
            }

            logger.debug( "Got members affected by membership divergence: {}", affectedMembers );
            if ( !affectedMembers.isEmpty() )
            {
                Set<Group> groups = new HashSet<>();
                groups.add( group );

                try
                {
                    groups.addAll( storeDataManager.query()
                                                   .packageType( group.getPackageType() )
                                                   .getGroupsAffectedBy( group.getKey() ) );
                }
                catch ( IndyDataException e )
                {
                    logger.error( String.format( "Cannot retrieve groups affected by: %s. Reason: %s", group.getKey(),
                                                 e.getMessage() ), e );
                }

                logger.debug( "Got affected groups: {}", groups );

                DrainingExecutorCompletionService<Integer> clearService =
                        new DrainingExecutorCompletionService<>( cleanupExecutor );

                final Predicate<? super String> mergableFilter = removeMergableOnly ? mergablePathStrings() : ( p ) -> true;

                // NOTE: We're NOT checking load for this executor, since this is an async process that is critical to
                // data integrity.
                affectedMembers.forEach( ( memberKey ) -> {
                    logger.debug( "Listing all {}paths in: {}", ( removeMergableOnly ? "mergeable " : "" ), memberKey );
                    listPathsAnd( memberKey, mergableFilter, p -> clearService.submit(
                            clearPathProcessor( p, memberKey, groups ) ) );

                } );

                drainAndCount( clearService, "store: " + store.getKey() );
            }
        }
    }

    private Callable<Integer> clearPathProcessor( final String path, final StoreKey key, final Set<Group> groups )
    {
        try
        {
            ArtifactStore store = storeDataManager.getArtifactStore( key );
            return clearPathProcessor( path, store, groups );
        }
        catch ( IndyDataException e )
        {
            logger.error( "Cannot clear paths for missing / inaccessible store: " + key, e );
        }

        return ()->0;
    }

    private Callable<Integer> clearPathProcessor( final String path, final ArtifactStore store, final Set<Group> groups )
    {
        return () -> {
            logger.debug( "Got mergable transfer from diverged portion of membership: {}", path );

            int cleared = clearPath( path, store, groups, false );
            return cleared;
        };
    }

    private int clearPath( String path, ArtifactStore origin, Set<Group> affectedGroups, boolean deleteOriginPath )
    {
        Logger logger = LoggerFactory.getLogger( getClass() );
        final boolean isReadonlyHosted = storeDataManager.isReadonly( origin );
        AtomicInteger cleared = new AtomicInteger( 0 );
        if ( deleteOriginPath && !isReadonlyHosted )
        {
            try
            {
                if ( delete( directContentAccess.getTransfer( origin, path ) ) )
                {
                    nfc.clearMissing( LocationUtils.toLocation( origin ) ); // clear NFC for this group
                    cleared.incrementAndGet();
                }
            }
            catch ( IndyWorkflowException e )
            {
                logger.error( String.format( "Failed to retrieve transfer for: %s in origin store: %s. Reason: %s", path,
                                             origin.getKey(), e.getMessage() ), e );
            }
        }

        affectedGroups.forEach( g -> {
            try
            {
                Transfer gt = directContentAccess.getTransfer( g, path );
                if ( delete( gt ) )
                {
                    cleared.incrementAndGet();
                }
            }
            catch ( IndyWorkflowException e )
            {
                logger.error( String.format( "Failed to retrieve transfer for: %s in group: %s. Reason: %s", path,
                                             g.getName(), e.getMessage() ), e );
            }
        } );

        logger.debug( "Clearing content via supplemental store-content actions..." );
        StreamSupport.stream( storeContentActions.spliterator(), false )
                     .forEach(
                             action -> action.clearStoreContent( path, origin, affectedGroups, deleteOriginPath ) );
        logger.debug( "All store-content actions done executing." );

        return cleared.get();
    }

    private boolean delete( Transfer t )
    {
        Logger logger = LoggerFactory.getLogger( getClass() );
        if ( t != null && t.exists() )
        {
            try
            {
                logger.debug( "Deleting: {}", t );
                boolean deleted = t.delete( true );
                if ( t.exists() )
                {
                    logger.error( "{} WAS NOT DELETED!", t );
                }

                return deleted;
            }
            catch ( IOException e )
            {
                logger.error( String.format( "Failed to delete: %s. Reason: %s", t, e.getMessage() ), e );
            }
        }

        return false;
    }

    private void listPathsAnd( StoreKey key, Predicate<? super String> pathFilter, Consumer<String> pathAction )
    {
        Logger logger = LoggerFactory.getLogger( getClass() );

        Transfer root = null;
        try
        {
            root = directContentAccess.getTransfer( key, ROOT );

//            root.lockWrite();

            List<Transfer> toProcess = new ArrayList<>();
            toProcess.add( root );
            while ( !toProcess.isEmpty() )
            {
                Transfer next = toProcess.remove( 0 );
                try
                {
                    Stream.of(next.list()).forEach( filename->{
                        Transfer t = next.getChild( filename );
                        if ( t.isDirectory() )
                        {
                            logger.debug( "Adding directory path for processing: {}", t.getPath() );
                            toProcess.add( t );
                        }
                        else
                        {
                            logger.trace( "Testing file path: {}", t.getPath() );
                            if( pathFilter.test( t.getPath() ) )
                            {
                                logger.trace( "Adding file path to results: {}", t.getPath() );
                                pathAction.accept( t.getPath() );
                            }
                            else
                            {
                                logger.trace( "Skipping file path: {}", t.getPath() );
                            }
                        }
                    } );
                }
                catch ( IOException e )
                {
                    logger.error( String.format( "Failed to list contents of: %s. Reason: %s", next, e ), e );
                }
            }
        }
        catch ( IndyWorkflowException  e )
        {
            logger.error( String.format( "Failed to retrieve root directory reference for: %s. Reason: %s", key, e ), e );
        }
        finally
        {
            if ( root != null )
            {
                root.unlock();
            }
        }
    }

    /**
     * List the mergable paths in target store and clean up the paths in affected groups.
     * @param event
     * @param pathFilter
     * @param deleteOriginPath
     */
    private void processAllPaths( final IndyStoreEvent event, Predicate<? super String> pathFilter, boolean deleteOriginPath )
    {
        DrainingExecutorCompletionService<Integer> clearService =
                new DrainingExecutorCompletionService<>( cleanupExecutor );

        Set<StoreKey> keys = event.getStores().stream().map( store -> store.getKey() ).collect( Collectors.toSet() );
        keys.forEach(key->{
            final Set<Group> groups = new HashSet<>();
            try
            {
                groups.addAll(
                        storeDataManager.query().packageType( key.getPackageType() ).getGroupsAffectedBy( key ) );

                listPathsAnd( key, pathFilter, p->clearService.submit(clearPathProcessor( p, key, groups )) );
            }
            catch ( IndyDataException e )
            {
                logger.error( "Failed to retrieve groups affected by: " + key, e );
            }
        } );

        drainAndCount( clearService, "stores: " + keys );
    }

    private int drainAndCount( final DrainingExecutorCompletionService<Integer> clearService, final String description )
    {
        AtomicInteger count = new AtomicInteger( 0 );
        try
        {
            clearService.drain( partialCount -> count.addAndGet( partialCount ) );
        }
        catch ( InterruptedException | ExecutionException e )
        {
            logger.error( "Failed to clear paths related to change in " + description, e );
        }

        logger.debug( "Cleared {} paths for changes in {}", count.get(), description );

        return count.get();
    }

    /**
     * Extensive version for clean up paths. This will clean all mergable files in affected groups regardless whether
     * the path is in the target store cache or not. It also clears NFC.
     * @param event
     * @param pathFilter
     */
    private void processAllPathsExt( final IndyStoreEvent event, Predicate<? super String> pathFilter )
    {
        DrainingExecutorCompletionService<Integer> clearService =
                new DrainingExecutorCompletionService<>( cleanupExecutor );

        Set<ArtifactStore> stores = new HashSet<>( event.getStores() );
        stores.forEach(store->{
            final StoreKey key = store.getKey();
            try
            {
                Set<Group> groups =
                                storeDataManager.query().packageType( key.getPackageType() ).getGroupsAffectedBy( key );
                if ( store instanceof Group )
                {
                    groups.add( (Group) store );
                }

                groups.forEach( g->{
                    listPathsAnd( key, pathFilter, p->clearService.submit(clearPathProcessor( p, key, groups )) );
                } );

                nfc.clearMissing( LocationUtils.toLocation( store ) ); // clear NFC for this store
            }
            catch ( IndyDataException e )
            {
                e.printStackTrace();
            }
        } );

        Set<StoreKey> keys = event.getStores().stream().map( s -> s.getKey() ).collect( Collectors.toSet() );
        drainAndCount( clearService, "stores: " + keys );
    }

    private Predicate<? super String> mergablePathStrings()
    {
        return ( path ) -> {
            // here we care if it's mergable or not, since this may be triggered by a new file being stored.
            SpecialPathInfo specialPathInfo = specialPathManager.getSpecialPathInfo( path );
            return ( specialPathInfo != null && specialPathInfo.isMergable() );
        };
    }

}