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

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.KojiBuildInfo;
import com.redhat.red.build.koji.model.xmlrpc.KojiSessionInfo;
import org.commonjava.atlas.maven.ident.ref.ArtifactRef;
import org.commonjava.atlas.maven.ident.ref.SimpleArtifactRef;
import org.commonjava.cdi.util.weft.DrainingExecutorCompletionService;
import org.commonjava.cdi.util.weft.ExecutorConfig;
import org.commonjava.cdi.util.weft.SingleThreadedExecutorService;
import org.commonjava.cdi.util.weft.WeftExecutorService;
import org.commonjava.cdi.util.weft.WeftManaged;
import org.commonjava.indy.IndyWorkflowException;
import org.commonjava.indy.audit.ChangeSummary;
import org.commonjava.indy.data.IndyDataException;
import org.commonjava.indy.data.StoreDataManager;
import org.commonjava.indy.koji.conf.IndyKojiConfig;
import org.commonjava.indy.koji.content.IndyKojiContentProvider;
import org.commonjava.indy.koji.content.KojiPathPatternFormatter;
import org.commonjava.indy.koji.model.KojiMultiRepairResult;
import org.commonjava.indy.koji.model.KojiRepairRequest;
import org.commonjava.indy.koji.model.KojiRepairResult;
import org.commonjava.indy.koji.util.KojiUtils;
import org.commonjava.indy.model.core.ArtifactStore;
import org.commonjava.indy.model.core.Group;
import org.commonjava.indy.model.core.RemoteRepository;
import org.commonjava.indy.model.core.StoreKey;
import org.commonjava.indy.model.core.StoreType;
import org.commonjava.maven.galley.event.EventMetadata;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;

import static org.commonjava.indy.core.ctl.PoolUtils.detectOverloadVoid;
import static org.commonjava.indy.koji.content.KojiContentManagerDecorator.CREATION_TRIGGER_GAV;
import static org.commonjava.indy.koji.model.IndyKojiConstants.KOJI_ORIGIN;
import static org.commonjava.indy.koji.model.IndyKojiConstants.KOJI_ORIGIN_BINARY;
import static org.commonjava.indy.model.core.StoreType.group;
import static org.commonjava.indy.model.core.StoreType.remote;

/**
 * Component responsible for repair Koji remote repositories.
 *
 * @author ruhan
 */
@ApplicationScoped
public class KojiRepairManager
{
    private final Logger logger = LoggerFactory.getLogger( getClass() );

    public static final String METADATA_KOJI_BUILD_ID = "koji-build-id"; // metadata key for recording koji build id

    @Inject
    private IndyKojiConfig config;

    @Inject
    private StoreDataManager storeManager;

    @Inject
    private IndyKojiContentProvider kojiCachedClient;

    @Inject
    private KojiPathPatternFormatter kojiPathFormatter;

    @Inject
    private KojiUtils kojiUtils;

    @Inject
    @WeftManaged
    @ExecutorConfig( named="koji-repairs", threads=50, priority = 3, loadSensitive = ExecutorConfig.BooleanLiteral.TRUE, maxLoadFactor = 100)
    private WeftExecutorService repairExecutor;

    private ReentrantLock opLock = new ReentrantLock(); // operations are synchronized

    protected KojiRepairManager()
    {
    }

    public KojiRepairManager( final StoreDataManager storeManager, final IndyKojiConfig config,
                              final KojiClient kojiClient )
    {
        this.storeManager = storeManager;
        this.config = config;
        this.kojiCachedClient = new IndyKojiContentProvider( kojiClient, null );
        this.repairExecutor = new SingleThreadedExecutorService( "koji-repairs" );
    }

    public KojiMultiRepairResult repairAllPathMasks( final String user )
            throws KojiRepairException, IndyWorkflowException
    {
        KojiMultiRepairResult result = new KojiMultiRepairResult();

        if ( opLock.tryLock() )
        {
            try
            {
                List<RemoteRepository> kojiRemotes = getAllKojiRemotes();

                DrainingExecutorCompletionService<KojiRepairResult> repairService =
                        new DrainingExecutorCompletionService<>( repairExecutor );

                detectOverloadVoid( () -> kojiRemotes.forEach( r -> repairService.submit( () -> {
                    logger.info( "Attempting to repair path masks in Koji remote: {}", r.getKey() );

                    KojiRepairRequest request = new KojiRepairRequest( r.getKey(), false );
                    try
                    {
                        return repairPathMask( request, user, true );
                    }
                    catch ( KojiRepairException e )
                    {
                        logger.error( "Failed to execute repair for: " + r.getKey(), e );
                    }

                    return null;
                } ) ) );

                List<KojiRepairResult> results = new ArrayList<>();
                try
                {
                    repairService.drain( r -> {
                        if ( r != null )
                        {
                            results.add( r );
                        }
                    } );
                }
                catch ( InterruptedException | ExecutionException e )
                {
                    logger.error( "Failed to repair path masks.", e );
                }

                result.setResults( results );
            }
            catch ( IndyDataException e )
            {
                throw new KojiRepairException( "Failed to list Koji remote repositories for repair. Reason: %s", e, e.getMessage() );
            }
            finally
            {
                opLock.unlock();
            }
        }
        else
        {
            throw new KojiRepairException( "Koji repair manager is busy." );
        }

        return result;
    }

    public KojiRepairResult repairPathMask( KojiRepairRequest request, String user )
            throws KojiRepairException
    {
        return repairPathMask( request, user, false );
    }

    public KojiRepairResult repairPathMask( KojiRepairRequest request, String user, boolean skipLock )
        throws KojiRepairException
    {
        KojiRepairResult ret = new KojiRepairResult( request );

        if ( skipLock || opLock.tryLock() )
        {
            try
            {
                ArtifactStore store = getRequestedStore(request, ret);

                if ( store == null )
                {
                    return ret;
                }

                store = store.copyOf();

                StoreKey remoteKey = request.getSource();

                if ( remoteKey.getType() == remote )
                {
                    final String nvr = kojiUtils.getBuildNvr( remoteKey );
                    if ( nvr == null )
                    {
                        String error = String.format( "Not a koji store: %s", remoteKey );
                        return ret.withError( error );
                    }

                    try
                    {
                        KojiSessionInfo session = null;
                        KojiBuildInfo build = kojiCachedClient.getBuildInfo( nvr, session );

                        List<KojiArchiveInfo> archives = kojiCachedClient.listArchivesForBuild( build.getId(), session );

                        ArtifactRef artifactRef = SimpleArtifactRef.parse( store.getMetadata( CREATION_TRIGGER_GAV ) );
                        if ( artifactRef == null )
                        {
                            String error = String.format(
                                    "Koji remote repository: %s does not have %s metadata. Cannot retrieve accurate path masks.",
                                    remoteKey, CREATION_TRIGGER_GAV );
                            return ret.withError( error );
                        }

                        // set pathMaskPatterns using build output paths
                        Set<String> patterns = kojiPathFormatter.getPatterns( artifactRef, archives, true );
                        logger.debug( "For repo: {}, resetting path_mask_patterns to:\n\n{}\n\n", store.getKey(),
                                     patterns );

                        KojiRepairResult.RepairResult repairResult = new KojiRepairResult.RepairResult( remoteKey );
                        repairResult.withPropertyChange( "path_mask_patterns", store.getPathMaskPatterns(), patterns );

                        ret.withResult( repairResult );

                        store.setPathMaskPatterns( patterns );

                        final ChangeSummary changeSummary = new ChangeSummary( user,
                                                                               "Repairing remote repository path masks to Koji build: "
                                                                                + build.getNvr() );

                        storeManager.storeArtifactStore( store, changeSummary, false, true, new EventMetadata() );
                    }
                    catch ( KojiClientException e )
                    {
                        String error = String.format( "Cannot getBuildInfo: %s, error: %s", remoteKey, e );
                        logger.debug( error, e );
                        return ret.withError( error, e );
                    }
                    catch ( IndyDataException e )
                    {
                        String error = String.format( "Failed to store changed remote repository: %s, error: %s", remoteKey, e );
                        logger.debug( error, e );
                        return ret.withError( error, e );
                    }
                }
                else
                {
                    String error = String.format( "Not a remote koji repository: %s", remoteKey );
                    return ret.withError( error );
                }
            }
            finally
            {
                if ( !skipLock )
                {
                    opLock.unlock();
                }
            }
        }
        else
        {
            throw new KojiRepairException( "Koji repair manager is busy." );
        }

        return ret;
    }

    public KojiRepairResult repairVol( KojiRepairRequest request, String user, String baseUrl )
                    throws KojiRepairException, KojiClientException
    {
        boolean flag = opLock.tryLock();
        if ( flag )
        {
            try
            {
                StoreKey storeKey = request.getSource();

                KojiRepairResult ret = new KojiRepairResult( request );

                ArtifactStore store;
                try
                {
                    store = storeManager.getArtifactStore( storeKey );
                }
                catch ( IndyDataException e )
                {
                    String error = String.format( "Cannot get store: %s, error: %s", storeKey, e );
                    logger.warn( error, e );
                    return ret.withError( error );
                }

                if ( store == null )
                {
                    String error = String.format( "No such store: %s.", storeKey );
                    return ret.withError( error );
                }

                if ( storeKey.getType() == group )
                {
                    return repairGroupVol( request, (Group) store, user );
                }
                else if ( storeKey.getType() == remote )
                {
                    return repairRemoteRepositoryVol( request, (RemoteRepository) store, user );
                }
                else
                {
                    String error = String.format( "Not a group or remote koji store: %s", storeKey );
                    return ret.withError( error );
                }
            }
            finally
            {
                opLock.unlock();
            }
        }
        else
        {
            throw new KojiRepairException( "opLock held by other" );
        }
    }

