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

import org.commonjava.indy.audit.ChangeSummary;
import org.commonjava.atlas.maven.ident.util.JoinString;
import org.eclipse.jgit.api.AddCommand;
import org.eclipse.jgit.api.CommitCommand;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.RmCommand;
import org.eclipse.jgit.api.Status;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.api.errors.JGitInternalException;
import org.eclipse.jgit.api.errors.NoFilepatternException;
import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.diff.DiffFormatter;
import org.eclipse.jgit.errors.NoWorkTreeException;
import org.eclipse.jgit.errors.RevisionSyntaxException;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.IndexDiff.StageState;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revplot.PlotCommit;
import org.eclipse.jgit.revplot.PlotCommitList;
import org.eclipse.jgit.revplot.PlotLane;
import org.eclipse.jgit.revplot.PlotWalk;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevTree;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
import org.eclipse.jgit.treewalk.CanonicalTreeParser;
import org.eclipse.jgit.treewalk.FileTreeIterator;
import org.eclipse.jgit.treewalk.filter.AndTreeFilter;
import org.eclipse.jgit.treewalk.filter.PathFilter;
import org.eclipse.jgit.treewalk.filter.TreeFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.enterprise.context.ApplicationScoped;
import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

import static org.apache.commons.lang.StringUtils.isEmpty;
import static org.apache.commons.lang.StringUtils.join;
import static org.commonjava.indy.audit.ChangeSummary.SYSTEM_USER;

@ApplicationScoped
public class GitManager
{

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

    private final Git git;

    private final String email;

    private final Repository repo;

    private final File rootDir;

    private final GitConfig config;

    private final ReentrantLock lockObject = new ReentrantLock();

    public GitManager( final GitConfig config )
        throws GitSubsystemException
    {
        this.config = config;
        rootDir = config.getContentDir();
        final String cloneUrl = config.getCloneFrom();

        boolean checkUpdate = false;
        if ( cloneUrl != null )
        {
            logger.info( "Cloning: {} into: {}", cloneUrl, rootDir );
            if ( rootDir.isDirectory() )
            {
                checkUpdate = true;
            }
            else
            {
                final boolean mkdirs = rootDir.mkdirs();
                logger.info( "git dir {} (mkdir result: {}; is directory? {}) contains:\n  {}", rootDir, mkdirs,
                             rootDir.isDirectory(), join( rootDir.listFiles(), "\n  " ) );
                try
                {
                    Git.cloneRepository()
                       .setURI( cloneUrl )
                       .setDirectory( rootDir )
                       .setRemote( "origin" )
                       .call();
                }
                catch ( final GitAPIException e )
                {
                    throw new GitSubsystemException( "Failed to clone remote URL: {} into: {}. Reason: {}", e,
                                                     cloneUrl, rootDir, e.getMessage() );
                }
            }
        }

        final File dotGitDir = new File( rootDir, ".git" );

        logger.info( "Setting up git manager for: {}", dotGitDir );
        try
        {
            repo = new FileRepositoryBuilder().readEnvironment()
                                              .setGitDir( dotGitDir )
                                              .build();
        }
        catch ( final IOException e )
        {
            throw new GitSubsystemException( "Failed to create Repository instance for: {}. Reason: {}", e, dotGitDir,
                                             e.getMessage() );
        }

        String[] preExistingFromCreate = null;
        if ( !dotGitDir.isDirectory() )
        {
            preExistingFromCreate = rootDir.list();

            try
            {
                repo.create();
            }
            catch ( final IOException e )
            {
                throw new GitSubsystemException( "Failed to create git repo: {}. Reason: {}", e, rootDir,
                                                 e.getMessage() );
            }
        }

        String originUrl = repo.getConfig()
                               .getString( "remote", "origin", "url" );
        if ( originUrl == null )
        {
            originUrl = cloneUrl;
            logger.info( "Setting origin URL: {}", originUrl );

            repo.getConfig()
                .setString( "remote", "origin", "url", originUrl );

            repo.getConfig()
                .setString( "remote", "origin", "fetch", "+refs/heads/*:refs/remotes/origin/*" );
        }

        String email = repo.getConfig()
                           .getString( "user", null, "email" );

        if ( email == null )
        {
            email = config.getUserEmail();
        }

        if ( email == null )
        {
            try
            {
                email = "indy@" + InetAddress.getLocalHost()
                                              .getCanonicalHostName();

            }
            catch ( final UnknownHostException e )
            {
                throw new GitSubsystemException( "Failed to resolve 'localhost'. Reason: {}", e, e.getMessage() );
            }
        }

        if ( repo.getConfig()
                 .getString( "user", null, "email" ) == null )
        {
            repo.getConfig()
                .setString( "user", null, "email", email );
        }

        this.email = email;

        git = new Git( repo );

        if ( preExistingFromCreate != null && preExistingFromCreate.length > 0 )
        {
            addPaths( new ChangeSummary( SYSTEM_USER, "Committing pre-existing files." ),
                               preExistingFromCreate );
            commit();
        }

        if ( checkUpdate )
        {
            pullUpdates();
        }
    }

    public GitManager addFiles( final ChangeSummary summary, final File... files )
        throws GitSubsystemException
    {
        return addFiles( summary, Arrays.asList( files ) );
    }

    public GitManager addFiles( final ChangeSummary summary, final Collection<File> files )
        throws GitSubsystemException
    {
        final Set<String> paths = new HashSet<>();
        for ( final File f : files )
        {
            final String path = relativize( f );

            if ( path != null && path.length() > 0 )
            {
                paths.add( path );
            }
        }

        return addPaths( summary, paths );
    }

    private String relativize( final File f )
    {
        return Paths.get( rootDir.toURI() )
                    .relativize( Paths.get( f.toURI() ) )
                    .toString();
    }

    public GitManager addPaths( final ChangeSummary summary, final String... paths )
        throws GitSubsystemException
    {
        return addPaths( summary, Arrays.asList( paths ) );
    }

    public GitManager addPaths( final ChangeSummary summary, final Collection<String> paths )
        throws GitSubsystemException
    {
        lockAnd(me->{
/*
            if ( !verifyChangesExist( paths ) )
            {
                logger.info( "No actual changes in:\n  {}\n\nSkipping commit.", join( paths, "\n  " ) );
                return this;
            }
*/

            try
            {
                final AddCommand add = git.add();
                for ( final String filepath : paths )
                {
                    add.addFilepattern( filepath );
                }

                logger.info( "Adding:\n  " + join( paths, "\n  " ) + "\n\nSummary: " + summary );

                add.call();
                changelogEntries.add( new ChangelogEntry( summary.getUser(), buildMessage( summary, paths ), paths ) );
            }
            catch ( final NoFilepatternException e )
            {
                throw new GitSubsystemException( "Cannot add to git: " + e.getMessage(), e );
            }
            catch ( final GitAPIException e )
            {
                throw new GitSubsystemException( "Cannot add to git: " + e.getMessage(), e );
            }

            return me;
        });

        return this;
    }

/*
    private boolean verifyChangesExist( final Collection<String> paths )
        throws GitSubsystemException
    {
        return lockAnd(me->{
            try
            {
                final DiffFormatter formatter = new DiffFormatter( System.out );
                formatter.setRepository( repo );

                // resolve the HEAD object
                final ObjectId oid = repo.resolve( Constants.HEAD );
                if ( oid == null )
                {
                    // if there's no head, then these must be real changes...
                    return true;
                }

                // reset a new tree object to the HEAD
                final RevWalk walk = new RevWalk( repo );
                final RevCommit commit = walk.parseCommit( oid );
                final RevTree treeWalk = walk.parseTree( commit );

                // construct filters for the paths we're trying to add/commit
                final List<TreeFilter> filters = new ArrayList<>();
                for ( final String path : paths )
                {
                    filters.add( PathFilter.create( path ) );
                }

                // we're interested in trees with an actual diff. This should improve walk performance.
                filters.add( TreeFilter.ANY_DIFF );

                // set the path filters from above
                walk.setTreeFilter( AndTreeFilter.create( filters ) );

                // setup the tree for doing the comparison vs. uncommitted files
                final CanonicalTreeParser tree = new CanonicalTreeParser();
                final ObjectReader oldReader = repo.newObjectReader();
                try
                {
                    tree.reset( oldReader, treeWalk.getId() );
                }
                finally
                {
                    oldReader.release();
                }
                walk.dispose();

                // this iterator will actually scan the uncommitted files for diff'ing
                final FileTreeIterator files = new FileTreeIterator( repo );

                // do the scan.
                final List<DiffEntry> entries = formatter.scan( tree, files );

                // we're not interested in WHAT the differences are, only that there are differences.
                return entries != null && !entries.isEmpty();
            }
            catch ( final IOException e )
            {
                throw new GitSubsystemException( "Failed to scan for actual changes among: %s. Reason: %s", e, paths,
                                                 e.getMessage() );
            }
        });
    }
*/

    private String buildMessage( final ChangeSummary summary, final Collection<String> paths )
    {
        final StringBuilder message =
                        new StringBuilder().append( summary.getSummary() );
        if ( config.isCommitFileManifestsEnabled() )
        {
            message.append( "\n\nFiles changed:\n" )
                   .append( join( paths, "\n" ) );
        }

        return message.toString();
    }

