GitHubService.java

/*
 * Copyright (c) 2002-2020, City of Paris
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 *  1. Redistributions of source code must retain the above copyright notice
 *     and the following disclaimer.
 *
 *  2. Redistributions in binary form must reproduce the above copyright notice
 *     and the following disclaimer in the documentation and/or other materials
 *     provided with the distribution.
 *
 *  3. Neither the name of 'Mairie de Paris' nor 'Lutece' nor the names of its
 *     contributors may be used to endorse or promote products derived from
 *     this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 *
 * License 1.0
 */
package fr.paris.lutece.plugins.lutecetools.service;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.SocketAddress;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.commons.lang.StringUtils;
import org.kohsuke.github.GHBranch;
import org.kohsuke.github.GHIssueState;
import org.kohsuke.github.GHOrganization;
import org.kohsuke.github.GHPullRequest;
import org.kohsuke.github.GHRepository;
import org.kohsuke.github.GitHub;
import org.kohsuke.github.GitHubBuilder;

import fr.paris.lutece.plugins.lutecetools.business.Component;
import fr.paris.lutece.portal.service.datastore.DatastoreService;
import fr.paris.lutece.portal.service.util.AppLogService;
import fr.paris.lutece.portal.service.util.AppPropertiesService;

/**
 * GitHub Service
 */
public class GitHubService extends AbstractGitPlatformService
{
    private static final String SERVICE_NAME = "GitHub Info filler service registered";
    private static final String PROPERTY_GITHUB_ACCOUNT_NAME = "lutecetools.github.account.name";
    private static final String PROPERTY_GITHUB_ACCOUNT_TOKEN = "lutecetools.github.account.token";
    private static final String PROPERTY_GITHUB_ORGANIZATIONS = "lutecetools.github.organization";
    private static final String DSKEY_PARENT_POM_VERSION = "lutecetools.site_property.globalPom.version";

    private static final String SITE_INDEX_PATH_PART1 = "/raw/develop/src/site/";
    private static final String SITE_INDEX_PATH_PART2 = "xdoc/index.xml";

    private static String _strParentPomVersion;
    private static Map<String, GHRepository> _mapRepositories;

    /**
     * Initialization
     */
    public GitHubService( )
    {
        _strParentPomVersion = DatastoreService.getDataValue( DSKEY_PARENT_POM_VERSION, "3.0.3" );
    }

    /**
     * {@inheritDoc }
     */
    @Override
    public String getName( )
    {
        return SERVICE_NAME;
    }

    /**
     * Update repositories info from GitHub
     */
    public static void updateGitHubRepositoriesList( )
    {
        _mapRepositories = getRepositories( );
    }

    /**
     * {@inheritDoc }
     */
    @Override
    public void fill( Component component, StringBuilder sbLogs )
    {
        String strRepository = getGitHubRepository( component );
        if ( strRepository == null )
        {
            return;
        }
        component.set( Component.IS_GIT_REPO, true );

        GHRepository repo = _mapRepositories.get( strRepository );

        try
        {
            component.set( GIT_GROUP, repo.getOwner( ).getLogin( ) );
            component.set( GIT_PLATFORM, getGitPlatform( ) );
            Map<String, GHBranch> mapBranches = repo.getBranches( );
            List<String> listBranches = new ArrayList<>( );

            for ( String strBranch : mapBranches.keySet( ) )
            {
                listBranches.add( strBranch );
            }

            component.set( BRANCHES_LIST, listBranches );
        }
        catch ( Exception ex )
        {
            sbLogs.append( "\n*** ERROR *** Retrieving GitHub infos (branches , readme, ...) for component " )
                    .append( component.getArtifactId( ) ).append( " : " ).append( ex.getMessage( ) );
        }
        try
        {
            repo.getReadme( );
            component.set( HAS_README, true );
        }
        catch ( Exception e )
        {
            if ( e instanceof FileNotFoundException )
            {
                component.set( HAS_README, false );
            }
        }
        try
        {
            List<GHPullRequest> prs = repo.getPullRequests( GHIssueState.OPEN );
            component.set( PULL_REQUEST_COUNT, prs.size( ) );
            long oldest = Long.MAX_VALUE;
            for ( GHPullRequest pr : prs )
            {
                if ( pr.getUpdatedAt( ).getTime( ) < oldest )
                {
                    oldest = pr.getUpdatedAt( ).getTime( );
                }
            }
            component.set( OLDEST_PULL_REQUEST, oldest );
        }
        catch ( IOException e )
        {
            sbLogs.append( "\n*** ERROR *** Retreiving Github pull requests for component " )
                    .append( component.getArtifactId( ) ).append( " : " ).append( e.getMessage( ) );
        }
        fillGitHubStatus( component );
        fillGitHubErrors( component );

        fillSiteInfos( component, sbLogs );
    }

    private static String getGitHubRepository( Component component )
    {
        if ( _mapRepositories == null )
        {
            _mapRepositories = getRepositories( );
        }
        for ( String strRepository : _mapRepositories.keySet( ) )
        {
            if ( strRepository.endsWith( component.getArtifactId( ) ) )
            {
                return strRepository;
            }
        }

        return null;

    }