    private KojiRepairResult repairGroupVol( KojiRepairRequest request, Group group, String user )
                    throws KojiClientException
    {
        KojiRepairResult ret = new KojiRepairResult( request );

        List<StoreKey> stores = group.getConstituents();
        if ( stores.isEmpty() )
        {
            return ret.withNoChange( group.getKey() );
        }

        KojiSessionInfo session = null;

        List<Object> args = new ArrayList<>(  );
        stores.forEach( storeKey -> {
            String nvr = kojiUtils.getBuildNvr( storeKey );
            if ( nvr != null )
            {
                args.add( nvr );
            }
            else
            {
                ret.withIgnore( storeKey );
            }
        } );
        List<KojiBuildInfo> buildInfoList = kojiCachedClient.getBuildInfo( args, session );

        buildInfoList.forEach( buildInfo -> {
            try
            {
                KojiRepairResult.RepairResult repairResult =
                                doRepair( group.getPackageType(), null, buildInfo, user, request.isDryRun() );
                ret.withResult( repairResult );
            }
            catch ( KojiRepairException e )
            {
                // we do not fail the whole attempt if one store failed
                logger.debug( "Repair failed", e );
                ret.withResult( new KojiRepairResult.RepairResult( e.getStoreKey(), e ) );
            }
        } );

        return ret;
    }

    private KojiRepairResult repairRemoteRepositoryVol( KojiRepairRequest request, RemoteRepository repository,
                                                     String user ) throws KojiRepairException
    {
        StoreKey storeKey = repository.getKey();

        KojiRepairResult ret = new KojiRepairResult( request );

        final String nvr = kojiUtils.getBuildNvr( storeKey );
        if ( nvr == null )
        {
            String error = String.format( "Not a koji store: %s", storeKey );
            return ret.withError( error );
        }

        KojiBuildInfo buildInfo;
        try
        {
            KojiSessionInfo session = null;
            buildInfo = kojiCachedClient.getBuildInfo( nvr, session );
        }
        catch ( KojiClientException e )
        {
            String error = String.format( "Cannot getBuildInfo: %s, error: %s", storeKey, e );
            logger.debug( error, e );
            return ret.withError( error, e );
        }

        KojiRepairResult.RepairResult repairResult =
                        doRepair( repository.getPackageType(), repository, buildInfo, user,
                                  request.isDryRun() );

        return ret.withResult( repairResult );
    }

    /**
     * Repair one remote repository.
     * @param packageType
     * @param repository repository to be repaired. If null, the repository name will be retrieved according to
     *                         the buildInfo and the naming format in koji.conf.
     * @param buildInfo koji build which this repository proxies
     * @param user the user does the repair
     * @param isDryRun
     * @return
     * @throws KojiRepairException
     */
    private KojiRepairResult.RepairResult doRepair( String packageType, RemoteRepository repository,
                                                    KojiBuildInfo buildInfo, String user, boolean isDryRun )
                    throws KojiRepairException
    {
        StoreKey storeKey;
        if ( repository != null )
        {
            storeKey = repository.getKey();
        }
        else
        {
            String name = kojiUtils.getRepositoryName( buildInfo );
            storeKey = new StoreKey( packageType, StoreType.remote, name );
            try
            {
                repository = (RemoteRepository) storeManager.getArtifactStore( storeKey );
            }
            catch ( IndyDataException e )
            {
                throw new KojiRepairException( "Cannot get store: %s. Reason: %s", e, storeKey,
                                               e.getMessage() );
            }
        }

        KojiRepairResult.RepairResult repairResult = new KojiRepairResult.RepairResult( storeKey );

        String url = repository.getUrl();

        String newUrl;
        try
        {
            newUrl = kojiUtils.formatStorageUrl( config.getStorageRootUrl(), buildInfo ); // volume is involved
        }
        catch ( MalformedURLException e )
        {
            throw new KojiRepairException( "Failed to format storage Url: %s. Reason: %s", e, storeKey,
                                           e.getMessage() );
        }

        boolean changed = !url.equals( newUrl );
        if ( changed )
        {
            repairResult.withPropertyChange( "url", url, newUrl );

            if ( !isDryRun )
            {
                ChangeSummary changeSummary = new ChangeSummary( user,
                                                                 "Repair " + storeKey + " url with volume: " + buildInfo
                                                                                .getVolumeName() );
                repository.setUrl( newUrl );
                repository.setMetadata( METADATA_KOJI_BUILD_ID, Integer.toString( buildInfo.getId() ) );
                boolean fireEvents = false;
                boolean skipIfExists = false;
                try
                {
                    storeManager.storeArtifactStore( repository, changeSummary, skipIfExists, fireEvents, new EventMetadata() );
                }
                catch ( IndyDataException e )
                {
                    throw new KojiRepairException( "Failed to repair store: %s. Reason: %s", e, storeKey, e.getMessage() );
                }
            }
        }
        return repairResult;
    }

    public KojiMultiRepairResult repairAllMetadataTimeout( final String user, boolean isDryRun )
            throws KojiRepairException, IndyWorkflowException
    {
        KojiMultiRepairResult result = new KojiMultiRepairResult();

        if ( opLock.tryLock() )
        {
            try
            {
                List<RemoteRepository> kojiRemotes = getAllKojiRemotes();

                DrainingExecutorCompletionService<KojiRepairResult> repairService =
                        new DrainingExecutorCompletionService<>( repairExecutor );

                detectOverloadVoid( () -> kojiRemotes.forEach( r -> repairService.submit( () -> {
                    logger.info( "Attempting to repair path masks in Koji remote: {}", r.getKey() );

                    KojiRepairRequest request = new KojiRepairRequest( r.getKey(), isDryRun );
                    try
                    {
                        return repairMetadataTimeout( request, user, true );
                    }
                    catch ( KojiRepairException e )
                    {
                        logger.error( "Failed to execute repair for: " + r.getKey(), e );
                    }

                    return null;
                } ) ) );

                List<KojiRepairResult> results = new ArrayList<>();
                try
                {
                    repairService.drain( r -> {
                        if ( r != null )
                        {
                            results.add( r );
                        }
                    } );
                }
                catch ( InterruptedException | ExecutionException e )
                {
                    logger.error( "Failed to repair metadata timeout.", e );
                }

                result.setResults( results );
            }
            catch ( IndyDataException e )
            {
                throw new KojiRepairException( "Failed to list Koji remote repositories for repair. Reason: %s", e, e.getMessage() );
            }
            finally
            {
                opLock.unlock();
            }
        }
        else
        {
            throw new KojiRepairException( "Koji repair manager is busy." );
        }

        return result;
    }

    public KojiRepairResult repairMetadataTimeout( KojiRepairRequest request, String user, boolean skipLock ) throws KojiRepairException{
        KojiRepairResult ret = new KojiRepairResult( request );

        if ( skipLock || opLock.tryLock() )
        {
            try
            {
                ArtifactStore store = getRequestedStore(request, ret);

                if ( store == null )
                {
                    return ret;
                }

                store = store.copyOf();

                StoreKey remoteKey = request.getSource();
                if ( remoteKey.getType() == remote )
                {
                    final String nvr = kojiUtils.getBuildNvr( remoteKey );
                    if ( nvr == null )
                    {
                        String error = String.format( "Not a koji store: %s", remoteKey );
                        return ret.withError( error );
                    }

                    try
                    {
                        final int NEVER_TIMEOUT_VALUE = -1;
                        if ( !request.isDryRun() )
                        {
                            ( (RemoteRepository) store ).setMetadataTimeoutSeconds( NEVER_TIMEOUT_VALUE );
                            final ChangeSummary changeSummary = new ChangeSummary( user,
                                                                                "Repairing remote repository path masks to Koji build: "
                                                                                + nvr );
                            storeManager.storeArtifactStore( store, changeSummary, false, true, new EventMetadata() );
                        }
                        KojiRepairResult.RepairResult repairResult = new KojiRepairResult.RepairResult( remoteKey );
                        repairResult.withPropertyChange( "metadata_timeout", ( (RemoteRepository) store ).getMetadataTimeoutSeconds(), NEVER_TIMEOUT_VALUE );

                        ret.withResult( repairResult );
                    }
                    catch ( IndyDataException e )
                    {
                        String error =
                                String.format( "Failed to store changed remote repository: %s, error: %s", remoteKey,
                                               e );
                        logger.debug( error, e );
                        return ret.withError( error, e );
                    }
                }
                else
                {
                    String error = String.format( "Not a remote koji repository: %s", remoteKey );
                    return ret.withError( error );
                }
            }
            finally
            {
                if ( !skipLock )
                {
                    opLock.unlock();
                }
            }
        }
        else
        {
            throw new KojiRepairException( "Koji repair manager is busy." );
        }

        return ret;
    }

    public KojiRepairResult repairMetadataTimeout( KojiRepairRequest request, String user )
            throws KojiRepairException
    {
        return repairMetadataTimeout( request, user, false );
    }

    private List<RemoteRepository> getAllKojiRemotes()
            throws IndyDataException
    {
        return storeManager.query()
                           .storeTypes( remote )
                           .stream( ( remote ) ->
                                            KOJI_ORIGIN.equals( remote.getMetadata( ArtifactStore.METADATA_ORIGIN ) )
                                                    || KOJI_ORIGIN_BINARY.equals(
                                                    remote.getMetadata( ArtifactStore.METADATA_ORIGIN ) ) )
                           .map( s -> (RemoteRepository) s )
                           .filter( Objects::nonNull )
                           .collect( Collectors.toList() );
    }