    public final static String COMMIT_CHANGELOG_ENTRIES = "Commit changelog entries";

    /*
     * For each modification/deletion event, GitManager add/rm those files in the current status BUT NOT COMMIT.
     * It append a new entry to the list of changelog entries pending for the next commit.
     */
    private List<ChangelogEntry> changelogEntries = Collections.synchronizedList( new ArrayList<>(  ) );

    /**
     * Commit the changelog entries
     * @return size of changelog entries being committed
     */
    public int commit()
                    throws GitSubsystemException
    {
        int committed = lockAnd(me->{
            final int size = changelogEntries.size();
            if ( !changelogEntries.isEmpty() )
            {
                try
                {
                    CommitCommand commit = git.commit();
                    StringBuilder sb = new StringBuilder().append( COMMIT_CHANGELOG_ENTRIES + "\n\n" );
                    for ( ChangelogEntry et : changelogEntries )
                    {
                        sb.append( et.getUsername() + " " + et.getTimestamp() + " " + et.getMessage() );
                        sb.append( "\n\n" );
                    }
                    String message = sb.toString();
                    logger.info( message );
                    commit.setMessage( message ).setAuthor( SYSTEM_USER, email ).call();
                    changelogEntries.clear();
                }
                catch ( final JGitInternalException | GitAPIException e )
                {
                    throw new GitSubsystemException( "Cannot commit to git: " + e.getMessage(), e );
                }
            }
            return size;
        });

        return committed;
    }

    public GitManager delete( final ChangeSummary summary, final File... deleted )
        throws GitSubsystemException
    {
        return delete( summary, Arrays.asList( deleted ) );
    }

    public GitManager delete( final ChangeSummary summary, final Collection<File> files )
        throws GitSubsystemException
    {
        final Set<String> paths = new HashSet<>();
        for ( final File f : files )
        {
            final String path = relativize( f );

            if ( path != null && path.length() > 0 )
            {
                paths.add( path );
            }
        }

        return deletePaths( summary, paths );
    }

    public GitManager deletePaths( final ChangeSummary summary, final String... paths )
        throws GitSubsystemException
    {
        return deletePaths( summary, Arrays.asList( paths ) );
    }

    public GitManager deletePaths( final ChangeSummary summary, final Collection<String> paths )
        throws GitSubsystemException
    {
        lockAnd(me->{
            try
            {
                RmCommand rm = git.rm();

                for ( final String path : paths )
                {
                    rm = rm.addFilepattern( path );
                }

                logger.info( "Deleting:\n  " + join( paths, "\n  " ) + "\n\nSummary: " + summary );
                rm.call();

                changelogEntries.add( new ChangelogEntry( summary.getUser(), buildMessage( summary, paths ), paths ) );
            }
            catch ( final NoFilepatternException e )
            {
                throw new GitSubsystemException( "Cannot remove from git: " + e.getMessage(), e );
            }
            catch ( final JGitInternalException | GitAPIException e )
            {
                throw new GitSubsystemException( "Cannot remove from git: " + e.getMessage(), e );
            }

            return me;
        });

        return this;
    }

    public ChangeSummary getHeadCommit( final File f )
        throws GitSubsystemException
    {
        return lockAnd(me->{
            try
            {
                final ObjectId oid = repo.resolve( "HEAD" );

                final PlotWalk pw = new PlotWalk( repo );
                final RevCommit rc = pw.parseCommit( oid );
                pw.markStart( rc );

                final String filepath = relativize( f );

                pw.setTreeFilter( AndTreeFilter.create( PathFilter.create( filepath ), TreeFilter.ANY_DIFF ) );

                final PlotCommitList<PlotLane> cl = new PlotCommitList<>();
                cl.source( pw );
                cl.fillTo( 1 );

                final PlotCommit<PlotLane> commit = cl.get( 0 );

                return toChangeSummary( commit );
            }
            catch ( RevisionSyntaxException | IOException e )
            {
                throw new GitSubsystemException( "Failed to resolve HEAD commit for: %s. Reason: %s", e, f, e.getMessage() );
            }
        });
    }

    public List<ChangeSummary> getChangelog( final File f, final int start, final int length )
        throws GitSubsystemException
    {
        return lockAnd(me->{
            if ( length == 0 )
            {
                return Collections.emptyList();
            }

            try
            {
                final ObjectId oid = repo.resolve( Constants.HEAD );

                final PlotWalk pw = new PlotWalk( repo );
                final RevCommit rc = pw.parseCommit( oid );
                toChangeSummary( rc );
                pw.markStart( rc );

                final String filepath = relativize( f );
                logger.info( "Getting changelog for: {} (start: {}, length: {})", filepath, start, length );

                if ( !isEmpty( filepath ) && !filepath.equals( "/" ) )
                {
                    pw.setTreeFilter( AndTreeFilter.create( PathFilter.create( filepath ), TreeFilter.ANY_DIFF ) );
                }
                else
                {
                    pw.setTreeFilter( TreeFilter.ANY_DIFF );
                }

                final List<ChangeSummary> changelogs = new ArrayList<ChangeSummary>();
                int count = 0;
                final int stop = length > 0 ? length + 1 : 0;
                RevCommit commit = null;
                while ( ( commit = pw.next() ) != null && ( stop < 1 || count < stop ) )
                {
                    if ( count < start )
                    {
                        count++;
                        continue;
                    }

                    //                printFiles( commit );
                    changelogs.add( toChangeSummary( commit ) );
                    count++;
                }

                if ( length < -1 )
                {
                    final int remove = ( -1 * length ) - 1;
                    for ( int i = 0; i < remove; i++ )
                    {
                        changelogs.remove( changelogs.size() - 1 );
                    }
                }

                return changelogs;
            }
            catch ( RevisionSyntaxException | IOException e )
            {
                throw new GitSubsystemException( "Failed to resolve HEAD commit for: %s. Reason: %s", e, f, e.getMessage() );
            }
        });
    }

    //    private void printFiles( final RevCommit commit )
    //        throws IOException
    //    {
    //        final RevWalk tree = new RevWalk( repo );
    //        final RevCommit parent = commit.getParentCount() > 0 ? tree.parseCommit( commit.getParent( 0 )
    //                                                                                .getId() ) : null;
    //
    //        final DiffFormatter df = new DiffFormatter( DisabledOutputStream.INSTANCE );
    //        df.setRepository( repo );
    //        df.setDiffComparator( RawTextComparator.DEFAULT );
    //        df.setDetectRenames( true );
    //
    //        final List<DiffEntry> diffs;
    //        if ( parent == null )
    //        {
    //            diffs =
    //                df.scan( new EmptyTreeIterator(),
    //                         new CanonicalTreeParser( null, tree.getObjectReader(), commit.getTree() ) );
    //        }
    //        else
    //        {
    //            diffs = df.scan( parent.getTree(), commit.getTree() );
    //        }
    //
    //        for ( final DiffEntry diff : diffs )
    //        {
    //            logger.info( "({} {} {}", diff.getChangeType()
    //                                          .name(), diff.getNewMode()
    //                                                       .getBits(), diff.getNewPath() );
    //        }
    //    }

    private ChangeSummary toChangeSummary( final RevCommit commit )
    {
        final PersonIdent who = commit.getAuthorIdent();
        final Date when = new Date( TimeUnit.MILLISECONDS.convert( commit.getCommitTime(), TimeUnit.SECONDS ) );
        return new ChangeSummary( who.getName(), commit.getFullMessage(), when, commit.getId()
                                                                                .name() );
    }

    public GitManager pullUpdates()
        throws GitSubsystemException
    {
        return pullUpdates( ConflictStrategy.merge );
    }

    public GitManager pullUpdates( final ConflictStrategy strategy )
        throws GitSubsystemException
    {
        lockAnd( me->{
            try
            {
                git.pull()
                   .setStrategy( strategy.mergeStrategy() )
                   .setRemoteBranchName( config.getRemoteBranchName() )
                   .setRebase( true )
                   .call();
            }
            catch ( final GitAPIException e )
            {
                throw new GitSubsystemException( "Cannot pull content updates via git: " + e.getMessage(), e );
            }

            return me;
        } );

        return this;
    }

    public GitManager pushUpdates()
        throws GitSubsystemException
    {
        lockAnd(me->{
            try
            {
                git.push()
                   .call();
            }
            catch ( final GitAPIException e )
            {
                throw new GitSubsystemException( "Cannot push content updates via git: " + e.getMessage(), e );
            }

            return me;
        });

        return this;
    }

    public String getOriginUrl()
    {
        return git.getRepository()
                  .getConfig()
                  .getString( "remote", "origin", "url" );
    }

    public GitManager addExternallyChangedFiles( final ChangeSummary changeSummary )
                    throws GitSubsystemException
    {
        lockAnd((me)->{
            Status status = getStatus();

            final Set<String> toAdd = new HashSet<>();
            final Set<String> modified = status.getModified();
            if ( modified != null && !modified.isEmpty() )
            {
                toAdd.addAll( modified );
            }

            final Set<String> untracked = status.getUntracked();
            if ( untracked != null && !untracked.isEmpty() )
            {
                toAdd.addAll( untracked );
            }

            final Set<String> untrackedFolders = status.getUntrackedFolders();
            if ( untrackedFolders != null && !untrackedFolders.isEmpty() )
            {
                toAdd.addAll( untrackedFolders );
            }

            if ( !toAdd.isEmpty() )
            {
                addPaths( changeSummary, toAdd );
            }
            return me;
        });

        return this;
    }