    /**
     * Gets all repositories of a given organization
     *
     * @return A map that contains repositories
     */
    static Map<String, GHRepository> getRepositories( )
    {
        String strOrganizations = AppPropertiesService.getProperty( PROPERTY_GITHUB_ORGANIZATIONS );

        String[] organizations = strOrganizations.split( "," );

        Map<String, GHRepository> mapRepositories = new ConcurrentHashMap<>( );

        for ( String strOrganization : organizations )
        {
            strOrganization = strOrganization.trim( );
            try
            {
                GitHub github = getGitHub( );
                GHOrganization organization = github.getOrganization( strOrganization );
                mapRepositories.putAll( organization.getRepositories( ) );
                int nSize = organization.getRepositories( ).size( );
                AppLogService.info( "LuteceTools : GitHub Service initialized - " + nSize
                        + " repositories found for organization " + strOrganization );
            }
            catch ( IOException ex )
            {
                AppLogService.error( "LuteceTools : Unable to access GitHub repositories", ex );
            }
        }
        return mapRepositories;
    }

    /**
     * Gets a GitHub object to request repositories
     * 
     * @return GitHub object
     * @throws IOException if an exception occurs
     */
    private static GitHub getGitHub( ) throws IOException
    {
        GitHub github;

        String strAccount = AppPropertiesService.getProperty( PROPERTY_GITHUB_ACCOUNT_NAME );
        String strToken = AppPropertiesService.getProperty( PROPERTY_GITHUB_ACCOUNT_TOKEN );
        String strProxyHost = AppPropertiesService.getProperty( "httpAccess.proxyHost" );
        int nProxyPort = AppPropertiesService.getPropertyInt( "httpAccess.proxyPort", 80 );
        if ( !StringUtils.isEmpty( strProxyHost ) )
        {
            GitHubBuilder builder = new GitHubBuilder( );
            SocketAddress address = new InetSocketAddress( strProxyHost, nProxyPort );
            Proxy proxy = new Proxy( Proxy.Type.HTTP, address );
            builder.withProxy( proxy );
            builder.withOAuthToken( strToken, strAccount );
            github = builder.build( );
            AppLogService.info( "LuteceTools : Using httpaccess.properties defined proxy to connect to GitHub." );
        }
        else
        {
            github = GitHub.connect( strAccount, strToken );
        }

        return github;

    }

    /**
     * Returns GitHub errors
     *
     * @param component The component
     */
    private void fillGitHubErrors( Component component )
    {
        StringBuilder sbErrors = new StringBuilder( "" );

        if ( Boolean.TRUE.equals( component.getBoolean( Component.IS_GIT_REPO ) ) )
        {
            String strScmUrl = component.get( Component.SCM_URL );
            if ( strScmUrl != null && strScmUrl.contains( "github" ) )
            {
                sbErrors.append( "Bad SCM info in the released POM. \n" );
            }

            String strSnapshotScmUrl = component.get( Component.SNAPSHOT_SCM_URL );
            if ( strSnapshotScmUrl != null && strSnapshotScmUrl.contains( "github" ) )
            {
                sbErrors.append( "Bad SCM info in the snapshot POM. \n" );
            }

            if ( !_strParentPomVersion.equals( component.get( Component.PARENT_POM_VERSION ) ) )
            {
                sbErrors.append( "Bad parent POM in release POM. should be global-pom version " )
                        .append( _strParentPomVersion ).append( '\n' );
            }

            if ( !_strParentPomVersion.equals( component.get( Component.SNAPSHOT_PARENT_POM_VERSION ) ) )
            {
                sbErrors.append( "Bad parent POM in snapshot POM. should be global-pom version " )
                        .append( _strParentPomVersion ).append( '\n' );
            }

            List listBranches = (List) component.getObject( BRANCHES_LIST );
            if ( ( listBranches != null ) && ( listBranches.contains( "develop" ) ) )
            {
                sbErrors.append( "Branch 'develop' is missing. \n" );
            }
        }

        component.set( GIT_REPO_ERRORS, sbErrors.toString( ) );
    }

    /**
     * Calculate GitHub status
     *
     * @param component The component
     */
    private void fillGitHubStatus( Component component )
    {
        int nStatus = 0;

        if ( Boolean.TRUE.equals( component.getBoolean( Component.IS_GIT_REPO ) ) )
        {
            nStatus++;
        }

        String strScmUrl = component.get( Component.SCM_URL );
        if ( strScmUrl != null && strScmUrl.contains( "github" ) )
        {
            nStatus++;
        }

        String strSnapshotScmUrl = component.get( Component.SNAPSHOT_SCM_URL );
        if ( strSnapshotScmUrl != null && strSnapshotScmUrl.contains( "github" ) )
        {
            nStatus++;
        }

        List listBranches = (List) component.getObject( BRANCHES_LIST );
        if ( ( listBranches != null ) && ( listBranches.contains( "develop" ) ) )
        {
            nStatus++;
        }
        component.set( GIT_REPO_STATUS, nStatus );
    }

    /**
     * fill site infos from xdoc site index
     *
     * @param component The component
     */
    private void fillSiteInfos( Component component, StringBuilder sbLogs )
    {
        String strScmUrl = component.get( Component.SCM_URL );
        if ( strScmUrl != null )
        {
            if ( strScmUrl.endsWith( ".git" ) )
            {
                strScmUrl = strScmUrl.substring( 0, strScmUrl.length( ) - 4 );
            }

            String strXdocSiteIndexUrl = strScmUrl + SITE_INDEX_PATH_PART1 + SITE_INDEX_PATH_PART2;
            SiteInfoService.instance( ).getSiteInfos( component, strXdocSiteIndexUrl, "en", sbLogs );

            strXdocSiteIndexUrl = strScmUrl + SITE_INDEX_PATH_PART1 + "fr/" + SITE_INDEX_PATH_PART2;
            SiteInfoService.instance( ).getSiteInfos( component, strXdocSiteIndexUrl, "fr", sbLogs );

        }
    }
}