    private ArtifactStore getRequestedStore(final KojiRepairRequest request, final KojiRepairResult ret ){
        StoreKey remoteKey = request.getSource();

        ArtifactStore store = null;
        try
        {
            store = storeManager.getArtifactStore( remoteKey );
        }
        catch ( IndyDataException e )
        {
            String error = String.format( "Cannot get store: %s, error: %s", remoteKey, e );
            logger.warn( error, e );
            ret.withError( error );
            return null;
        }

        if ( store == null )
        {
            String error = String.format( "No such store: %s.", remoteKey );
            ret.withError( error );
            return null;
        }

        return store;
    }

}


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

import org.apache.commons.codec.binary.Base64;
import org.commonjava.indy.httprox.conf.HttproxConfig;
import org.commonjava.indy.subsys.http.HttpWrapper;
import org.commonjava.indy.subsys.http.util.UserPass;
import org.commonjava.indy.subsys.keycloak.KeycloakAuthenticator;
import org.commonjava.indy.subsys.keycloak.conf.KeycloakConfig;
import org.commonjava.indy.subsys.keycloak.util.KeycloakBearerTokenDebug;
import org.commonjava.indy.util.ApplicationHeader;
import org.commonjava.indy.util.ApplicationStatus;
import org.keycloak.RSATokenVerifier;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.KeycloakDeploymentBuilder;
import org.keycloak.adapters.rotation.HardcodedPublicKeyLocator;
import org.keycloak.adapters.rotation.PublicKeyLocator;
import org.keycloak.common.VerificationException;
import org.keycloak.representations.AccessToken;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.security.PublicKey;
import java.util.List;

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

import static org.commonjava.indy.httprox.util.HttpProxyConstants.PROXY_AUTHENTICATE_FORMAT;

/**
 * Created by jdcasey on 9/1/15.
 * Shamelessly ripped from BearerTokenRequestAuthenticator in keycloak-adapter-api.
 */
@ApplicationScoped
public class KeycloakProxyAuthenticator
        implements KeycloakAuthenticator
{

    private static final String TOKEN_HEADER = "TOKEN";

    @Inject
    private HttproxConfig httproxConfig;

    @Inject
    private KeycloakConfig keycloakConfig;

    private KeycloakDeployment deployment;

    protected KeycloakProxyAuthenticator()
    {
    }

    public KeycloakProxyAuthenticator( KeycloakConfig config, HttproxConfig httproxConfig )
    {
        this.keycloakConfig = config;
        this.httproxConfig = httproxConfig;
    }

    private PublicKey getHardcodedRealmKey( KeycloakDeployment deployment )
    {
        PublicKeyLocator l = deployment.getPublicKeyLocator();
        if ( l instanceof HardcodedPublicKeyLocator )
        {
            return l.getPublicKey( null, null );
        }
        return null;
    }

    @Override
    public boolean authenticate( UserPass userPass, HttpWrapper http )
            throws IOException
    {
        Logger logger = LoggerFactory.getLogger( getClass() );
        if ( !keycloakConfig.isEnabled() || !httproxConfig.isSecured() )
        {
            logger.debug( "Keycloak httprox translation authenticator is disabled OR httprox is running in unsecured mode. Skipping authentication." );
            return true;
        }

        synchronized ( this )
        {
            if ( deployment == null )
            {
                String jsonPath = keycloakConfig.getKeycloakJson();

                logger.debug( "Reading keycloak deployment info from: {}", jsonPath );

                File jsonFile = new File( jsonPath );
                if ( jsonFile.exists() )
                {
                    try (FileInputStream in = new FileInputStream( jsonFile ))
                    {
                        deployment = KeycloakDeploymentBuilder.build( in );
                        logger.debug( "Got public key: '{}'", getHardcodedRealmKey( deployment ) );
                    }
                }
                else
                {
                    logger.warn( "Cannot read keycloak.json from: {}", jsonPath );
                    return false;
                }
            }
        }

        String tokenString = userPass.getPassword();

        AuthResult result = null;
        if ( tokenString != null )
        {
            result = authenticateToken( http, tokenString );
        }

        if ( result == null || !result.success )
        {
            String ts = null;
            List<String> headers = http.getHeaders( TOKEN_HEADER );
            if ( headers != null && !headers.isEmpty() )
            {
                ts = new String( Base64.decodeBase64( headers.get( 0 ) ) );
                result = authenticateToken( http, ts );
            }
        }

        if ( result == null )
        {
            logger.info( "No keycloak bearer token provided! This must either be in the password of a BASIC "
                                 + "authentication header, or in a separate Base64-encoded header: {}",
                         TOKEN_HEADER );

            sendChallengeResponse( http, null, null );
            result = new AuthResult( false );
        }
        else if (!result.success)
        {
            sendChallengeResponse( http, result.reason, result.description );
        }

        return result.success;
    }

    private static final class AuthResult
    {
        private boolean success;
        private String reason;
        private String description;

        private AuthResult( boolean success, String reason, String description )
        {
            this.success = success;
            this.reason = reason;
            this.description = description;
        }

        private AuthResult( boolean success )
        {
            this.success = success;
        }
    }

    protected AuthResult authenticateToken( HttpWrapper exchange, String tokenString )
            throws IOException
    {
        Logger logger = LoggerFactory.getLogger( getClass() );

        AccessToken token;
        try
        {
            KeycloakBearerTokenDebug.debugToken( tokenString );

            logger.debug( "Verifying token: '{}'", tokenString );
            token = RSATokenVerifier.verifyToken( tokenString, getHardcodedRealmKey( deployment ), deployment.getRealmInfoUrl() );
        }
        catch ( VerificationException e )
        {
            logger.error( "Failed to verify token", e );
            return new AuthResult( false, "invalid_token", e.getMessage() );
        }
        if ( token.getIssuedAt() < deployment.getNotBefore() )
        {
            logger.error( "Stale token" );
            return new AuthResult( false, "invalid_token", "Stale token" );
        }

        // TODO: Not yet supported.
        //        boolean verifyCaller = false;
        //        if (deployment.isUseResourceRoleMappings()) {
        //            verifyCaller = token.isVerifyCaller(deployment.getResourceName());
        //        } else {
        //            verifyCaller = token.isVerifyCaller();
        //        }
        //
        //        String surrogate = null;
        //        if (verifyCaller) {
        //            if (token.getTrustedCertificates() == null || token.getTrustedCertificates().size() == 0) {
        //                logger.warn("No trusted certificates in token");
        //                sendClientCertChallenge( exchange );
        //                return false;
        //            }
        //
        //            // for now, we just make sure Undertow did two-way SSL
        //            // assume JBoss Web verifies the client cert
        //            X509Certificate[] chain = new X509Certificate[0];
        //            try {
        //                chain = exchange.getCertificateChain();
        //            } catch (Exception ignore) {
        //
        //            }
        //            if (chain == null || chain.length == 0) {
        //                logger.warn("No certificates provided by undertow to verify the caller");
        //                sendClientCertChallenge( exchange );
        //                return false;
        //            }
        //            surrogate = chain[0].getSubjectDN().getName();
        //        }

        logger.debug( "Token verification succeeded!" );
        return new AuthResult( true );
    }

    //    protected void sendClientCertChallenge(HttpWrapper exchange) {
    //        return new AuthChallenge() {
    //            @Override
    //            public boolean errorPage() {
    //                return false;
    //            }
    //
    //            @Override
    //            public boolean challenge(HttpFacade exchange) {
    //                // do the same thing as client cert auth
    //                return false;
    //            }
    //        };
    //    }

    protected void sendChallengeResponse( HttpWrapper http, String error, String description )
            throws IOException
    {
        StringBuilder header = new StringBuilder( String.format( PROXY_AUTHENTICATE_FORMAT,
                                                                 httproxConfig.getProxyRealm() ) );
        if ( error != null )
        {
            header.append( ", error=\"" ).append( error ).append( "\"" );
        }
        if ( description != null )
        {
            header.append( ", error_description=\"" ).append( description ).append( "\"" );
        }

        final String challenge = header.toString();

        ApplicationStatus stat = ApplicationStatus.PROXY_AUTHENTICATION_REQUIRED;
        http.writeStatus( stat.code(), stat.message() );
        http.writeHeader( ApplicationHeader.proxy_authenticate, challenge );
    }
}


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

import org.commonjava.cdi.util.weft.ThreadContext;
import org.commonjava.indy.measure.annotation.Measure;
import org.commonjava.indy.measure.annotation.MetricNamed;
import org.commonjava.indy.metrics.IndyMetricsConstants;
import org.commonjava.indy.metrics.IndyMetricsManager;
import org.commonjava.maven.galley.model.SpecialPathInfo;
import org.commonjava.maven.galley.spi.cache.CacheProvider;
import org.commonjava.maven.galley.spi.io.SpecialPathManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;

import static org.commonjava.indy.bind.jaxrs.RequestContextConstants.CLIENT_ADDR;
import static org.commonjava.indy.bind.jaxrs.RequestContextConstants.EXTERNAL_ID;
import static org.commonjava.indy.bind.jaxrs.RequestContextConstants.INTERNAL_ID;
import static org.commonjava.indy.bind.jaxrs.RequestContextConstants.PREFERRED_ID;
import static org.commonjava.indy.bind.jaxrs.RequestContextConstants.X_FORWARDED_FOR;
import static org.commonjava.indy.measure.annotation.MetricNamed.DEFAULT;
import static org.commonjava.indy.metrics.IndyMetricsConstants.getDefaultName;