    public GitManager deleteExternallyRemovedFiles( final ChangeSummary changeSummary )
        throws GitSubsystemException
    {
        lockAnd((me)->{
            Status status = getStatus();

            final Set<String> removed = status.getRemoved();
            if ( removed != null && !removed.isEmpty() )
            {
                deletePaths( changeSummary, removed );
            }
            return me;
        });

        return this;
    }

    private Status getStatus() throws GitSubsystemException
    {
        Status status;
        try
        {
            status = git.status().call();
        }
        catch ( NoWorkTreeException | GitAPIException e )
        {
            throw new GitSubsystemException( "Failed to retrieve status of: %s. Reason: %s", e, rootDir, e.getMessage() );
        }

        final Map<String, StageState> css = status.getConflictingStageState();
        if ( !css.isEmpty() )
        {
            throw new GitSubsystemException( "%s contains conflicts. Cannot auto-commit.\n  %s", rootDir,
                                             new JoinString( "\n  ", css.entrySet() ) );
        }
        return status;
    }

    private interface GitFunction<T>
    {
        T execute( GitManager me )
                throws GitSubsystemException;
    }

    private <T> T lockAnd(GitFunction<T> operation)
            throws GitSubsystemException
    {
        lockObject.lock();
        try
        {
            return operation.execute( this );
        }
        finally
        {
            lockObject.unlock();
        }
    }
}


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

import org.commonjava.indy.conf.IndyConfigInfo;
import org.commonjava.web.config.annotation.ConfigName;
import org.commonjava.web.config.annotation.SectionName;
import org.infinispan.client.hotrod.impl.ConfigurationProperties;

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

import static org.apache.commons.lang3.StringUtils.isNotBlank;

@SectionName( "infinispan-remote" )
@ApplicationScoped
public class ISPNRemoteConfiguration
        implements IndyConfigInfo
{
    private static final String DEFAULT_REMOTE_SERVER = "localhost";

    private static final Boolean DEFAULT_ENABLED = Boolean.FALSE;

    private Boolean enabled;

    private String remoteServer;

    private Integer hotrodPort;

    private String remotePatterns;

    public ISPNRemoteConfiguration()
    {
    }

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

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

    public String getRemoteServer()
    {
        return remoteServer == null ? DEFAULT_REMOTE_SERVER : remoteServer;
    }

    @ConfigName( "remote.server" )
    public void setRemoteServer( String remoteServer )
    {
        this.remoteServer = remoteServer;
    }

    public Integer getHotrodPort()
    {
        return hotrodPort == null ? ConfigurationProperties.DEFAULT_HOTROD_PORT : hotrodPort;
    }

    @ConfigName( "hotrod.port" )
    public void setHotrodPort( Integer hotrodPort )
    {
        this.hotrodPort = hotrodPort;
    }

    public String getRemotePatterns()
    {
        return remotePatterns;
    }

    @ConfigName( "remote.patterns" )
    public void setRemotePatterns( String remotePatterns )
    {
        this.remotePatterns = remotePatterns;
    }

    // utils

    public boolean isRemoteCache( String cacheName )
    {
        if ( remotePatterns == null )
        {
            return false;
        }

        String[] patterns = remotePatterns.split( "," );
        for ( String pattern : patterns )
        {
            if ( isNotBlank( pattern ) )
            {
                if ( cacheName.matches( pattern ) || cacheName.equals( pattern ) )
                {
                    return true;
                }
            }
        }

        return false;
    }

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

    @Override
    public InputStream getDefaultConfig()
    {
        return Thread.currentThread()
                     .getContextClassLoader()
                     .getResourceAsStream( "default-infinispan-remote.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.metrics.zabbix.reporter;

import com.codahale.metrics.Counter;
import com.codahale.metrics.Gauge;
import com.codahale.metrics.Histogram;
import com.codahale.metrics.Meter;
import com.codahale.metrics.Metered;
import com.codahale.metrics.MetricFilter;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.ScheduledReporter;
import com.codahale.metrics.Snapshot;
import com.codahale.metrics.Timer;
import org.commonjava.indy.metrics.exception.IndyMetricsException;
import org.commonjava.indy.metrics.zabbix.sender.DataObject;
import org.commonjava.indy.metrics.zabbix.sender.IndyZabbixSender;
import org.commonjava.indy.metrics.zabbix.sender.SenderResult;
import org.commonjava.indy.subsys.http.IndyHttpException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import java.util.concurrent.TimeUnit;

/**
 * Created by xiabai on 3/29/17.
 */
public class IndyZabbixReporter
                extends ScheduledReporter
{
    private static final Logger logger = LoggerFactory.getLogger( IndyZabbixReporter.class );

    private final IndyZabbixSender indyZabbixSender;

    private final String hostName;

    private final String prefix;

    private final String suffix;

    public static Builder forRegistry( MetricRegistry registry )
    {
        return new Builder( registry );
    }

    public static class Builder
    {

        private final MetricRegistry registry;

        private String name = "zabbix-reporter";

        private TimeUnit rateUnit;

        private TimeUnit durationUnit;

        private MetricFilter filter;

        private String hostName;

        private String prefix = "";

        private String suffix = "";

        private String zabbixHostName;

        private IndyZabbixSender indyZabbixSender;

        public Builder( MetricRegistry registry )
        {
            this.registry = registry;

            this.rateUnit = TimeUnit.SECONDS;
            this.durationUnit = TimeUnit.MILLISECONDS;
            this.filter = MetricFilter.ALL;

        }

        /**
         * Convert rates to the given time unit.
         *
         * @param rateUnit
         *            a unit of time
         * @return {@code this}
         */
        public Builder convertRatesTo( TimeUnit rateUnit )
        {
            this.rateUnit = rateUnit;
            return this;
        }

        /**
         * Convert durations to the given time unit.
         *
         * @param durationUnit
         *            a unit of time
         * @return {@code this}
         */
        public Builder convertDurationsTo( TimeUnit durationUnit )
        {
            this.durationUnit = durationUnit;
            return this;
        }

        /**
         * Only report metrics which match the given filter.
         *
         * @param filter
         *            a {@link MetricFilter}
         * @return {@code this}
         */
        public Builder filter( MetricFilter filter )
        {
            this.filter = filter;
            return this;
        }

        /**
         * default register name is "zabbix-reporter".
         *
         * @param name
         * @return
         */
        public Builder name( String name )
        {
            this.name = name;
            return this;
        }

        public Builder hostName( String hostName )
        {
            this.hostName = hostName;
            return this;
        }

        public Builder prefix( String prefix )
        {
            this.prefix = prefix + ".";
            return this;
        }

        public Builder suffix( String suffix )
        {
            this.suffix = suffix;
            return this;
        }

        public Builder zabbixHostName( String zabbixHostName )
        {
            this.zabbixHostName = zabbixHostName;
            return this;
        }

        public Builder zabbixSender( IndyZabbixSender indyZabbixSender )
        {
            this.indyZabbixSender = indyZabbixSender;
            return this;
        }

        /**
         * Builds a {@link IndyZabbixReporter} with the given properties.
         *
         * @return a {@link IndyZabbixReporter}
         */
        public IndyZabbixReporter build( IndyZabbixSender zabbixSender )
        {
            if ( hostName == null )
            {
                hostName = HostUtil.getHostName();
                logger.info( name + " detect hostName: " + hostName );
            }
            return new IndyZabbixReporter( registry, name, rateUnit, durationUnit, filter, zabbixSender, hostName,
                                           prefix, suffix );
        }
    }

    private IndyZabbixReporter( MetricRegistry registry, String name, TimeUnit rateUnit, TimeUnit durationUnit,
                                MetricFilter filter, IndyZabbixSender indyZabbixSender, String hostName, String prefix,
                                String suffix )
    {
        super( registry, name, filter, rateUnit, durationUnit );
        this.indyZabbixSender = indyZabbixSender;
        this.hostName = hostName;
        this.prefix = prefix;
        this.suffix = suffix;
    }

    private DataObject toDataObject( String key, String keySuffix, Object value, long clock )
    {
        return DataObject.builder()
                         .host( hostName )
                         .key( prefix + key + keySuffix + suffix )
                         .clock( clock )
                         .value( String.valueOf( value ) )
                         .build();
    }

    /**
     * for histograms.
     *
     * @param key
     * @param snapshot
     * @param dataObjectList
     */
    private void addSnapshotDataObject( String key, Snapshot snapshot, long clock, List<DataObject> dataObjectList )
    {
        dataObjectList.add( toDataObject( key, ".min", snapshot.getMin(), clock ) );
        dataObjectList.add( toDataObject( key, ".max", snapshot.getMax(), clock ) );
        dataObjectList.add( toDataObject( key, ".mean", snapshot.getMean(), clock ) );
        dataObjectList.add( toDataObject( key, ".stddev", snapshot.getStdDev(), clock ) );
        dataObjectList.add( toDataObject( key, ".median", snapshot.getMedian(), clock ) );
        dataObjectList.add( toDataObject( key, ".75th", snapshot.get75thPercentile(), clock ) );
        dataObjectList.add( toDataObject( key, ".95th", snapshot.get95thPercentile(), clock ) );
        dataObjectList.add( toDataObject( key, ".98th", snapshot.get98thPercentile(), clock ) );
        dataObjectList.add( toDataObject( key, ".99th", snapshot.get99thPercentile(), clock ) );
        dataObjectList.add( toDataObject( key, ".99.9th", snapshot.get999thPercentile(), clock ) );
    }

    /**
     * for timer.
     *
     * @param key
     * @param snapshot
     * @param dataObjectList
     */
    private void addSnapshotDataObjectWithConvertDuration( String key, Snapshot snapshot, long clock,
                                                           List<DataObject> dataObjectList )
    {
        dataObjectList.add( toDataObject( key, ".min", convertDuration( snapshot.getMin() ), clock ) );
        dataObjectList.add( toDataObject( key, ".max", convertDuration( snapshot.getMax() ), clock ) );
        dataObjectList.add( toDataObject( key, ".mean", convertDuration( snapshot.getMean() ), clock ) );
        dataObjectList.add( toDataObject( key, ".stddev", convertDuration( snapshot.getStdDev() ), clock ) );
        dataObjectList.add( toDataObject( key, ".median", convertDuration( snapshot.getMedian() ), clock ) );
        dataObjectList.add( toDataObject( key, ".75th", convertDuration( snapshot.get75thPercentile() ), clock ) );
        dataObjectList.add( toDataObject( key, ".95th", convertDuration( snapshot.get95thPercentile() ), clock ) );
        dataObjectList.add( toDataObject( key, ".98th", convertDuration( snapshot.get98thPercentile() ), clock ) );
        dataObjectList.add( toDataObject( key, ".99th", convertDuration( snapshot.get99thPercentile() ), clock ) );
        dataObjectList.add( toDataObject( key, ".99.9th", convertDuration( snapshot.get999thPercentile() ), clock ) );
    }

    private void addMeterDataObject( String key, Metered meter, long clock, List<DataObject> dataObjectList )
    {
        dataObjectList.add( toDataObject( key, ".count", meter.getCount(), clock ) );
        dataObjectList.add( toDataObject( key, ".meanRate", convertRate( meter.getMeanRate() ), clock ) );
        dataObjectList.add( toDataObject( key, ".1-minuteRate", convertRate( meter.getOneMinuteRate() ), clock ) );
        dataObjectList.add( toDataObject( key, ".5-minuteRate", convertRate( meter.getFiveMinuteRate() ), clock ) );
        dataObjectList.add( toDataObject( key, ".15-minuteRate", convertRate( meter.getFifteenMinuteRate() ), clock ) );
    }

    @SuppressWarnings( "rawtypes" )
    @Override
    public void report( SortedMap<String, Gauge> gauges, SortedMap<String, Counter> counters,
                        SortedMap<String, Histogram> histograms, SortedMap<String, Meter> meters,
                        SortedMap<String, Timer> timers )
    {
        final long clock = System.currentTimeMillis() / 1000;
        List<DataObject> dataObjectList = new LinkedList<DataObject>();
        for ( Map.Entry<String, Gauge> entry : gauges.entrySet() )
        {
            DataObject dataObject =
                            toDataObject( entry.getKey(), "", String.valueOf( entry.getValue().getValue() ), clock );
            dataObjectList.add( dataObject );
        }

        for ( Map.Entry<String, Counter> entry : counters.entrySet() )
        {
            DataObject dataObject =
                            toDataObject( entry.getKey(), "", String.valueOf( entry.getValue().getCount() ), clock );
            dataObjectList.add( dataObject );
        }

        for ( Map.Entry<String, Histogram> entry : histograms.entrySet() )
        {
            Histogram histogram = entry.getValue();
            Snapshot snapshot = histogram.getSnapshot();
            addSnapshotDataObject( entry.getKey(), snapshot, clock, dataObjectList );
        }

        for ( Map.Entry<String, Meter> entry : meters.entrySet() )
        {
            Meter meter = entry.getValue();
            addMeterDataObject( entry.getKey(), meter, clock, dataObjectList );
        }

        for ( Map.Entry<String, Timer> entry : timers.entrySet() )
        {
            Timer timer = entry.getValue();
            addMeterDataObject( entry.getKey(), timer, clock, dataObjectList );
            addSnapshotDataObjectWithConvertDuration( entry.getKey(), timer.getSnapshot(), clock, dataObjectList );
        }

        try
        {
            SenderResult senderResult = indyZabbixSender.send( dataObjectList, clock );
            if ( !senderResult.success() )
            {
                logger.warn( "report metrics to zabbix not success!" + senderResult );
            }
            else if ( logger.isDebugEnabled() )
            {
                logger.info( "report metrics to zabbix success. " + senderResult );
            }
        }
        catch ( IOException e )
        {
            logger.error( "report metrics to zabbix error! " + e );
            e.printStackTrace();
        }
        catch ( IndyMetricsException e )
        {
            logger.error( "Indy metrics config error " + e );
            e.printStackTrace();
        }
        catch ( IndyHttpException e )
        {
            logger.error( "Indy http client error " + e );
            e.printStackTrace();
        }
    }

}


/**
 * Copyright (C) 2011-2018 Red Hat, Inc. (https://github.com/Commonjava/indy)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.commonjava.indy.core.change;

import org.apache.commons.lang.StringUtils;
import org.commonjava.indy.audit.ChangeSummary;
import org.commonjava.indy.change.event.ArtifactStoreEnablementEvent;
import org.commonjava.indy.change.event.IndyStoreErrorEvent;
import org.commonjava.indy.conf.IndyConfiguration;
import org.commonjava.indy.core.expire.IndySchedulerException;
import org.commonjava.indy.core.expire.ScheduleManager;
import org.commonjava.indy.core.expire.SchedulerEvent;
import org.commonjava.indy.core.expire.SchedulerTriggerEvent;
import org.commonjava.indy.data.IndyDataException;
import org.commonjava.indy.data.StoreDataManager;
import org.commonjava.indy.measure.annotation.Measure;
import org.commonjava.indy.measure.annotation.MetricNamed;
import org.commonjava.indy.model.core.ArtifactStore;
import org.commonjava.indy.model.core.StoreKey;
import org.commonjava.indy.model.core.io.IndyObjectMapper;
import org.commonjava.maven.galley.event.EventMetadata;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.event.Observes;
import javax.inject.Inject;
import java.io.IOException;

import static org.commonjava.indy.measure.annotation.MetricNamed.DEFAULT;

@ApplicationScoped
public class StoreEnablementManager
{

    public static final String DISABLE_TIMEOUT = "Disable-Timeout";

    public static final int TIMEOUT_NEVER_DISABLE = -1;

    public static final int TIMEOUT_USE_DEFAULT = 0;

    @Inject
    private StoreDataManager storeDataManager;

    @Inject
    private ScheduleManager scheduleManager;

    @Inject
    private IndyObjectMapper objectMapper;

    @Inject
    private IndyConfiguration config;

    //FIXME: Convert to using ArtifactStoreEnablementEvent.
    @Measure( timers = @MetricNamed( DEFAULT ) )
    public void onStoreEnablementChange( @Observes ArtifactStoreEnablementEvent event )
    {
        if ( event.isPreprocessing() )
        {
            return;
        }

        for ( ArtifactStore store : event )
        {
            if ( ! event.isDisabling() )
            {
                try
                {
                    cancelReEnablementTimeout( store.getKey() );
                }
                catch ( IndySchedulerException e )
                {
                    Logger logger = LoggerFactory.getLogger( getClass() );
                    logger.error( String.format( "Failed to delete re-enablement job for %s.", store.getKey() ), e );
                }
            }
        }
    }

//    public void onStoreUpdate( @Observes ArtifactStorePostUpdateEvent event )
//    {
//        for ( ArtifactStore store : event )
//        {
//            if ( store.isDisabled() )
//            {
//                String toStr = store.getMetadata( DISABLE_TIMEOUT );
//                if ( isNotEmpty( toStr ) )
//                {
//                    int timeout = Integer.parseInt( toStr );
//                    try
//                    {
//                        setReEnablementTimeout( store.getKey(), timeout );
//                    }
//                    catch ( IndySchedulerException e )
//                    {
//                        Logger logger = LoggerFactory.getLogger( getClass() );
//                        logger.error( String.format( "Failed to schedule re-enablement of %s.", store.getKey() ), e );
//                    }
//                }
//            }
////            else
////            {
////                try
////                {
////                    cancelReEnablementTimeout( store.getKey() );
////                }
////                catch ( IndySchedulerException e )
////                {
////                    Logger logger = LoggerFactory.getLogger( getClass() );
////                    logger.error( String.format( "Failed to delete re-enablement job for %s.", store.getKey() ), e );
////                }
////            }
//        }
//    }

    @Measure( timers = @MetricNamed( DEFAULT ) )
    public void onStoreError( @Observes IndyStoreErrorEvent evt )
    {
        Logger logger = LoggerFactory.getLogger( getClass() );
        StoreKey key = evt.getStoreKey();
        Throwable error = evt.getError();
        try
        {
            ArtifactStore store = storeDataManager.getArtifactStore( key );
            if ( store == null )
            {
                logger.warn( "Attempt to disable missing repo! Skipping." );
                return;
            }

            store = store.copyOf();

            int disableTimeout = store.getDisableTimeout();
            if ( disableTimeout <= TIMEOUT_NEVER_DISABLE )
            {
                logger.debug( "Disable-timeout set to {}, will never disable the repo", disableTimeout );
                store.setDisabled( false );
            }
            else
            {
                store.setDisabled( true );
                final ChangeSummary changeSummary = new ChangeSummary( ChangeSummary.SYSTEM_USER, String.format(
                        "Disabling %s due to error: %s\n\nStack Trace:\n  %s", key, error,
                        StringUtils.join( error.getStackTrace(), "\n  " ) ) );

                storeDataManager.storeArtifactStore( store, changeSummary, false, true, new EventMetadata() );

                logger.warn( "{} has been disabled due to store-level error: {}\n Will re-enable in {} seconds.", key,
                             error, disableTimeout == TIMEOUT_USE_DEFAULT ? config.getStoreDisableTimeoutSeconds() : disableTimeout );

                // TODO: How is it this doesn't duplicate the event handler method onStoreUpdate()...we're updating the store just above here.
                setReEnablementTimeout( key );
            }
        }
        catch ( IndyDataException e )
        {
            logger.error( String.format( "Failed to disable %s on error: %s", key, error ), e);
        }
        catch ( IndySchedulerException e )
        {
            logger.error( String.format( "Failed to schedule re-enablement of %s for retry.", key ), e );
        }
    }

    @Measure( timers = @MetricNamed( DEFAULT) )
    public void onDisableTimeout( @Observes SchedulerEvent evt )
    {
        Logger logger = LoggerFactory.getLogger( getClass() );
        logger.debug( "Checking for store-reenable event in: {} (trigger? {} Disable-Timeout? {})", evt,
                      evt instanceof SchedulerTriggerEvent, DISABLE_TIMEOUT.equals( evt.getJobType() ) );

        if ( (evt instanceof SchedulerTriggerEvent) && DISABLE_TIMEOUT.equals( evt.getJobType() ) )
        {
            String keystr = evt.getPayload();
            StoreKey key = null;
            try
            {
                key = objectMapper.readValue( keystr, StoreKey.class );
            }
            catch ( IOException e )
            {
                logger.warn( "Failed to read StoreKey from JSON string: '{}' in event payload.", keystr );
            }

            logger.debug( "Read key: {} from JSON string: '{}' in event payload.", key, keystr );
            if ( key != null )
            {
                try
                {
                    ArtifactStore store = storeDataManager.getArtifactStore( key );
                    if ( store == null )
                    {
                        logger.warn( "Attempt to re-enable missing repository! Skipping." );
                        cancelReEnablementTimeout( key );
                        return;
                    }

                    store = store.copyOf();
                    if ( store.isDisabled() )
                    {
                        store.setDisabled( false );

                        storeDataManager.storeArtifactStore( store, new ChangeSummary( ChangeSummary.SYSTEM_USER,
                                                                                "Re-enabling " + key ),
                                                             false, true, new EventMetadata() );

                        cancelReEnablementTimeout( key );
                    }
                }
                catch ( IndyDataException e )
                {
                    logger.error( String.format( "Failed to re-enable %s", key ), e);
                }
                catch ( IndySchedulerException e )
                {
                    logger.error( String.format( "Failed to delete re-enablement job for %s.", key ), e );
                }
            }
        }
    }

    private void cancelReEnablementTimeout( StoreKey key )
            throws IndySchedulerException
    {
        Logger logger = LoggerFactory.getLogger( getClass() );
        logger.warn( "{} has been re-enabled for use.", key );

        scheduleManager.deleteJob( ScheduleManager.groupName( key, DISABLE_TIMEOUT ), DISABLE_TIMEOUT );
    }

    private void setReEnablementTimeout( StoreKey key )
            throws IndySchedulerException
    {
        Logger logger = LoggerFactory.getLogger( getClass() );
        logger.warn( "Reschedule enabling for: {}", key );

        scheduleManager.rescheduleDisableTimeout(key);
    }

}


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

import org.commonjava.indy.audit.ChangeSummary;
import org.commonjava.indy.data.ArtifactStoreQuery;
import org.commonjava.indy.data.IndyDataException;
import org.commonjava.indy.data.StoreDataManager;
import org.commonjava.indy.mem.data.MemoryStoreDataManager;
import org.commonjava.indy.model.core.ArtifactStore;
import org.commonjava.indy.model.core.Group;
import org.commonjava.indy.model.core.HostedRepository;
import org.commonjava.indy.model.core.RemoteRepository;
import org.commonjava.indy.model.core.StoreKey;
import org.commonjava.indy.model.core.StoreType;
import org.commonjava.indy.util.LocationUtils;
import org.commonjava.maven.galley.TransferException;
import org.commonjava.maven.galley.TransferManager;
import org.commonjava.maven.galley.event.EventMetadata;
import org.commonjava.maven.galley.model.ConcreteResource;
import org.commonjava.maven.galley.util.PathUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.decorator.Decorator;
import javax.decorator.Delegate;
import javax.enterprise.inject.Any;
import javax.inject.Inject;
import java.util.ArrayList;

@Decorator
public abstract class AutoProxDataManagerDecorator
        implements StoreDataManager
{

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

    @Delegate
    @Any
    @Inject
    private StoreDataManager dataManager;

    @Inject
    private AutoProxCatalogManager catalog;

    @Inject
    private TransferManager transferManager;

    protected AutoProxDataManagerDecorator()
    {
    }

    public AutoProxDataManagerDecorator( final MemoryStoreDataManager dataManager, final AutoProxCatalogManager catalog,
                                         final TransferManager transferManager )
    {
        this.dataManager = dataManager;
        this.catalog = catalog;
        this.transferManager = transferManager;
    }

    @Override
    public ArtifactStoreQuery<ArtifactStore> query()
    {
        logger.debug( "Rewrapping query for data manager: {}", this );
        return dataManager.query().rewrap( this );
    }

    protected final StoreDataManager getDelegate()
    {
        return dataManager;
    }

    private Group getGroup( final StoreKey key, final StoreKey impliedBy )
            throws IndyDataException
    {
        logger.debug( "DECORATED (getGroup: {})", key );
        Group g = (Group) dataManager.getArtifactStore( key );

        if ( !catalog.isEnabled() )
        {
            logger.debug( "AutoProx decorator disabled; returning: {}", g );
            return g;
        }

        logger.debug( "AutoProx decorator active" );
        if ( g == null )
        {
            logger.debug( "AutoProx: creating repository for: {}", key );
            if ( !checkValidity( key ) )
            {
                return null;
            }

            try
            {
                g = catalog.createGroup( key );
            }
            catch ( final AutoProxRuleException e )
            {
                throw new IndyDataException(
                        "[AUTOPROX] Failed to create new group from factory matching: '%s'. Reason: %s", e, key,
                        e.getMessage() );
            }

            if ( g != null )
            {
                logger.debug( "Validating group: {}", g );
                for ( final StoreKey memberKey : new ArrayList<>( g.getConstituents() ) )
                {
                    final ArtifactStore store = getArtifactStore( memberKey, impliedBy == null ? g.getKey() : impliedBy );
                    if ( store == null )
                    {
                        g.removeConstituent( memberKey );
                    }
                }

                if ( g.getConstituents().isEmpty() )
                {
                    return null;
                }

                final EventMetadata eventMetadata =
                        new EventMetadata().set( StoreDataManager.EVENT_ORIGIN, AutoProxConstants.STORE_ORIGIN )
                                           .set( AutoProxConstants.ORIGINATING_STORE,
                                                 impliedBy == null ? g.getKey() : impliedBy );

                dataManager.storeArtifactStore( g, new ChangeSummary( ChangeSummary.SYSTEM_USER,
                                                                          "AUTOPROX: Creating group for: '" + key
                                                                                + "'" ),
                                                false, true, eventMetadata );
            }
        }

        return g;
    }

    /**
     * Validates the remote connection, produced from rule-set for given key,
     * for a remote repo or group containing a remote. If:
     *
     *

     *
rule.isValidationEnabled() == false, return true

     *
rule.getValidationRemote() == null, return true

     *

     *     rule.getRemoteValidationPath() != null, validate remote.getUrl() + validationPath
     *

     *
if response code is 200 OK, then return true

     *
otherwise, return false

     *

     *

     *

     * @throws IndyDataException if the selected rule encounters an error while creating the new group/repository instance(s).
     */
    private boolean checkValidity( final StoreKey key )
            throws IndyDataException
    {
        if ( catalog.isValidationEnabled( key ) )
        {
            try
            {
                final RemoteRepository validationRepo = catalog.createValidationRemote( key );
                if ( validationRepo == null )
                {
                    logger.info( "No validation repository was created: assuming {} is valid.", key );
                    return true;
                }

                String path = catalog.getRemoteValidationPath( key );
                if ( path == null )
                {
                    path = PathUtils.ROOT;
                }

                logger.debug( "\n\n\n\n\n[AutoProx] Checking path: {} under remote URL: {}", path, validationRepo.getUrl() );
                boolean result = false;
                try
                {
                    result = transferManager.exists(
                            new ConcreteResource( LocationUtils.toLocation( validationRepo ), path ) );
                }
                catch ( final TransferException e )
                {
                    logger.warn( "[AutoProx] Cannot connect to target repository: '{}'. Reason: {}", validationRepo, e.getMessage() );
                    logger.debug( "[AutoProx] exception from validation attempt for: " + validationRepo, e );
                }

                logger.debug( "Validation result for: {} is: {}", validationRepo, result );

                return result;
            }
            catch ( final AutoProxRuleException e )
            {
                throw new IndyDataException(
                        "[AUTOPROX] Failed to create new group from factory matching: '%s'. Reason: %s", e, key,
                        e.getMessage() );
            }
        }

        return true;
    }

    private RemoteRepository getRemoteRepository( final StoreKey key, final StoreKey impliedBy )
            throws IndyDataException
    {
        logger.debug( "DECORATED (getRemoteRepository: {})", key );
        RemoteRepository repo = (RemoteRepository) dataManager.getArtifactStore( key );
        if ( !catalog.isEnabled() )
        {
            logger.debug( "AutoProx decorator disabled; returning: {}", repo );
            return repo;
        }

        logger.debug( "AutoProx decorator active" );
        if ( repo == null )
        {
            logger.info( "AutoProx: creating repository for: {}", key );

            try
            {
                repo = catalog.createRemoteRepository( key );
                if ( repo != null )
                {
                    if ( !checkValidity( key ) )
                    {
                        return null;
                    }

                    final EventMetadata eventMetadata =
                            new EventMetadata().set( StoreDataManager.EVENT_ORIGIN, AutoProxConstants.STORE_ORIGIN )
                                               .set( AutoProxConstants.ORIGINATING_STORE,
                                                     impliedBy == null ? repo.getKey() : impliedBy );

                    dataManager.storeArtifactStore( repo, new ChangeSummary( ChangeSummary.SYSTEM_USER,
                                                                               "AUTOPROX: Creating remote repository for: '"
                                                                                + key + "'" ),
                                                    false, true, eventMetadata );
                }
            }
            catch ( final AutoProxRuleException e )
            {
                throw new IndyDataException(
                        "[AUTOPROX] Failed to create new remote repository from factory matching: '%s'. Reason: %s", e,
                        key, e.getMessage() );
            }
        }

        return repo;
    }

    private HostedRepository getHostedRepository( final StoreKey key, final StoreKey impliedBy )
            throws IndyDataException
    {
        HostedRepository repo = (HostedRepository) dataManager.getArtifactStore( key );
        if ( !catalog.isEnabled() )
        {
            logger.debug( "AutoProx decorator disabled; returning: {}", repo );
            return repo;
        }

        logger.debug( "AutoProx decorator active" );
        if ( repo == null )
        {
            logger.info( "AutoProx: creating repository for: {}", key );

            try
            {
                repo = catalog.createHostedRepository( key );
            }
            catch ( final AutoProxRuleException e )
            {
                throw new IndyDataException(
                        "[AUTOPROX] Failed to create new hosted repository from factory matching: '%s'. Reason: %s", e,
                        key, e.getMessage() );
            }

            if ( repo != null )
            {
                final ChangeSummary changeSummary = new ChangeSummary( ChangeSummary.SYSTEM_USER,
                                                                       "AUTOPROX: Creating hosted repository for: '"
                                                                               + key + "'" );

                final EventMetadata eventMetadata =
                        new EventMetadata().set( StoreDataManager.EVENT_ORIGIN, AutoProxConstants.STORE_ORIGIN )
                                           .set( AutoProxConstants.ORIGINATING_STORE,
                                                 impliedBy == null ? repo.getKey() : impliedBy );

                dataManager.storeArtifactStore( repo, changeSummary, false, true,
                                                eventMetadata );
            }
        }

        return repo;
    }

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

    private ArtifactStore getArtifactStore( final StoreKey key, final StoreKey impliedBy )
            throws IndyDataException
    {
        if ( key == null )
        {
            return null;
        }

        logger.debug( "DECORATED (getArtifactStore: {})", key );
        if ( key.getType() == StoreType.group )
        {
            return getGroup( key, impliedBy );
        }
        else if ( key.getType() == StoreType.remote )
        {
            return getRemoteRepository( key, impliedBy );
        }
        else
        {
            return getHostedRepository( key, impliedBy );
        }
    }

    @Override
    public boolean hasArtifactStore( final StoreKey key )
    {
        try
        {
            return getArtifactStore( key ) != null;
        }
        catch ( final IndyDataException e )
        {
            logger.error( String.format( "Failed to retrieve/create: %s. Reason: %s", key, e.getMessage() ), e );
        }

        return false;
    }

}


/**
 * Copyright (C) 2011-2018 Red Hat, Inc. (https://github.com/Commonjava/indy)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.commonjava.indy.httprox.util;

import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.Timer;
import org.apache.commons.lang.StringUtils;
import org.apache.http.HttpRequest;
import org.commonjava.indy.IndyWorkflowException;
import org.commonjava.indy.audit.ChangeSummary;
import org.commonjava.indy.core.ctl.ContentController;
import org.commonjava.indy.data.ArtifactStoreQuery;
import org.commonjava.indy.data.IndyDataException;
import org.commonjava.indy.data.StoreDataManager;
import org.commonjava.indy.folo.ctl.FoloConstants;
import org.commonjava.indy.folo.model.TrackingKey;
import org.commonjava.indy.httprox.conf.HttproxConfig;
import org.commonjava.indy.httprox.handler.AbstractProxyRepositoryCreator;
import org.commonjava.indy.httprox.handler.ProxyCreationResult;
import org.commonjava.indy.httprox.handler.ProxyRepositoryCreator;
import org.commonjava.indy.metrics.conf.IndyMetricsConfig;
import org.commonjava.indy.model.core.AccessChannel;
import org.commonjava.indy.model.core.ArtifactStore;
import org.commonjava.indy.model.core.Group;
import org.commonjava.indy.model.core.HostedRepository;
import org.commonjava.indy.model.core.RemoteRepository;
import org.commonjava.indy.model.core.StoreType;
import org.commonjava.indy.subsys.http.util.UserPass;
import org.commonjava.indy.util.ApplicationHeader;
import org.commonjava.indy.util.ApplicationStatus;
import org.commonjava.indy.util.UrlInfo;
import org.commonjava.maven.galley.TransferException;
import org.commonjava.maven.galley.event.EventMetadata;
import org.commonjava.maven.galley.model.Transfer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.net.URL;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import static com.codahale.metrics.MetricRegistry.name;
import static org.commonjava.indy.model.core.ArtifactStore.TRACKING_ID;
import static org.commonjava.indy.model.core.GenericPackageTypeDescriptor.GENERIC_PKG_KEY;
import static org.commonjava.maven.galley.io.SpecialPathConstants.PKG_TYPE_GENERIC_HTTP;

/**
 * Created by ruhan on 9/20/18.
 */
public class ProxyResponseHelper
{
    private final Logger logger = LoggerFactory.getLogger( getClass() );

    private static final String TRACKED_USER_SUFFIX = "+tracking";

    private final HttpRequest httpRequest;

    private final ProxyRepositoryCreator repoCreator;

    private final StoreDataManager storeManager;

    private final IndyMetricsConfig metricsConfig;

    private final MetricRegistry metricRegistry;

    private final String cls;

    private final HttproxConfig config;

    private final ContentController contentController;

    private boolean transferred;

    public ProxyResponseHelper( HttpRequest httpRequest, HttproxConfig config, ContentController contentController,
                                ProxyRepositoryCreator repoCreator, StoreDataManager storeManager, IndyMetricsConfig metricsConfig, MetricRegistry metricRegistry, String cls )
    {
        this.httpRequest = httpRequest;
        this.config = config;
        this.contentController = contentController;
        this.repoCreator = repoCreator;
        this.storeManager = storeManager;
        this.metricsConfig = metricsConfig;
        this.metricRegistry = metricRegistry;
        this.cls = cls;
    }

    public ArtifactStore getArtifactStore( String trackingId, final URL url )
                    throws IndyDataException
    {
        if ( metricsConfig == null || metricRegistry == null )
        {
            return doGetArtifactStore( trackingId, url );
        }

        Timer timer = metricRegistry.timer( name( metricsConfig.getNodePrefix(), cls, "getArtifactStore" ) );
        Timer.Context timerContext = timer.time();
        try
        {
            return doGetArtifactStore( trackingId, url );
        }
        finally
        {
            timerContext.stop();
        }
    }

    private ArtifactStore doGetArtifactStore( String trackingId, final URL url )
                    throws IndyDataException
    {
        int port = getPort( url );

        if ( trackingId != null )
        {
            String groupName = repoCreator.formatId( url.getHost(), port, 0, trackingId, StoreType.group );

            ArtifactStoreQuery<Group> query =
                            storeManager.query().packageType( GENERIC_PKG_KEY ).storeType( Group.class );

            Group group = query.getGroup( groupName );
            logger.debug( "Get httproxy group, group: {}", group );

            if ( group == null )
            {
                logger.debug( "Creating repositories (group, hosted, remote) for HTTProx request: {}, trackingId: {}",
                              url, trackingId );
                ProxyCreationResult result = createRepo( trackingId, url, null );
                group = result.getGroup();
            }
            return group;
        }
        else
        {
            RemoteRepository remote;
            final String baseUrl = getBaseUrl( url, false );

            ArtifactStoreQuery<RemoteRepository> query =
                            storeManager.query().packageType( GENERIC_PKG_KEY ).storeType( RemoteRepository.class );

            remote = query.stream()
                          .filter( store -> store.getUrl().equals( baseUrl )
                                          && store.getMetadata( TRACKING_ID ) == null )
                          .findFirst()
                          .orElse( null );

            logger.debug( "Get httproxy remote, remote: {}", remote );
            if ( remote == null )
            {
                logger.debug( "Creating remote repository for HTTProx request: {}", url );
                String name = getRemoteRepositoryName( url );
                ProxyCreationResult result = createRepo( null, url, name );
                remote = result.getRemote();
            }
            return remote;
        }
    }

    /**
     * Create repositories (group, remote, hosted) when trackingId is present. Otherwise create normal remote
     * repository with specified name.
     *
     * @param trackingId
     * @param url
     * @param name distinct remote repository name. null if trackingId is given
     */
    private ProxyCreationResult createRepo( String trackingId, URL url, String name )
                    throws IndyDataException
    {
        UrlInfo info = new UrlInfo( url.toExternalForm() );

        UserPass up = UserPass.parse( ApplicationHeader.authorization, httpRequest, url.getAuthority() );
        String baseUrl = getBaseUrl( url, false );

        logger.debug( ">>>> Create repo: trackingId=" + trackingId + ", name=" + name );
        ProxyCreationResult result = repoCreator.create( trackingId, name, baseUrl, info, up,
                                                         LoggerFactory.getLogger( repoCreator.getClass() ) );
        ChangeSummary changeSummary =
                        new ChangeSummary( ChangeSummary.SYSTEM_USER, "Creating HTTProx proxy for: " + info.getUrl() );

        RemoteRepository remote = result.getRemote();
        if ( remote != null )
        {
            storeManager.storeArtifactStore( remote, changeSummary, false, true, new EventMetadata() );
        }

        HostedRepository hosted = result.getHosted();
        if ( hosted != null )
        {
            storeManager.storeArtifactStore( hosted, changeSummary, false, true, new EventMetadata() );
        }

        Group group = result.getGroup();
        if ( group != null )
        {
            storeManager.storeArtifactStore( group, changeSummary, false, true, new EventMetadata() );
        }

        return result;
    }

    /**
     * if repo with this name already exists, we need to use a different name
     */
    private String getRemoteRepositoryName( URL url ) throws IndyDataException
    {
        final String name = repoCreator.formatId( url.getHost(), getPort( url ), 0, null, StoreType.remote );

        logger.debug( "Looking for remote repo starts with name: {}", name );

        AbstractProxyRepositoryCreator abstractProxyRepositoryCreator = null;
        if ( repoCreator instanceof AbstractProxyRepositoryCreator )
        {
            abstractProxyRepositoryCreator = (AbstractProxyRepositoryCreator) repoCreator;
        }

        if ( abstractProxyRepositoryCreator == null )
        {
            return name;
        }

        Predicate<ArtifactStore> filter = abstractProxyRepositoryCreator.getNameFilter( name );
        List<String> l = storeManager.query()
                                     .packageType( GENERIC_PKG_KEY )
                                     .storeType( RemoteRepository.class )
                                     .stream( filter )
                                     .map( repository -> repository.getName() )
                                     .collect( Collectors.toList() );

        if ( l.isEmpty() )
        {
            return name;
        }
        return abstractProxyRepositoryCreator.getNextName( l );
    }

    private int getPort( URL url )
    {
        int port = url.getPort();
        if ( port < 1 )
        {
            port = url.getDefaultPort();
        }
        return port;
    }

    private String getBaseUrl( URL url, boolean includeDefaultPort )
    {
        int port = getPort( url );
        String portStr;
        if ( includeDefaultPort || port != url.getDefaultPort() )
        {
            portStr = ":" + port;
        }
        else
        {
            portStr = "";
        }
        return String.format( "%s://%s%s/", url.getProtocol(), url.getHost(), portStr );
    }

    public void transfer( final HttpConduitWrapper http, final ArtifactStore store, final String path,
                   final boolean writeBody, final UserPass proxyUserPass )
                    throws IOException, IndyWorkflowException
    {
        if ( metricsConfig == null || metricRegistry == null )
        {
            doTransfer( http, store, path, writeBody, proxyUserPass );
            return;
        }

        Timer timer = metricRegistry.timer( name( metricsConfig.getNodePrefix(), cls, "transfer" ) );
        Timer.Context timerContext = timer.time();
        try
        {
            doTransfer( http, store, path, writeBody, proxyUserPass );
        }
        finally
        {
            timerContext.stop();
        }
    }

    private void doTransfer( final HttpConduitWrapper http, final ArtifactStore store, final String path,
                             final boolean writeBody, final UserPass proxyUserPass )
                    throws IOException, IndyWorkflowException
    {
        if ( transferred )
        {
            return;
        }

        transferred = true;
        if ( !http.isOpen() )
        {
            throw new IOException( "Sink channel already closed (or null)!" );
        }

        final EventMetadata eventMetadata = createEventMetadata( writeBody, proxyUserPass, path, store );

        Transfer txfr = null;
        try
        {
            txfr = contentController.get( store.getKey(), path, eventMetadata );
        }
        catch ( final IndyWorkflowException e )
        {
            if ( !( e.getCause() instanceof TransferException ) )
            {
                throw e;
            }
            logger.debug( "Suppressed exception for further handling inside proxy logic:", e );
        }

        if ( txfr != null && txfr.exists() )
        {
            http.writeExistingTransfer( txfr, writeBody, path, eventMetadata );
        }
        else
        {
            http.writeNotFoundTransfer( store, path );
        }
    }

    private EventMetadata createEventMetadata( final boolean writeBody, final UserPass proxyUserPass, final String path,
                                       final ArtifactStore store )
                    throws IndyWorkflowException
    {
        final EventMetadata eventMetadata = new EventMetadata();
        if ( writeBody )
        {
            TrackingKey tk = getTrackingKey( proxyUserPass );

            if ( tk != null )
            {
                logger.debug( "TRACKING {} in {} (KEY: {})", path, store, tk );
                eventMetadata.set( FoloConstants.TRACKING_KEY, tk );

                eventMetadata.set( FoloConstants.ACCESS_CHANNEL, AccessChannel.GENERIC_PROXY );
            }
            else
            {
                logger.debug( "NOT TRACKING: {} in {}", path, store );
            }
        }
        else
        {
            logger.debug( "NOT TRACKING non-body request: {} in {}", path, store );
        }

        eventMetadata.setPackageType( PKG_TYPE_GENERIC_HTTP );

        return eventMetadata;
    }

    public TrackingKey getTrackingKey( UserPass proxyUserPass ) throws IndyWorkflowException
    {
        TrackingKey tk = null;
        switch ( config.getTrackingType() )
        {
            case ALWAYS:
            {
                if ( proxyUserPass == null )
                {
                    throw new IndyWorkflowException( ApplicationStatus.BAD_REQUEST.code(),
                                                     "Tracking is always-on, but no username was provided! Cannot initialize tracking key." );
                }

                tk = new TrackingKey( proxyUserPass.getUser() );

                break;
            }
            case SUFFIX:
            {
                if ( proxyUserPass != null )
                {
                    final String user = proxyUserPass.getUser();

                    if ( user != null && user.endsWith( TRACKED_USER_SUFFIX ) && user.length() > TRACKED_USER_SUFFIX.length() )
                    {
                        tk = new TrackingKey( StringUtils.substring( user, 0, - TRACKED_USER_SUFFIX.length() ) );
                    }
                }

                break;
            }
            default:
            {
            }
        }
        return tk;
    }

}


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

import org.commonjava.indy.model.core.ArtifactStore;
import org.commonjava.indy.model.core.Group;
import org.commonjava.indy.model.core.HostedRepository;
import org.commonjava.indy.model.core.RemoteRepository;
import org.commonjava.indy.model.core.StoreKey;
import org.commonjava.indy.model.core.StoreType;
import org.commonjava.indy.subsys.http.util.UserPass;
import org.commonjava.indy.util.UrlInfo;
import org.slf4j.Logger;

import java.util.Collections;
import java.util.List;
import java.util.function.Predicate;

import static org.commonjava.indy.httprox.util.HttpProxyConstants.PROXY_REPO_PREFIX;
import static org.commonjava.indy.model.core.ArtifactStore.METADATA_ORIGIN;
import static org.commonjava.indy.model.core.ArtifactStore.TRACKING_ID;
import static org.commonjava.indy.model.core.GenericPackageTypeDescriptor.GENERIC_PKG_KEY;
import static org.commonjava.indy.model.core.PathStyle.hashed;

/**
 * Created by ruhan on 4/16/18.
 */
public abstract class AbstractProxyRepositoryCreator
                implements ProxyRepositoryCreator
{
    @Override
    public abstract ProxyCreationResult create( String trackingID, String name, String baseUrl, UrlInfo urlInfo,
                                                UserPass userPass, Logger logger );

    @Override
    public String formatId( String host, int port, int index, String trackingID, StoreType storeType )
    {
        host = host.replaceAll( "\\.", "-" );
        if ( trackingID == null )
        {
            return PROXY_REPO_PREFIX + host + "_" + port + ( index > 0 ? "_" + index : "" );
        }
        else
        {
            String prefix = null;
            switch ( storeType )
            {
                case group:
                    prefix = "g";
                    break;
                case remote:
                    prefix = "r";
                    break;
                case hosted:
                    prefix = "h";
                    break;
                default:
                    break;
            }
            return prefix + "-" + host + "-" + trackingID;
        }
    }

    protected RemoteRepository createRemote( String name, String baseUrl, UrlInfo info, UserPass up, Logger logger )
    {
        return createRemote( null, name, baseUrl, info, up, logger );
    }

    protected RemoteRepository createRemote( String trackingID, String name, String baseUrl, UrlInfo info, UserPass up,
                                             Logger logger )
    {
        RemoteRepository remote = new RemoteRepository( GENERIC_PKG_KEY, name, baseUrl );
        if ( up != null )
        {
            remote.setUser( up.getUser() );
            remote.setPassword( up.getPassword() );
        }
        if ( trackingID == null )
        {
            remote.setPassthrough( true ); // to prevent long-term caching of content, default false
        }

        remote.setTimeoutSeconds( 5 * 60 ); // 5 minutes
        remote.setIgnoreHostnameVerification( true ); // not verify peer cert against hostname to avoid some redirected hostname inconsistency

        setPropsAndMetadata( remote, trackingID, info );
        return remote;
    }

    protected HostedRepository createHosted( String trackingID, String name, UrlInfo info, Logger logger )
    {
        HostedRepository hosted = new HostedRepository( GENERIC_PKG_KEY, name );
        setPropsAndMetadata( hosted, trackingID, info );
        return hosted;
    }

    protected Group createGroup( String trackingID, String name, UrlInfo info, Logger logger, StoreKey... constituents )
    {
        Group group = new Group( GENERIC_PKG_KEY, name, constituents );
        setPropsAndMetadata( group, trackingID, info );
        return group;
    }

    private void setPropsAndMetadata( ArtifactStore store, String trackingID, UrlInfo info )
    {
        store.setDescription( "HTTProx proxy based on: " + info.getUrl() );
        store.setPathStyle( hashed );

        store.setMetadata( METADATA_ORIGIN, ProxyAcceptHandler.HTTPROX_ORIGIN );
        if ( trackingID != null )
        {
            store.setMetadata( TRACKING_ID, trackingID );
        }
    }

    /**
     * Get the remote repo names with name starts with base name (PROXY_REPO_PREFIX + host + "_" + port)
     */
    public Predicate<ArtifactStore> getNameFilter( String name )
    {
        return store -> store.getName().startsWith( name );
    }

    /**
     * Get the next distinct name based on the query result by filter of getNameFilter
     * @param names
     * @return
     */
    public String getNextName( List<String> names )
    {
        if ( names.isEmpty() )
        {
            return null;
        }
        String name0 = names.get( 0 );
        if ( names.size() == 1 )
        {
            return name0 + "_1";
        }
        Collections.sort( names );
        String last = names.get( names.size() - 1 );
        String index = last.substring( last.lastIndexOf( "_" ) + 1 );
        return name0 + "_" + ( Integer.parseInt( index ) + 1 );
    }

}


/**
 * Copyright (C) 2011-2018 Red Hat, Inc. (https://github.com/Commonjava/indy)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.commonjava.indy.core.change.event;

import org.commonjava.indy.change.event.IndyStoreErrorEvent;
import org.commonjava.indy.content.ContentManager;
import org.commonjava.maven.galley.event.EventMetadata;
import org.commonjava.maven.galley.event.FileAccessEvent;
import org.commonjava.maven.galley.event.FileDeletionEvent;
import org.commonjava.maven.galley.event.FileErrorEvent;
import org.commonjava.maven.galley.event.FileNotFoundEvent;
import org.commonjava.maven.galley.event.FileStorageEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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

import static org.commonjava.indy.change.EventUtils.fireEvent;

/**
 * Helper class to provide simple methods to handle null-checking, etc. around the firing of Indy filesystem events.
 */
@ApplicationScoped
public class IndyFileEventManager
        implements org.commonjava.maven.galley.spi.event.FileEventManager
{

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

    @Inject
    private Event<FileStorageEvent> storageEvent;

    @Inject
    private Event<FileAccessEvent> accessEvent;

    @Inject
    private Event<FileDeletionEvent> deleteEvent;

    @Inject
    private Event<FileErrorEvent> errorEvent;

    @Inject
    private Event<FileNotFoundEvent> notFoundEvent;

    @Inject
    private Event<IndyStoreErrorEvent> storeErrorEvent;

//    @ExecutorConfig( named = CoreEventManagerConstants.DISPATCH_EXECUTOR_NAME,
//                     threads = CoreEventManagerConstants.DISPATCH_EXECUTOR_THREADS,
//                     priority = CoreEventManagerConstants.DISPATCH_EXECUTOR_PRIORITY )
//    @WeftManaged
//    @Inject
//    private Executor executor;

    @Override
    public void fire( final FileNotFoundEvent evt )
    {
        if ( shouldFireEvent( evt.getEventMetadata() ) ){
            fireEvent( notFoundEvent, evt );
        }
    }

    @Override
    public void fire( final FileStorageEvent evt )
    {
        if ( shouldFireEvent( evt.getEventMetadata() ) )
        {
            fireEvent( storageEvent, evt );
        }
    }

    @Override
    public void fire( final FileAccessEvent evt )
    {
        if ( shouldFireEvent( evt.getEventMetadata() ) )
        {
            fireEvent( accessEvent, evt );
        }
    }

    @Override
    public void fire( final FileDeletionEvent evt )
    {
        if ( shouldFireEvent( evt.getEventMetadata() ) )
        {
            fireEvent( deleteEvent, evt );
        }
    }

    @Override
    public void fire( final FileErrorEvent evt )
    {
        if ( shouldFireEvent( evt.getEventMetadata() ) )
        {
            fireEvent( errorEvent, evt );
        }
    }

    public void fire( final IndyStoreErrorEvent evt )
    {
        fireEvent( storeErrorEvent, evt );
    }

    private boolean shouldFireEvent( EventMetadata eventMetadata )
    {
        return ( eventMetadata == null || !Boolean.TRUE.equals( eventMetadata.get( ContentManager.SUPPRESS_EVENTS ) ) );
    }

}


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

import org.apache.kafka.clients.producer.Callback;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.commonjava.indy.subsys.kafka.conf.KafkaConfig;
import org.commonjava.indy.subsys.kafka.util.LogbackFormatter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import java.io.IOException;
import java.util.Properties;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;

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

    @Inject
    private KafkaConfig config;

    private KafkaProducer kafkaProducer;

    private final Consumer<Exception> exceptionHandler = ( e ) -> logger.error( "Send to Kafka failed", e );

    private final Callback callback = ( metadata, exception ) ->
    {
        if ( exception != null )
        {
            exceptionHandler.accept( exception );
        }
        else
        {
            logger.trace( "Message sent to Kafka. Partition:{}, timestamp {}.", metadata.partition(),
                          metadata.timestamp() );
        }
    };

    public IndyKafkaProducer( Properties properties )
    {
        this.kafkaProducer = new KafkaProducer<>( properties );
    }

    @PostConstruct
    private void init()
    {
        if ( config.isEnabled() )
        {
            Properties props = new Properties();
            props.putAll( config.getConfiguration() );
            kafkaProducer = new KafkaProducer<>( props );
        }
    }

    /**
     * Non-blocking send. The message will not be really available to consumers until flush()
     * or close() is called, or until another blocking send is called.
     */
    public void send( String topic, String message ) throws IOException
    {
        send( topic, message, null );
    }

    /**
     * Non-blocking send with a logback formatter to format the message.
     */
    public void send( String topic, String message, LogbackFormatter formatter ) throws IOException
    {
        doKafkaSend( topic, message, formatter );
    }

    /**
     * Blocking send. The message will be available to consumers immediately. Wait for at most the given time
     * for the operation to complete.
     */
    public void send( String topic, String message, long timeoutMillis )
                    throws IOException, InterruptedException, ExecutionException, TimeoutException
    {
        send( topic, message, null, timeoutMillis );
    }

    /**
     * Blocking send with a logback formatter to format the message.
     */
    public void send( String topic, String message, LogbackFormatter formatter, long timeoutMillis )
                    throws IOException, InterruptedException, ExecutionException, TimeoutException
    {
        Future future = doKafkaSend( topic, message, formatter );
        if ( future != null )
        {
            future.get( timeoutMillis, TimeUnit.MILLISECONDS );
        }
    }

    //
    private Future doKafkaSend( String topic, String message, LogbackFormatter formatter ) throws IOException
    {
        if ( kafkaProducer != null )
        {
            ProducerRecord<String, String> producerRecord =
                            new ProducerRecord<>( topic, formatter != null ? formatter.format( message ) : message );

            return kafkaProducer.send( producerRecord, callback );
        }
        return null;
    }

    /**
     * Flush all the non-blocking send messages so that the consumers can see them.
     */
    public void flush()
    {
        if ( kafkaProducer != null )
        {
            kafkaProducer.flush();
        }
    }

    @PreDestroy
    public void close() throws IOException
    {
        if ( kafkaProducer != null )
        {
            logger.info( "Closing {}", this.getClass() );
            kafkaProducer.close();
        }
    }

}