@ApplicationScoped
public class ResourceManagementFilter
        implements Filter
{

    public static final String HTTP_REQUEST = "http-request";

    public static final String ORIGINAL_THREAD_NAME = "original-thread-name";

    public static final String METHOD_PATH_TIME = "method-path-time";

    private static final String BASE_CONTENT_METRIC = "indy.content.";

    private static final String POM_CONTENT_METRIC = BASE_CONTENT_METRIC + "pom";

    private static final String NORMAL_CONTENT_METRIC = BASE_CONTENT_METRIC + "other";

    private static final String METADATA_CONTENT_METRIC = BASE_CONTENT_METRIC + "metadata";

    private static final String SPECIAL_CONTENT_METRIC = BASE_CONTENT_METRIC + "special";

    @Inject
    private CacheProvider cacheProvider;

    @Inject
    private MDCManager mdcManager;

    @Inject
    private SpecialPathManager specialPathManager;

    @Inject
    private IndyMetricsManager metricsManager;

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

    private final Logger restLogger = LoggerFactory.getLogger( "org.commonjava.topic.rest.inbound" );

    @Override
    public void init( final FilterConfig filterConfig )
            throws ServletException
    {
        if ( logger.isTraceEnabled() )
        {
            cacheProvider.startReporting();
        }
    }

    @Override
    @Measure
    public void doFilter( final ServletRequest request, final ServletResponse response, final FilterChain chain )
            throws IOException, ServletException
    {
        String name = Thread.currentThread().getName();
        String clientAddr = request.getRemoteAddr();

        final HttpServletRequest hsr = (HttpServletRequest) request;
        final String xForwardFor = hsr.getHeader( X_FORWARDED_FOR );
        if ( xForwardFor != null )
        {
            clientAddr = xForwardFor; // OSE proxy use HTTP header 'x-forwarded-for' to represent user IP
        }

        String tn = hsr.getMethod() + " " + hsr.getPathInfo() + " (" + System.currentTimeMillis() + "." + System.nanoTime() + ")";
        String qs = hsr.getQueryString();

        try
        {
            ThreadContext.clearContext();
            ThreadContext threadContext = ThreadContext.getContext( true );

            threadContext.put( ORIGINAL_THREAD_NAME, name );

            threadContext.put( HTTP_REQUEST, hsr );

            threadContext.put( METHOD_PATH_TIME, tn );

            threadContext.put( CLIENT_ADDR, clientAddr );

            putRequestIDs( hsr, threadContext, mdcManager );

            mdcManager.putUserIP( clientAddr );

            mdcManager.putExtraHeaders( hsr );

            logger.debug( "START request: {} (from: {})", tn, clientAddr );

            Thread.currentThread().setName( tn );

            restLogger.info( "START {}{} (from: {})", hsr.getRequestURL(), qs == null ? "" : "?" + qs, clientAddr );

            AtomicReference<IOException> ioex = new AtomicReference<>();
            AtomicReference<ServletException> seex = new AtomicReference<>();

            metricsManager.wrapWithStandardMetrics( () -> {
                try
                {
                    chain.doFilter( request, response );
                }
                catch ( IOException e )
                {
                    ioex.set( e );
                }
                catch ( ServletException e )
                {
                    seex.set( e );
                }
                return null;
            }, pathClassifier( hsr.getPathInfo() ) );

            if ( ioex.get() != null )
            {
                throw ioex.get();
            }

            if ( seex.get() != null )
            {
                throw seex.get();
            }
        }
        finally
        {
            logger.debug( "Cleaning up resources for thread: {}", Thread.currentThread().getName() );
            try
            {
                cacheProvider.cleanupCurrentThread();
            }
            catch ( Exception e )
            {
                logger.error( "Failed to cleanup resources", e );
            }

            restLogger.info( "END {}{} (from: {})", hsr.getRequestURL(), qs == null ? "" : "?" + qs, clientAddr );

            Thread.currentThread().setName( name );
            ThreadContext.clearContext();

            logger.debug( "END request: {} (from: {})", tn, clientAddr );

            mdcManager.clear();
        }
    }

    private Supplier<String> pathClassifier( final String pathInfo )
    {
        return ()->{
            if ( !pathInfo.contains( "content" ))
            {
                return IndyMetricsConstants.SKIP_METRIC;
            }
            SpecialPathInfo spi = specialPathManager.getSpecialPathInfo( pathInfo );
            if ( spi == null )
            {
                return pathInfo.endsWith( ".pom" ) ? POM_CONTENT_METRIC : NORMAL_CONTENT_METRIC;
            }
            else if ( spi.isMetadata() )
            {
                return METADATA_CONTENT_METRIC;
            }
            else
            {
                return SPECIAL_CONTENT_METRIC;
            }
        };
    }

    /**
     * Put to MDC / threadContext request IDs.
    */
    private void putRequestIDs( HttpServletRequest hsr, ThreadContext threadContext, MDCManager mdcManager )
    {
        /* We would always generate internalID and provide that in the MDC.
         * If the calling service supplies an externalID, we'd map that under its own key.
         * PreferredID should try to use externalID if it's available, and default over to using internalID if it's not.
         * What this gives us is a single key we can use to reference an ID for the request,
         * and whenever possible it'll reflect the externally supplied ID.
         */
        String internalID = UUID.randomUUID().toString();
        String externalID = hsr.getHeader( EXTERNAL_ID );
        String preferredID = externalID != null ? externalID : internalID;

        mdcManager.putRequestIDs( internalID, externalID, preferredID );

        /*
         * We should also put the same values in the ThreadContext map, so we can reference them from code without
         * having to go through the logging framework
         */
        threadContext.put( INTERNAL_ID, internalID );
        if ( externalID != null )
        {
            threadContext.put( EXTERNAL_ID, externalID );
        }
        threadContext.put( PREFERRED_ID, preferredID );
    }

    @Override
    public void destroy()
    {
    }

}


/**
 * 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.pkg.maven.content;

import org.commonjava.indy.change.event.ArtifactStoreDeletePreEvent;
import org.commonjava.indy.change.event.ArtifactStorePreUpdateEvent;
import org.commonjava.indy.change.event.ArtifactStoreUpdateType;
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.StoreType;
import org.commonjava.indy.model.galley.KeyedLocation;
import org.commonjava.indy.pkg.maven.content.cache.MavenVersionMetadataCache;
import org.commonjava.indy.subsys.infinispan.CacheHandle;
import org.commonjava.maven.galley.event.EventMetadata;
import org.commonjava.maven.galley.event.FileDeletionEvent;
import org.commonjava.maven.galley.model.Location;
import org.commonjava.maven.galley.model.Transfer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.event.Observes;
import javax.inject.Inject;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Stream;

import static org.commonjava.indy.IndyContentConstants.CHECK_CACHE_ONLY;
import static org.commonjava.indy.pkg.maven.content.group.MavenMetadataMerger.METADATA_NAME;

/**
 * This listener will do these tasks:
 *

 *
When there are member changes for a group, or some members disabled/enabled in a group, delete group metadata caches to force next regeneration of the metadata files of the group(cascaded)

 *

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

    @Inject
    private MavenMetadataGenerator metadataGenerator;

    @Inject
    private StoreDataManager storeManager;

    @Inject
    @MavenVersionMetadataCache
    private CacheHandle<StoreKey, Map> versionMetadataCache;

    /**
     * Listen to an #{@link ArtifactStorePreUpdateEvent} and clear the metadata cache due to changed memeber in that event
     *
     * @param event
     */
    public void onStoreUpdate( @Observes final ArtifactStorePreUpdateEvent event )
    {
        logger.trace( "Got store-update event: {}", event );

        if ( ArtifactStoreUpdateType.UPDATE == event.getType() )
        {
            for ( ArtifactStore store : event )
            {
                removeMetadataCacheContent( store, event.getChangeMap() );
            }
        }
    }

    public void onStoreDelete( @Observes final ArtifactStoreDeletePreEvent event )
    {
        logger.trace( "Got store-delete event: {}", event );

        for ( ArtifactStore store : event )
        {
            removeMetadataCache( store );
        }
    }

    /**
     * Indy normally does not handle FileDeletionEvent when the cached metadata files were deleted due to store
     * enable/disable/delete, etc. Lately we add a force-deletion for group/remote-repo cache files. This requires to
     * delete affected group metadata files and clear ISPN cache too. We use this method for just this case, i.e.,
     * only if CHECK_CACHE_ONLY is true.
     */
    public void onMetadataFileForceDelete( @Observes final FileDeletionEvent event )
    {
        EventMetadata eventMetadata = event.getEventMetadata();
        if ( !Boolean.TRUE.equals( eventMetadata.get( CHECK_CACHE_ONLY ) ) )
        {
            return;
        }

        logger.trace( "Got file-delete event: {}", event );

        Transfer transfer = event.getTransfer();
        String path = transfer.getPath();

        if ( !path.endsWith( METADATA_NAME ) )
        {
            logger.trace( "Not {} , path: {}", METADATA_NAME, path );
            return;
        }

        Location loc = transfer.getLocation();

        if ( !( loc instanceof KeyedLocation ) )
        {
            logger.trace( "Ignore FileDeletionEvent, not a KeyedLocation, location: {}", loc );
            return;
        }

        KeyedLocation keyedLocation = (KeyedLocation) loc;
        StoreKey storeKey = keyedLocation.getKey();
        try
        {
            ArtifactStore store = storeManager.getArtifactStore( storeKey );
            metadataGenerator.clearAllMerged( store, path );
        }
        catch ( IndyDataException e )
        {
            logger.error( "Handle FileDeletionEvent failed", e );
        }
    }

    private void removeMetadataCacheContent( final ArtifactStore store,
                                             final Map<ArtifactStore, ArtifactStore> changeMap )
    {
        logger.trace( "Processing update event for: {}", store.getKey() );
        handleStoreDisableOrEnable( store, changeMap );

        handleGroupMembersChanged( store, changeMap );
    }

    // if a store is disabled/enabled, we should clear its metadata cache and all of its affected groups cache too.
    private void handleStoreDisableOrEnable(final ArtifactStore store,
                                            final Map<ArtifactStore, ArtifactStore> changeMap){
        logger.trace( "Processing en/disable event for: {}", store.getKey() );

        final ArtifactStore oldStore = changeMap.get( store );
        if ( store.isDisabled() != oldStore.isDisabled() )
        {
            logger.trace( "En/disable state changed for: {}", store.getKey() );
            removeMetadataCache( store );
        }
        else
        {
            logger.trace( "En/disable state has not changed for: {}", store.getKey() );
        }
    }

    // If group members changed, should clear the cascading groups metadata cache
    private void handleGroupMembersChanged(final ArtifactStore store,
                                           final Map<ArtifactStore, ArtifactStore> changeMap)
    {
        final StoreKey key = store.getKey();
        if ( StoreType.group == key.getType() )
        {
            final List<StoreKey> newMembers = ( (Group) store ).getConstituents();
            logger.trace( "New members of: {} are: {}", store.getKey(), newMembers );

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

            boolean membersChanged = false;

            if ( newMembers.size() != oldMembers.size() )
            {
                membersChanged = true;
            }
            else
            {
                for ( StoreKey storeKey : newMembers )
                {
                    if ( !oldMembers.contains( storeKey ) )
                    {
                        membersChanged = true;
                    }
                }
            }

            if ( membersChanged )
            {
                logger.trace( "Membership change confirmed. Clearing caches for group: {} and groups affected by it.", group.getKey() );
                clearGroupMetaCache( group, group );
                try
                {
                    storeManager.query().getGroupsAffectedBy( group.getKey() ).forEach( g -> clearGroupMetaCache( g, group ) );
                }
                catch ( IndyDataException e )
                {
                    logger.error( String.format( "Can not get affected groups of %s", group.getKey() ), e );
                }
            }
            else
            {
                logger.trace( "No members changed, no need to expunge merged metadata" );
            }
        }
    }

    private void removeMetadataCache( ArtifactStore store )
    {
        logger.trace( "Removing cached metadata for: {}", store.getKey() );

        versionMetadataCache.remove( store.getKey() );
        try
        {
            storeManager.query().getGroupsAffectedBy( store.getKey() ).forEach( g -> clearGroupMetaCache( g, store ) );
        }
        catch ( IndyDataException e )
        {
            logger.error( String.format( "Can not get affected groups of %s", store.getKey() ), e );
        }
    }

    private void clearGroupMetaCache( final Group group, final ArtifactStore store )
    {
        final Map<String, MetadataInfo> metadataMap = versionMetadataCache.get( group.getKey() );

        Logger logger = LoggerFactory.getLogger( getClass() );
        logger.trace( "Clearing metadata for group: {} on store update: {}\n{}", group.getKey(), store.getKey(), metadataMap );

        if ( metadataMap == null || metadataMap.isEmpty() )
        {
            logger.trace( "No cached metadata for: {}", group.getKey() );
            return;
        }

        String[] paths = new String[metadataMap.size()];
        paths = metadataMap.keySet().toArray( paths );

        List<String> pathsList = Arrays.asList( paths );

        logger.trace(
                "Clearing merged paths in MavenMetadataGenerator for: {} as a result of change in: {} (paths: {})",
                group.getKey(), store.getKey(), pathsList );

        metadataGenerator.clearAllMerged( group, paths );

        logger.trace( "Clearing cached, merged paths for: {} as a result of change in: {} (paths: {})", group.getKey(),
                      store.getKey(), pathsList );

        versionMetadataCache.remove( group.getKey() );
    }

}


/**
 * 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.pkg.maven.content;

import org.commonjava.indy.content.DirectContentAccess;
import org.commonjava.indy.content.MergedContentAction;
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.pkg.maven.content.cache.MavenVersionMetadataCache;
import org.commonjava.indy.subsys.infinispan.CacheHandle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import java.util.Map;
import java.util.Set;

/**
 * This listener will do these tasks:
 *

 *
When the metadata file changed of a member in a group, delete correspond cache of that file path of the member and group (cascaded)

 *

 */
@ApplicationScoped
public class MetadataMergeListener
        implements MergedContentAction
{
    final Logger logger = LoggerFactory.getLogger( getClass() );

    @Inject
    private DirectContentAccess fileManager;

    @Inject
    @MavenVersionMetadataCache
    private CacheHandle<StoreKey, Map> versionMetadataCache;

    /**
     * Will clear the both merge path and merge info file of member and group contains that member(cascaded)
     * if that path of file changed in the member of #originatingStore
     */
    @Override
    public void clearMergedPath( ArtifactStore originatingStore, Set<Group> affectedGroups, String path )
    {
        final Map<String, MetadataInfo> metadataMap = versionMetadataCache.get( originatingStore.getKey() );

        if ( metadataMap != null && !metadataMap.isEmpty() )
        {
            if ( metadataMap.get( path ) != null )
            {
                metadataMap.remove( path );
                affectedGroups.forEach( group -> {
                    final Map<String, MetadataInfo> grpMetaMap = versionMetadataCache.get( group.getKey() );
                    if ( grpMetaMap != null && !grpMetaMap.isEmpty() )
                    {
                        grpMetaMap.remove( path );
                    }
                } );
            }
        }
    }

}


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

import static org.apache.commons.lang.StringUtils.isEmpty;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.commonjava.indy.model.core.StoreKey;
import org.commonjava.indy.model.core.StoreType;

public class SettingsURIMatcher
    implements URIMatcher
{

    public static final String SETTINGS_TYPE_PATTERN = "\\/?settings(\\/(hosted|group|remote)(\\/settings-(.+).xml)?)?";

    private static final int TYPE_GRP = 2;

    private static final int NAME_GRP = 4;

    private final Matcher matcher;

    private final String uri;

    public SettingsURIMatcher( final String uri )
    {
        this.uri = uri;
        this.matcher = Pattern.compile( SETTINGS_TYPE_PATTERN )
                              .matcher( uri );
    }

    @Override
    public boolean matches()
    {
        return matcher.matches();
    }

    public boolean isSettingsRootResource()
    {
        return matches() && matcher.group( TYPE_GRP ) == null;
    }

    public boolean isSettingsTypeResource()
    {
        return matches() && matcher.group( TYPE_GRP ) != null;
    }

    public boolean isSettingsFileResource()
    {
        return matches() && matcher.group( NAME_GRP ) != null;
    }

    /* (non-Javadoc)
     * @see org.commonjava.indy.dotmaven.util.URIMatcher#getStoreType()
     */
    @Override
    public StoreType getStoreType()
    {
        if ( !matches() )
        {
            return null;
        }

        final String typePart = matcher.group( TYPE_GRP );
        if ( typePart == null )
        {
            return null;
        }

        final StoreType type = StoreType.get( typePart );
        return type;
    }

    /* (non-Javadoc)
     * @see org.commonjava.indy.dotmaven.util.URIMatcher#getStoreKey()
     */
    @Override
    public StoreKey getStoreKey()
    {
        final StoreType type = getStoreType();

        if ( type == null )
        {
            return null;
        }

        final String name = matcher.group( NAME_GRP );
        if ( isEmpty( name ) )
        {
            return null;
        }

        return new StoreKey( type, name );
    }

    /* (non-Javadoc)
     * @see org.commonjava.indy.dotmaven.util.URIMatcher#getURI()
     */
    @Override
    public String getURI()
    {
        return uri;
    }
}


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

import org.commonjava.indy.conf.IndyConfigFactory;
import org.commonjava.indy.conf.IndyConfigInfo;
import org.commonjava.maven.galley.util.PathUtils;
import org.commonjava.web.config.annotation.ConfigName;
import org.commonjava.web.config.annotation.SectionName;

import javax.enterprise.context.ApplicationScoped;
import java.io.File;
import java.io.InputStream;
import java.util.Properties;

@SectionName( "keycloak" )
@ApplicationScoped
public class KeycloakConfig
    implements IndyConfigInfo
{

    private static final boolean DEFAULT_ENABLED = false;

    private static final String DEFAULT_REALM = "indy";

    private static final String DEFAULT_KEYCLOAK_JSON = "keycloak/keycloak.json";

    private static final String DEFAULT_KEYCLOAK_UI_JSON = "keycloak/keycloak-ui.json";

    private static final String DEFAULT_SECURITY_BINDINGS_JSON = "keycloak/security-bindings.json";

    private static final String DEFAULT_SERVER_RESOURCE = "indy";

    private static final String DEFAULT_UI_RESOURCE = "indy-ui";

    public static final String KEYCLOAK_REALM = "keycloak.realm";

    public static final String KEYCLOAK_URL = "keycloak.url";

    public static final String KEYCLOAK_SERVER_RESOURCE = "keycloak.serverResource";

    public static final String KEYCLOAK_UI_RESOURCE = "keycloak.uiResource";

    private static final String KEYCLOAK_SERVER_CREDENTIAL_SECRET = "keycloak.serverCredentialSecret";

    public static final String KEYCLOAK_REALM_PUBLIC_KEY = "keycloak.realmPublicKey";

    private String realm;

    private Boolean enabled;

    private String keycloakJson;

    private String keycloakUiJson;

    private String securityBindingsJson;

    private String url;

    private String serverCredentialSecret;

    private String serverResource;

    private String uiResource;

    private String realmPublicKey;

    public boolean isEnabled()
    {
        return enabled == null ? DEFAULT_ENABLED : enabled;
    }

    @ConfigName( "enabled" )
    public void setEnabled( final Boolean enabled )
    {
        this.enabled = enabled;
    }

    public String getRealm()
    {
        return realm == null ? DEFAULT_REALM : realm;
    }

    @ConfigName( "realm" )
    public void setRealm( final String realm )
    {
        this.realm = realm;
    }

    public String getKeycloakJson()
    {
        if ( keycloakJson == null )
        {
            return getDefaultConfFile( DEFAULT_KEYCLOAK_JSON );
        }

        return keycloakJson;
    }

    @ConfigName( "keycloak.json" )
    public void setKeycloakJson( final String keycloakJson )
    {
        this.keycloakJson = keycloakJson;
    }

    public String getKeycloakUiJson()
    {
        if ( keycloakUiJson == null )
        {
            return getDefaultConfFile( DEFAULT_KEYCLOAK_UI_JSON );
        }

        return keycloakUiJson;
    }

    @ConfigName( "keycloak-ui.json" )
    public void setKeycloakUiJson( final String keycloakUiJson )
    {
        this.keycloakUiJson = keycloakUiJson;
    }

    public String getSecurityBindingsJson()
    {
        if ( securityBindingsJson == null )
        {
            return getDefaultConfFile( DEFAULT_SECURITY_BINDINGS_JSON );
        }

        return securityBindingsJson;
    }

    @ConfigName( "security-bindings.json" )
    public void setSecurityBindingsJson( final String securityConstraintsJson )
    {
        this.securityBindingsJson = securityConstraintsJson;
    }

    /**
     * Set system properties for keycloak to use when filtering keycloak.json...
     */
    public KeycloakConfig setSystemProperties()
    {
        if ( !isEnabled() )
        {
            return this;
        }

        final Properties properties = System.getProperties();
        properties.setProperty( KEYCLOAK_REALM, getRealm() );
        properties.setProperty( KEYCLOAK_URL, getUrl() );

        if ( getServerResource() != null )
        {
            properties.setProperty( KEYCLOAK_SERVER_RESOURCE, getServerResource() );
        }

        if ( getServerCredentialSecret() != null )
        {
            properties.setProperty( KEYCLOAK_SERVER_CREDENTIAL_SECRET, getServerCredentialSecret() );
        }

        if ( getRealmPublicKey() != null )
        {
            properties.setProperty( KEYCLOAK_REALM_PUBLIC_KEY, getRealmPublicKey() );
        }

        System.setProperties( properties );

        return this;
    }

    private String getDefaultConfFile( final String confFile )
    {
        String confDir = System.getProperty( IndyConfigFactory.CONFIG_DIR_PROP );
        if ( confDir == null )
        {
            confDir = PathUtils.normalize( System.getProperty( IndyConfigFactory.INDY_HOME_PROP ), "etc/indy" );
        }

        return PathUtils.normalize( confDir, confFile );
    }

    public String getUrl()
    {
        return url;
    }

    @ConfigName( "url" )
    public void setUrl( final String url )
    {
        this.url = url;
    }

    public String getServerCredentialSecret()
    {
        return serverCredentialSecret;
    }

    @ConfigName( "server.credential.secret" )
    public void setServerCredentialSecret( final String serverCredentialSecret )
    {
        this.serverCredentialSecret = serverCredentialSecret;
    }

    public String getServerResource()
    {
        return serverResource == null ? DEFAULT_SERVER_RESOURCE : serverResource;
    }

    @ConfigName( "server.resource" )
    public void setServerResource( final String serverResource )
    {
        this.serverResource = serverResource;
    }

    public String getUiResource()
    {
        return uiResource == null ? DEFAULT_UI_RESOURCE : uiResource;
    }

    @ConfigName( "ui.resource" )
    public void setUiResource( final String uiResource )
    {
        this.uiResource = uiResource;
    }

    public String getRealmPublicKey()
    {
        return realmPublicKey;
    }

    @ConfigName( "realm.public.key" )
    public void setRealmPublicKey( final String realmPublicKey )
    {
        this.realmPublicKey = realmPublicKey;
    }

    @Override
    public String getDefaultConfigFileName()
    {
        return new File( IndyConfigInfo.CONF_INCLUDES_DIR, "keycloak.conf" ).getPath();
    }

    @Override
    public InputStream getDefaultConfig()
    {
        return Thread.currentThread()
                     .getContextClassLoader()
                     .getResourceAsStream( "default-keycloak.conf" );
    }

}


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

import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

import javax.annotation.PostConstruct;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.inject.Instance;
import javax.inject.Inject;
import org.commonjava.indy.stats.IndyVersioning;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Handles the startup sequence (managing {@link BootupAction}, {@link MigrationAction}, and {@link StartupAction} instances in order), and the
 * shutdown sequence (managing {@link ShutdownAction} instances in order.
 *
 * @author jdcasey
 */
@ApplicationScoped
public class IndyLifecycleManager
{

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

    @Inject
    private IndyVersioning versioning;

    @Inject
    private Instance<BootupAction> bootupActionCdiInstances;

    @Inject
    private Instance<MigrationAction> migrationActionCdiInstances;

    @Inject
    private Instance<StartupAction> startupActionCdiInstances;

    @Inject
    private Instance<ShutdownAction> shutdownActionCdiInstances;

    @Inject
    private IndyLifecycleEventManager lifecycleEvents;

    @Inject
    UserLifecycleManager userLifecycleManager;

    private List<BootupAction> bootupActions;

    private List<MigrationAction> migrationActions;

    private List<StartupAction> startupActions;

    private List<ShutdownAction> shutdownActions;

    private Iterable<BootupAction> bootupActionInstances;

    private Iterable<MigrationAction> migrationActionInstances;

    private Iterable<StartupAction> startupActionInstances;

    private Iterable<ShutdownAction> shutdownActionInstances;

    protected IndyLifecycleManager()
    {
    }

    public IndyLifecycleManager( final IndyVersioning versioning, final Iterable<BootupAction> bootupActionInstances,
                                  final Iterable<MigrationAction> migrationActionInstances,
                                  final Iterable<StartupAction> startupActionInstances,
                                  final Iterable<ShutdownAction> shutdownActionInstances )
    {
        initialize( bootupActionInstances, migrationActionInstances, startupActionInstances, shutdownActionInstances );
    }

    @PostConstruct
    public void init()
    {
        initialize( bootupActionCdiInstances, migrationActionCdiInstances, startupActionCdiInstances, shutdownActionCdiInstances );
    }

    private void initialize( final Iterable<BootupAction> bootupActionInstances,
                             final Iterable<MigrationAction> migrationActionInstances,
                             final Iterable<StartupAction> startupActionInstances,
                             final Iterable<ShutdownAction> shutdownActionInstances )
    {
        this.bootupActionInstances = bootupActionInstances;
        this.migrationActionInstances = migrationActionInstances;
        this.startupActionInstances = startupActionInstances;
        this.shutdownActionInstances = shutdownActionInstances;
//        try
//        {
//
//
//
//        }
//        catch ( Throwable e )
//        {
//            Throwable realError = ( e instanceof InvocationTargetException ?
//                    ( (InvocationTargetException) e ).getTargetException() :
//                    e );
//
//            logger.error( "Failed to initialize Indy lifecycle components.", realError );
//
//            throw new IllegalStateException( "Failed to initialize lifecycle components", realError );
//        }
    }

    /**
     * Start sequence is:
     *

     *
Start all {@link BootupAction} instances, with highest priority executing first.

     *
Run all {@link MigrationAction} instances, with highest priority executing first.

     *
Run all {@link StartupAction} instances, with highest priority executing first.

     *

     * @throws IndyLifecycleException
     */
    public void start()
        throws IndyLifecycleException
    {
        logger.info( "\n\n\n\n\n STARTING Indy\n    Version: {}\n    Built-By: {}\n    Commit-ID: {}\n    Built-On: {}\n\n\n\n\n",
                     versioning.getVersion(), versioning.getBuilder(), versioning.getCommitId(),
                     versioning.getTimestamp() );

        runBootupActions();
        runMigrationActions();
        runStartupActions();

        logger.info( "...done. Indy is ready to run." );

        if ( lifecycleEvents == null )
        {
            logger.error( "Cannot fire IndyLifecycleEvent::started! Event manager is null. Did you construct this {} instance by hand?",
                          getClass().getSimpleName() );
        }
        else
        {
            lifecycleEvents.fireStarted();
        }
    }

    /**
     * Run all {@link ShutdownAction} instances, with highest priority executing first.
     * @throws IndyLifecycleException
     */
    public void stop()
        throws IndyLifecycleException
    {
        System.out.println( "Shutting down Indy" );

        logger.info( "\n\n\n\n\n SHUTTING DOWN Indy\n    Version: {}\n    Built-By: {}\n    Commit-ID: {}\n    Built-On: {}\n\n\n\n\n",
                     versioning.getVersion(), versioning.getBuilder(), versioning.getCommitId(),
                     versioning.getTimestamp() );

        runShutdownActions();

        logger.info( "...done. Indy is ready to shut down." );
    }

    private void runBootupActions()
        throws IndyLifecycleException
    {
        bootupActions = new ArrayList<>();
        for ( final BootupAction action : bootupActionInstances )
        {
            bootupActions.add( action );
        }
        bootupActions.addAll(userLifecycleManager.getUserLifecycleActions("boot",
                                                                          BootupAction.class));
        Collections.sort( bootupActions, BOOT_PRIORITY_COMPARATOR );

        if ( bootupActions != null )
        {
            logger.info( "Running bootup actions..." );
            for ( final BootupAction action : bootupActions )
            {
                logger.info( "Running bootup action: '{}'", action.getId() );
                action.init();
            }
        }
    }

    private void runMigrationActions()
        throws IndyLifecycleException
    {
        migrationActions = new ArrayList<>();
        for ( final MigrationAction action : migrationActionInstances )
        {
            migrationActions.add( action );
        }
        migrationActions.addAll(userLifecycleManager.getUserLifecycleActions("migrate",
                                                                             MigrationAction.class));
        Collections.sort( migrationActions, MIGRATION_PRIORITY_COMPARATOR );

        boolean changed = false;
        if ( migrationActions != null )
        {
            logger.info( "Running migration actions..." );
            for ( final MigrationAction action : migrationActions )
            {
                logger.info( "Running migration action: '{}'", action.getId() );
                changed = action.migrate() || changed;
            }
        }
    }

    private void runStartupActions()
        throws IndyLifecycleException
    {
        startupActions = new ArrayList<>();
        for ( final StartupAction action : startupActionInstances )
        {
            startupActions.add( action );
        }
        startupActions.addAll(userLifecycleManager.getUserLifecycleActions("start",
                                                                           StartupAction.class));
        Collections.sort( startupActions, START_PRIORITY_COMPARATOR );

        if ( startupActions != null )
        {
            logger.info( "Running startup actions..." );
            for ( final StartupAction action : startupActions )
            {
                logger.info( "Running startup action: '{}'", action.getId() );
                action.start();
            }
        }
    }

    private void runShutdownActions()
        throws IndyLifecycleException
    {
        shutdownActions = new ArrayList<>();
        for ( final ShutdownAction action : shutdownActionInstances )
        {
            shutdownActions.add( action );
        }
        shutdownActions.addAll(userLifecycleManager.getUserLifecycleActions("shutdown",
                                                                            ShutdownAction.class));
        Collections.sort( shutdownActions, SHUTDOWN_PRIORITY_COMPARATOR );

        if ( shutdownActions != null )
        {
            logger.info( "Running shutdown actions..." );
            for ( final ShutdownAction action : shutdownActions )
            {
                logger.info( "Running shutdown action: '{}'", action.getId() );
                action.stop();
            }
        }
    }

    /**
     * Create a Runnable that can be used in {@link Runtime#addShutdownHook(Thread)}.
     */
    public Runnable createShutdownRunnable()
    {
        return ()->
        {
            try
            {
                stop();
            }
            catch ( final IndyLifecycleException e )
            {
                throw new RuntimeException( "\n\nFailed to stop Indy: " + e.getMessage(), e );
            }
        };
    }

    private static final Comparator<BootupAction> BOOT_PRIORITY_COMPARATOR = new Comparator<BootupAction>()
    {
        @Override
        public int compare( final BootupAction first, final BootupAction second )
        {
            final int comp = first.getBootPriority() - second.getBootPriority();
            if ( comp < 0 )
            {
                return 1;
            }
            else if ( comp > 0 )
            {
                return -1;
            }

            return 0;
        }
    };

    private static final Comparator<MigrationAction> MIGRATION_PRIORITY_COMPARATOR = new Comparator<MigrationAction>()
    {
        @Override
        public int compare( final MigrationAction first, final MigrationAction second )
        {
            final int comp = first.getMigrationPriority() - second.getMigrationPriority();
            if ( comp < 0 )
            {
                return 1;
            }
            else if ( comp > 0 )
            {
                return -1;
            }

            return 0;
        }
    };

    private static final Comparator<StartupAction> START_PRIORITY_COMPARATOR = new Comparator<StartupAction>()
    {
        @Override
        public int compare( final StartupAction first, final StartupAction second )
        {
            final int comp = first.getStartupPriority() - second.getStartupPriority();
            if ( comp < 0 )
            {
                return 1;
            }
            else if ( comp > 0 )
            {
                return -1;
            }

            return 0;
        }
    };

    private static final Comparator<ShutdownAction> SHUTDOWN_PRIORITY_COMPARATOR = new Comparator<ShutdownAction>()
    {
        @Override
        public int compare( final ShutdownAction first, final ShutdownAction second )
        {
            final int comp = first.getShutdownPriority() - second.getShutdownPriority();
            if ( comp < 0 )
            {
                return 1;
            }
            else if ( comp > 0 )
            {
                return -1;
            }

            return 0;
        }
    };
}


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

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.codehaus.plexus.interpolation.InterpolationException;
import org.codehaus.plexus.interpolation.PropertiesBasedValueSource;
import org.codehaus.plexus.interpolation.StringSearchInterpolator;
import org.commonjava.indy.action.IndyLifecycleException;
import org.commonjava.indy.action.ShutdownAction;
import org.commonjava.indy.conf.IndyConfiguration;
import org.commonjava.indy.metrics.IndyMetricsManager;
import org.commonjava.indy.metrics.conf.IndyMetricsConfig;
import org.commonjava.indy.subsys.infinispan.config.ISPNClusterConfiguration;
import org.commonjava.indy.subsys.infinispan.config.ISPNRemoteConfiguration;
import org.infinispan.Cache;
import org.infinispan.client.hotrod.RemoteCache;
import org.infinispan.client.hotrod.RemoteCacheManager;
import org.infinispan.commons.marshall.MarshallableTypeHints;
import org.infinispan.configuration.ConfigurationManager;
import org.infinispan.configuration.cache.Configuration;
import org.infinispan.configuration.parsing.ConfigurationBuilderHolder;
import org.infinispan.configuration.parsing.ParserRegistry;
import org.infinispan.manager.DefaultCacheManager;
import org.infinispan.manager.EmbeddedCacheManager;
import org.infinispan.client.hotrod.configuration.ConfigurationBuilder;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.PostConstruct;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import java.io.ByteArrayInputStream;
import java.io.Externalizable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import static org.commonjava.indy.metrics.IndyMetricsConstants.getSupername;
import static org.commonjava.indy.subsys.infinispan.metrics.IspnCheckRegistrySet.INDY_METRIC_ISPN;

/**
 * Created by jdcasey on 3/8/16.
 */
@ApplicationScoped
public class CacheProducer
        implements ShutdownAction
{
    Logger logger = LoggerFactory.getLogger( getClass() );

    private static final String ISPN_XML = "infinispan.xml";
    private static final String ISPN_CLUSTER_XML = "infinispan-cluster.xml";

    private EmbeddedCacheManager cacheManager;

    @Inject
    private ISPNRemoteConfiguration remoteConfiguration;

    @Inject
    private IndyConfiguration indyConfiguration;

    @Inject
    private IndyMetricsManager metricsManager;

    @Inject
    private IndyMetricsConfig metricsConfig;

    @Inject
    private ISPNClusterConfiguration clusterConfiguration;

    private Map<String, BasicCacheHandle> caches = new ConcurrentHashMap<>(); // hold embedded and remote caches

    protected CacheProducer()
    {
    }

    public CacheProducer( IndyConfiguration indyConfiguration, EmbeddedCacheManager cacheManager, ISPNRemoteConfiguration remoteConfiguration )
    {
        this.indyConfiguration = indyConfiguration;
        this.cacheManager = cacheManager;
        this.remoteConfiguration = remoteConfiguration;
    }

    @PostConstruct
    public void start()
    {
        startRemoteManager();
        startEmbeddedManager();
        startClusterManager();
    }

    private RemoteCacheManager remoteCacheManager;

    private EmbeddedCacheManager clusterCacheManager;

    private void startRemoteManager()
    {
        if ( remoteConfiguration == null || !remoteConfiguration.isEnabled() )
        {
            logger.info( "Infinispan remote configuration not enabled. Skip." );
            return;
        }

        ConfigurationBuilder builder = new ConfigurationBuilder();
        builder.addServer().host( remoteConfiguration.getRemoteServer() ).port( remoteConfiguration.getHotrodPort() );
        remoteCacheManager = new RemoteCacheManager( builder.build() );
        logger.info( "Infinispan remote cache manager started." );
    }

    private void startEmbeddedManager()
    {
        startCacheManager( ISPN_XML, false );
    }

    private void startClusterManager()
    {
        if ( clusterConfiguration == null || !clusterConfiguration.isEnabled() )
        {
            logger.info( "Infinispan cluster configuration not enabled. Skip." );
            return;
        }
        startCacheManager( ISPN_CLUSTER_XML, true );
    }

    private void startCacheManager( String configFile, Boolean isCluster )
    {
        // FIXME This is just here to trigger shutdown hook init for embedded log4j in infinispan-embedded-query.
        // FIXES:
        //
        // Thread-15 ERROR Unable to register shutdown hook because JVM is shutting down.
        // java.lang.IllegalStateException: Cannot add new shutdown hook as this is not started. Current state: STOPPED
        //
        new MarshallableTypeHints().getBufferSizePredictor( CacheHandle.class );

        File confDir = indyConfiguration.getIndyConfDir();
        File ispnConf = new File( confDir, configFile );

        try(InputStream resouceStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(
                        configFile ))
        {

            String resourceStr = interpolateStrFromStream( resouceStream, "CLASSPATH:" + configFile );

            if ( ispnConf.exists() )
            {
                try (InputStream confStream = FileUtils.openInputStream( ispnConf ))
                {
                    String confStr = interpolateStrFromStream( confStream, ispnConf.getPath() );
                    mergedCachesFromConfig( confStr, "CUSTOMER" );
                    mergedCachesFromConfig( resourceStr, "CLASSPATH" );
                }
                catch ( IOException e )
                {
                    throw new RuntimeException( "Cannot read infinispan configuration from file: " + ispnConf, e );
                }
            }
            else
            {
                try
                {
                    logger.info( "Using CLASSPATH resource Infinispan configuration:\n\n{}\n\n", resourceStr );
                    if ( isCluster )
                    {
                        clusterCacheManager = new DefaultCacheManager(
                                        new ByteArrayInputStream( resourceStr.getBytes( StandardCharsets.UTF_8 ) ) );
                        clusterCacheManager.startCaches( String.join( ",",  clusterCacheManager.getCacheNames()) );
                    }
                    else
                    {
                        cacheManager = new DefaultCacheManager(
                                        new ByteArrayInputStream( resourceStr.getBytes( StandardCharsets.UTF_8 ) ) );
                    }
                }
                catch ( IOException e )
                {
                    throw new RuntimeException(
                                    "Failed to construct ISPN cacheManger due to CLASSPATH xml stream read error.", e );
                }
            }
        }
        catch ( IOException e )
        {
            throw new RuntimeException(
                            "Failed to construct ISPN cacheManger due to CLASSPATH xml stream read error.", e );
        }
    }

    /**
     * Get a BasicCache instance. If the remote cache is enabled, it will match the named with remote.patterns.
     * If matched, it will create/return a RemoteCache. If not matched, an embedded cache will be created/returned to the caller.
     */
    public synchronized <K, V> BasicCacheHandle<K, V> getBasicCache( String named )
    {
        BasicCacheHandle handle = caches.computeIfAbsent( named, ( k ) -> {
            if ( remoteConfiguration.isEnabled() && remoteConfiguration.isRemoteCache( k ) )
            {
                RemoteCache<K, V> cache = null;
                try
                {
                    cache = remoteCacheManager.getCache( k );
                    if ( cache == null )
                    {
                        logger.warn( "Can not get remote cache, name: {}", k );
                        return null;
                    }
                }
                catch ( Exception e )
                {
                    logger.warn( "Get remote cache failed", e );
                    return null;
                }
                logger.info( "Get remote cache, name: {}", k );
                return new RemoteCacheHandle( k, cache, metricsManager, getCacheMetricPrefix( k ) );
            }
            return null;
        } );

        if ( handle == null )
        {
            handle = getCache( named );
        }

        return handle;
    }

    /**
     * Get named cache and verify that the cache obeys our expectations for clustering.
     * There is no way to find out the runtime type of generic type parameters and we need to pass the k/v class types.
     */
    public synchronized <K, V> CacheHandle<K, V> getClusterizableCache( String named, Class<K> kClass, Class<V> vClass )
    {
        verifyClusterizable( kClass, vClass );
        return getCache( named );
    }

    private <K, V> void verifyClusterizable( Class<K> kClass, Class<V> vClass )
    {
        if ( !Serializable.class.isAssignableFrom( kClass ) && !Externalizable.class.isAssignableFrom( kClass )
                        || !Serializable.class.isAssignableFrom( vClass ) && !Externalizable.class.isAssignableFrom(
                        vClass ) )
        {
            throw new RuntimeException( kClass + " or " + vClass + " is not Serializable/Externalizable" );
        }
    }

    /**
     * Retrieve an embedded cache with a pre-defined configuration (from infinispan.xml) or the default cache configuration.
     */
    public synchronized <K, V> CacheHandle<K, V> getCache( String named )
    {
        logger.debug( "Get embedded cache, name: {}", named );
        return (CacheHandle) caches.computeIfAbsent( named, ( k ) -> {
            Cache<K, V> cache;
            if ( clusterConfiguration != null
                            && clusterConfiguration.isEnabled()
                            && clusterCacheManager.cacheExists( k ) )
            {
                cache = clusterCacheManager.getCache( k );
            }
            else
            {
                cache = cacheManager.getCache( k );
            }
            return new CacheHandle( k, cache, metricsManager, getCacheMetricPrefix( k ) );
        } );
    }

    private String getCacheMetricPrefix( String named )
    {
        return metricsManager == null ? null : getSupername( metricsConfig.getNodePrefix(), INDY_METRIC_ISPN, named );
    }

    public synchronized Configuration getCacheConfiguration( String name )
    {
        if ( cacheManager == null )
        {
            throw new IllegalStateException( "Cannot access CacheManager. Indy seems to be in a state of shutdown." );
        }
        return cacheManager.getCacheConfiguration( name );
    }

    public synchronized Configuration getDefaultCacheConfiguration()
    {
        if ( cacheManager == null )
        {
            throw new IllegalStateException( "Cannot access CacheManager. Indy seems to be in a state of shutdown." );
        }

        return cacheManager.getDefaultCacheConfiguration();
    }

    public synchronized Configuration setCacheConfiguration( String name, Configuration config )
    {
        if ( cacheManager == null )
        {
            throw new IllegalStateException( "Cannot access CacheManager. Indy seems to be in a state of shutdown." );
        }
        return cacheManager.defineConfiguration( name, config );
    }

    @Override
    public synchronized void stop()
            throws IndyLifecycleException
    {
        logger.info( "Stopping Infinispan caches." );
        caches.forEach( ( name, cacheHandle ) -> cacheHandle.stop() );

        if ( cacheManager != null )
        {
            cacheManager.stop();
            cacheManager = null;
        }

        if ( remoteCacheManager != null )
        {
            remoteCacheManager.stop();
            remoteCacheManager = null;
        }
    }

    @Override
    public int getShutdownPriority()
    {
        return 10;
    }

    @Override
    public String getId()
    {
        return "infinispan-caches";
    }

    private String interpolateStrFromStream( InputStream inputStream, String path )
    {
        String configuration;
        try
        {
            configuration = IOUtils.toString( inputStream );
        }
        catch ( IOException e )
        {
            throw new RuntimeException( "Cannot read infinispan configuration from : " + path, e );
        }

        StringSearchInterpolator interpolator = new StringSearchInterpolator();
        interpolator.addValueSource( new PropertiesBasedValueSource( System.getProperties() ) );

        try
        {
            configuration = interpolator.interpolate( configuration );
        }
        catch ( InterpolationException e )
        {
            throw new RuntimeException( "Cannot resolve expressions in infinispan configuration from: " + path, e );
        }
        return configuration;
    }

    /**
     * For the ISPN merging, we should involve at least two different xml config scopes here,
     * one is from indy self default resource xml, another one is from customer's config xml.
     *
     * To prevent the error of EmbeddedCacheManager instances configured with same JMX domain,
     * ISPN should enable 'allowDuplicateDomains' attribute for per GlobalConfigurationBuilder build,
     * that will cost more price there for DefaultCacheManager construct and ConfigurationBuilder build.
     *
     * Since what we need here is simply parsing xml inputStreams to the defined configurations that ISPN
     * could accept, then merging the two stream branches into a entire one.
     * What classes this method uses from ISPN are:
     * {@link ConfigurationBuilderHolder}
     * {@link ParserRegistry}
     * {@link ConfigurationManager}
     *
     * @param config
     * @param path
     */
    private void mergedCachesFromConfig( String config, String path )
    {
        logger.debug( "[ISPN xml merge] cache config xml to merge:\n {}", config );
        // FIXME: here may cause ISPN000343 problem if your cache config has enabled distributed cache. Because distributed
        //       cache needs transport support, so if the cache manager does not enable it and then add this type of cache
        //       by defineConfiguration, it will report ISPN000343. So we should ensure the transport has been added by initialization.
        if ( cacheManager == null )
        {
            try
            {
                logger.info(
                        "Using {} resource Infinispan configuration to construct mergable cache configuration:\n\n{}\n\n", path,
                        config );
                cacheManager = new DefaultCacheManager(
                        new ByteArrayInputStream( config.getBytes( StandardCharsets.UTF_8 ) ) );
            }
            catch ( IOException e )
            {
                throw new RuntimeException(
                        String.format( "Failed to construct ISPN cacheManger due to %s xml stream read error.", path ),
                        e );
            }
        }

        final ConfigurationBuilderHolder holder = ( new ParserRegistry() ).parse( IOUtils.toInputStream( config ) );
        final ConfigurationManager manager = new ConfigurationManager( holder );

        final Set<String> definedCaches = cacheManager.getCacheNames();

        for ( String name : manager.getDefinedCaches() )
        {
            if ( definedCaches.isEmpty() || !definedCaches.contains( name ) )
            {
                logger.info( "[ISPN xml merge] Define cache: {} from {} config.", name, path );
                cacheManager.defineConfiguration( name, manager.getConfiguration( name, false ) );
            }
        }
    }

    public EmbeddedCacheManager getCacheManager()
    {
        return cacheManager;
    }
}


/**
 * Copyright Microsoft Corporation
 *
 * 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 com.microsoft.azure.storage.file;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Stack;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;

import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

import com.microsoft.azure.storage.Constants;
import com.microsoft.azure.storage.core.SR;
import com.microsoft.azure.storage.core.Utility;

/**
 * RESERVED FOR INTERNAL USE. A class used to deserialize a list of file ranges.
 */
final class FileRangeHandler extends DefaultHandler {

    private final Stack<String> elementStack = new Stack<String>();
    private StringBuilder bld = new StringBuilder();

    private final ArrayList<FileRange> fileRanges = new ArrayList<FileRange>();

    private long startOffset;
    private long endOffset;

    /**
     * Returns an ArrayList of File Ranges for the given stream.
     *
     * @return an ArrayList of File Ranges for the given stream.
     * @throws SAXException
     * @throws ParserConfigurationException
     * @throws IOException
     */
    protected static ArrayList<FileRange> getFileRanges(InputStream streamRef) throws ParserConfigurationException,
            SAXException, IOException {
        SAXParser saxParser = Utility.getSAXParser();
        FileRangeHandler handler = new FileRangeHandler();
        saxParser.parse(streamRef, handler);

        return handler.fileRanges;
    }

    @Override
    public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
        this.elementStack.push(localName);
    }

    @Override
    public void endElement(String uri, String localName, String qName) throws SAXException {
        String currentNode = this.elementStack.pop();

        // if the node popped from the stack and the localName don't match, the xml document is improperly formatted
        if (!localName.equals(currentNode)) {
            throw new SAXException(SR.INVALID_RESPONSE_RECEIVED);
        }

        String value = this.bld.toString();
        if (value.isEmpty()) {
            value = null;
        }

        if (FileConstants.FILE_RANGE_ELEMENT.equals(currentNode)) {
            final FileRange fileRangeRef = new FileRange(this.startOffset, this.endOffset);
            this.fileRanges.add(fileRangeRef);
        }
        else if (FileConstants.START_ELEMENT.equals(currentNode)) {
            this.startOffset = Long.parseLong(value);
        }
        else if (Constants.END_ELEMENT.equals(currentNode)) {
            this.endOffset = Long.parseLong(value);
        }

        this.bld = new StringBuilder();
    }

    @Override
    public void characters(char ch[], int start, int length) throws SAXException {
        this.bld.append(ch, start, length);
    }
}