ComponentService.java

/*
 * Copyright (c) 2002-2021, 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.releaser.service;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import javax.servlet.http.HttpServletRequest;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.jgit.api.Git;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

import fr.paris.lutece.plugins.releaser.business.Component;
import fr.paris.lutece.plugins.releaser.business.ReleaserUser;
import fr.paris.lutece.plugins.releaser.business.RepositoryType;
import fr.paris.lutece.plugins.releaser.business.WorkflowReleaseContext;
import fr.paris.lutece.plugins.releaser.business.ReleaserUser.Credential;
import fr.paris.lutece.plugins.releaser.util.CommandResult;
import fr.paris.lutece.plugins.releaser.util.ConstanteUtils;
import fr.paris.lutece.plugins.releaser.util.ReleaserUtils;
import fr.paris.lutece.plugins.releaser.util.file.FileUtils;
import fr.paris.lutece.plugins.releaser.util.github.GitUtils;
import fr.paris.lutece.plugins.releaser.util.github.GithubSearchRepoItem;
import fr.paris.lutece.plugins.releaser.util.github.GithubSearchResult;
import fr.paris.lutece.plugins.releaser.util.pom.PomParser;
import fr.paris.lutece.plugins.releaser.util.version.Version;
import fr.paris.lutece.plugins.releaser.util.version.VersionParsingException;
import fr.paris.lutece.portal.business.user.AdminUser;
import fr.paris.lutece.portal.service.datastore.DatastoreService;
import fr.paris.lutece.portal.service.rbac.RBACService;
import fr.paris.lutece.portal.service.spring.SpringContextService;
import fr.paris.lutece.portal.service.util.AppLogService;
import fr.paris.lutece.portal.service.util.AppPropertiesService;
import fr.paris.lutece.portal.web.util.LocalizedPaginator;
import fr.paris.lutece.util.httpaccess.HttpAccess;
import fr.paris.lutece.util.httpaccess.HttpAccessException;

/**
 * ComponentService
 */
public class ComponentService implements IComponentService
{
    private ExecutorService _executor;
    private ObjectMapper _mapper;
    private static final String PROPERTY_COMPONENT_WEBSERVICE = "releaser.component.webservice.url";
    private static final String URL_COMPONENT_WEBSERVICE = AppPropertiesService.getProperty( PROPERTY_COMPONENT_WEBSERVICE );
    private static final String FIELD_COMPONENT = "component";
    private static final String FIELD_VERSION = "version";
    private static final String FIELD_SNAPSHOT_VERSION = "snapshotVersion";
    private static final String FIELD_ATTRIBUTES = "attributes";

    private static final String FIELD_JIRA_CODE = "jiraKey";
    private static final String FIELD_ROADMAP_URL = "jiraRoadmapUrl";
    private static final String FIELD_CLOSED_ISSUES = "jiraFixedIssuesCount";
    private static final String FIELD_OPENED_ISSUES = "jiraUnresolvedIssuesCount";
    private static final String FIELD_SCM_DEVELOPER_CONNECTION = "scmDeveloperConnection";
    private static final String RELEASE_NOT_FOUND = "Release not found";

    private static IComponentService _instance;

    /**
     * @return get service
     */
    public static IComponentService getService( )
    {
        if ( _instance == null )
        {
            _instance = SpringContextService.getBean( ConstanteUtils.BEAN_COMPONENT_SERVICE );
            _instance.init( );
        }

        return _instance;

    }

    public void setRemoteInformations( Component component, boolean bCache ) throws HttpAccessException, IOException
    {
        try
        {
            HttpAccess httpAccess = new HttpAccess( );
            String strInfosJSON;
            String strUrl = MessageFormat.format( URL_COMPONENT_WEBSERVICE, component.getArtifactId( ), bCache, component.getType( ) );
            if ( component.getType( ) == null )
            {
                strUrl = strUrl.replace( "&type=null", "" );
            }
            strInfosJSON = httpAccess.doGet( strUrl );
            JsonNode nodeRoot = _mapper.readTree( strInfosJSON );
            if ( nodeRoot != null )
            {
                JsonNode nodeComponent = nodeRoot.path( FIELD_COMPONENT );
                if ( nodeComponent != null )
                {
                    String strVersion = nodeComponent.get( FIELD_VERSION ).asText( );
                    if ( !RELEASE_NOT_FOUND.equals( strVersion ) )
                    {
                        component.setLastAvailableVersion( nodeComponent.get( FIELD_VERSION ).asText( ) );
                    }

                    JsonNode jnSnapshoteVersion = nodeComponent.get( FIELD_ATTRIBUTES ).get( FIELD_SNAPSHOT_VERSION );
                    JsonNode jnJiraCode = nodeComponent.get( FIELD_ATTRIBUTES ).get( FIELD_JIRA_CODE );
                    JsonNode jnJiraRoadMap = nodeComponent.get( FIELD_ATTRIBUTES ).get( FIELD_ROADMAP_URL );
                    JsonNode jnJiraCurrentVersionOpenedIssues = nodeComponent.get( FIELD_ATTRIBUTES ).get( FIELD_OPENED_ISSUES );
                    JsonNode jnJiraCurrentVersionClosedIssues = nodeComponent.get( FIELD_ATTRIBUTES ).get( FIELD_CLOSED_ISSUES );
                    JsonNode jnScmDeveloperConnection = nodeComponent.get( FIELD_ATTRIBUTES ).get( FIELD_SCM_DEVELOPER_CONNECTION );

                    component.setLastAvailableSnapshotVersion( jnSnapshoteVersion != null ? jnSnapshoteVersion.asText( ) : null );
                    component.setJiraCode( jnJiraCode != null ? jnJiraCode.asText( ) : null );
                    component.setJiraRoadmapUrl( jnJiraRoadMap != null ? jnJiraRoadMap.asText( ) : null );
                    component.setJiraCurrentVersionOpenedIssues( jnJiraCurrentVersionOpenedIssues != null ? jnJiraCurrentVersionOpenedIssues.asInt( ) : 0 );
                    component.setJiraCurrentVersionClosedIssues( jnJiraCurrentVersionClosedIssues != null ? jnJiraCurrentVersionClosedIssues.asInt( ) : 0 );

                    if ( jnScmDeveloperConnection != null && !StringUtils.isEmpty( jnScmDeveloperConnection.asText( ) )
                            && !jnScmDeveloperConnection.asText( ).equals( "null" ) )
                    {
                        component.setScmDeveloperConnection( jnScmDeveloperConnection.asText( ) );
                    }
                }
            }
        }
        catch( HttpAccessException | IOException ex )
        {
            AppLogService.error( "Error getting Remote informations : " + ex.getMessage( ), ex );
        }

    }

    /**
     * Returns the LastAvailableVersion
     * 
     * @return The LastAvailableVersion
     */
    public String getLastReleaseVersion( String strArtifactId )
    {

        return DatastoreService.getDataValue( ReleaserUtils.getLastReleaseVersionDataKey( strArtifactId ), null );

    }

    /**
     * set the LastAvailableVersion
     * 
     * set The LastAvailableVersion
     */
    public void setLastReleaseVersion( String strArtifactId, String strVersion )
    {

        DatastoreService.setDataValue( ReleaserUtils.getLastReleaseVersionDataKey( strArtifactId ), strVersion );

    }

    /**
     * Returns the LastAvailableVersion
     * 
     * @return The LastAvailableVersion
     */
    public String getLastReleaseNextSnapshotVersion( String strArtifactId )
    {

        return DatastoreService.getDataValue( ReleaserUtils.getLastReleaseNextSnapshotVersionDataKey( strArtifactId ), null );

    }

    /**
     * set the LastAvailableVersion
     * 
     * set The LastAvailableVersion
     */
    public void setLastReleaseNextSnapshotVersion( String strArtifactId, String strVersion )
    {

        DatastoreService.setDataValue( ReleaserUtils.getLastReleaseNextSnapshotVersionDataKey( strArtifactId ), strVersion );
    }

    @Override
    public int release( Component component, Locale locale, AdminUser user, HttpServletRequest request, boolean forceRelease )
    {

        // Test if version in progression before release
        if ( WorkflowReleaseContextService.getService( ).isReleaseInProgress( component.getArtifactId( ) )
                || ( !forceRelease && ( !component.isProject( ) || !component.shouldBeReleased( ) ) ) )
        {
            return -1;
        }

        WorkflowReleaseContext context = new WorkflowReleaseContext( );
        context.setComponent( component );
        context.setReleaserUser( ReleaserUtils.getReleaserUser( request, locale ) );

        int nIdWorkflow = WorkflowReleaseContextService.getService( ).getIdWorkflow( context );
        WorkflowReleaseContextService.getService( ).addWorkflowReleaseContext( context );

        // Compare Latest vesion of component before rekease
        WorkflowReleaseContextService.getService( ).startWorkflowReleaseContext( context, nIdWorkflow, locale, request, user );

        return context.getId( );
    }

    public int release( Component component, Locale locale, AdminUser user, HttpServletRequest request )
    {
        return release( component, locale, user, request, false );
    }

    public boolean isGitComponent( Component component )
    {
        return !StringUtils.isEmpty( component.getScmDeveloperConnection( ) )
                && component.getScmDeveloperConnection( ).trim( ).startsWith( ConstanteUtils.CONSTANTE_SUFFIX_GIT );
    }

    public void init( )
    {

        _mapper = new ObjectMapper( );
        _executor = Executors.newFixedThreadPool( AppPropertiesService.getPropertyInt( ConstanteUtils.PROPERTY_THREAD_RELEASE_POOL_MAX_SIZE, 10 ) );

    }

    @Override
    public void updateRemoteInformations( Component component )
    {
        String strLastReleaseVersion = ComponentService.getService( ).getLastReleaseVersion( component.getArtifactId( ) );
        String strLastReleaseNextSnapshotVersion = ComponentService.getService( ).getLastReleaseNextSnapshotVersion( component.getArtifactId( ) );
        if ( component.getLastAvailableVersion( ) == null )
        {
            component.setLastAvailableVersion( strLastReleaseVersion );
        }

        if ( component.getLastAvailableSnapshotVersion( ) == null )
        {
            component.setLastAvailableSnapshotVersion( strLastReleaseNextSnapshotVersion );
        }

        if ( component.getLastAvailableVersion( ) != null && strLastReleaseVersion != null )
        {
            try
            {
                Version vLastReleaseVersion = Version.parse( strLastReleaseVersion );
                Version vLastAvailableVersion = Version.parse( component.getLastAvailableVersion( ) );
                if ( vLastReleaseVersion.compareTo( vLastAvailableVersion ) > 0 )
                {
                    component.setLastAvailableVersion( strLastReleaseVersion );
                }

            }
            catch( VersionParsingException e )
            {
                AppLogService.error( e );
            }
        }
        if ( component.getLastAvailableSnapshotVersion( ) != null && strLastReleaseNextSnapshotVersion != null )
        {
            try
            {
                Version vLastReleaseNextSnapshotVersionVersion = Version.parse( strLastReleaseNextSnapshotVersion );
                Version vLastAvailableSnapshotVersion = Version.parse( component.getLastAvailableSnapshotVersion( ) );
                if ( vLastReleaseNextSnapshotVersionVersion.compareTo( vLastAvailableSnapshotVersion ) > 0 )
                {
                    component.setLastAvailableSnapshotVersion( strLastReleaseNextSnapshotVersion );
                }

            }
            catch( VersionParsingException e )
            {
                AppLogService.error( e );
            }
        }
    }

    public LocalizedPaginator<Component> getSearchComponent( String strSearch, HttpServletRequest request, Locale locale, String strPaginateUrl,
            String strCurrentPageIndex )
    {

        int nItemsPerPageLoad = AppPropertiesService.getPropertyInt( ConstanteUtils.PROPERTY_NB_SEARCH_ITEM_PER_PAGE_LOAD, 10 );
        ReleaserUser user = ReleaserUtils.getReleaserUser( request, locale );
        String strUserLogin = user.getCredential( RepositoryType.GITHUB ).getLogin( );
        String strUserPassword = user.getCredential( RepositoryType.GITHUB ).getPassword( );
        List<Component> listResultAll = getListComponent(
                GitUtils.searchRepo( strSearch, ConstanteUtils.CONSTANTE_GITHUB_ORG_LUTECE_PLATFORM, strUserLogin, strUserPassword ), strUserLogin,
                strUserPassword );
        listResultAll.addAll(
                getListComponent( GitUtils.searchRepo( strSearch, ConstanteUtils.CONSTANTE_GITHUB_ORG_LUTECE_SECTEUR_PUBLIC, strUserLogin, strUserPassword ),
                        strUserLogin, strUserPassword ) );

        List<Component> listResult = new ArrayList<Component>();        
        for ( Component component : listResultAll )
        {
            // Load only information on the current page
            loadComponent( component,
                    GitUtils.getFileContent( component.getFullName( ), "pom.xml", component.getBranchReleaseFrom( ), strUserLogin, strUserPassword ),
                    strUserLogin, strUserPassword );
            
            if (component.getArtifactId() != null && !component.getArtifactId().isEmpty())
            {
            	listResult.add(component);
            }
        }
        
        LocalizedPaginator<Component> paginator = new LocalizedPaginator<Component>( listResult, nItemsPerPageLoad, strPaginateUrl,
                LocalizedPaginator.PARAMETER_PAGE_INDEX, strCurrentPageIndex, locale );

        return paginator;
    }

    private List<Component> getListComponent( GithubSearchResult searchResult, String strUser, String strPassword )
    {

        List<Component> listComponent = new ArrayList<>( );
        Component component = null;

        if ( searchResult != null && searchResult.getListRepoItem( ) != null )
        {
            for ( GithubSearchRepoItem item : searchResult.getListRepoItem( ) )
            {

                component = new Component( );
                component.setFullName( item.getFullName( ) );
                component.setName( item.getName( ) );
                component.setBranchReleaseFrom(GitUtils.DEFAULT_RELEASE_BRANCH);
                listComponent.add( component );
            }

        }
        return listComponent;

    }

    public Component loadComponent( Component component, String strPom, String stUser, String strPassword )
    {

        PomParser parser = new PomParser( );
        parser.parse( component, strPom );

        try
        {
            ComponentService.getService( ).setRemoteInformations( component, false );

        }
        catch( HttpAccessException | IOException e )
        {
            AppLogService.error( e );
        }

        ComponentService.getService( ).updateRemoteInformations( component );

        if ( component.getBranchReleaseFrom( )!=null && component.getBranchReleaseFrom( ).equals( GitUtils.DEFAULT_RELEASE_BRANCH ) )
        {
              component = getNextVersions( component, component.getLastAvailableVersion( ), component.getCurrentVersion( ), component.getCurrentVersion() );
        }
        else
        {
            String strBranchReleaseCurrentVersion = component.getBranchReleaseVersion( );

            component = getNextVersions( component, strBranchReleaseCurrentVersion, strBranchReleaseCurrentVersion, strBranchReleaseCurrentVersion );
        }

        return component;
    }

    private Component getNextVersions( Component component, String strLastAvailableVersion, String strCurrentVersion, String strTargetVersion )
    {

        component.setTargetVersions( Version.getNextReleaseVersions( strLastAvailableVersion ) );
        component.setTargetVersion( Version.getReleaseVersion( strCurrentVersion ) );

        String strNextSnapshotVersion = null;
        try
        {
            Version version = Version.parse( strTargetVersion );
            boolean bSnapshot = true;
            strNextSnapshotVersion = version.nextPatch( bSnapshot ).toString( );
        }
        catch( VersionParsingException ex )
        {
            AppLogService.error( "Error parsing version for component " + component.getArtifactId( ) + " : " + ex.getMessage( ), ex );

        }
        component.setNextSnapshotVersion( strNextSnapshotVersion );
        component.setLastAvailableSnapshotVersion( strCurrentVersion );

        return component;
    }

    public boolean isErrorSnapshotComponentInformations( Component component, String strComponentPomPath )
    {

        boolean bError = true;

        PomParser parser = new PomParser( );
        Component componentPom = new Component( );

        FileInputStream inputStream;
        try
        {
            inputStream = new FileInputStream( strComponentPomPath );
            parser.parse( componentPom, inputStream );

            if ( component != null && componentPom != null && component.getArtifactId( ).equals( componentPom.getArtifactId( ) )
                    && component.getLastAvailableSnapshotVersion( ).equals( componentPom.getCurrentVersion( ) ) )
            {

                bError = false;

            }

        }
        catch( FileNotFoundException e )
        {
            AppLogService.error( e );

        }

        return bError;
    }

    /**
     * Change the next release version
     * 
     * @param component
     *            The component
     */
    public void changeNextReleaseVersion( Component component )
    {

        List<String> listTargetVersions = component.getTargetVersions( );
        if ( !CollectionUtils.isEmpty( listTargetVersions ) )
        {
            int nNewIndex = ( component.getTargetVersionIndex( ) + 1 ) % listTargetVersions.size( );
            String strTargetVersion = listTargetVersions.get( nNewIndex );
            component.setTargetVersion( strTargetVersion );
            component.setTargetVersionIndex( nNewIndex );
            component.setNextSnapshotVersion( Version.getNextSnapshotVersion( strTargetVersion ) );
        }
    }

    public static boolean IsSearchComponentAuthorized( AdminUser adminUser )
    {

        if ( RBACService.isAuthorized( new Component( ), ComponentResourceIdService.PERMISSION_SEARCH, adminUser ) )
        {
            return true;
        }

        return false;
    }

    @Override
    public Component getComponentBranchList( Component component, RepositoryType repositoryType, ReleaserUser user )
    {
    	
        Credential credential = user.getCredential( repositoryType );
        String strLogin = credential.getLogin( );
        String strPwd = credential.getPassword( );
        
    	CommandResult commandResult = new CommandResult( );
        WorkflowReleaseContext context = new WorkflowReleaseContext( );
        commandResult.setLog( new StringBuffer( ) );
        context.setCommandResult( commandResult );
        context.setComponent( component );

        ReleaserUtils.logStartAction( context, " Clone Component '" + component.getName( ) + "'" );

        String strLocalComponentPath = ReleaserUtils.getLocalPath( context );

        String strRepoUrl = GitUtils.getRepoUrl( context.getReleaserResource( ).getScmUrl( ) );
        File fLocalRepo = new File( strLocalComponentPath );
        if ( fLocalRepo.exists( ) )
        {
            commandResult.getLog( ).append( "Local repository " + strLocalComponentPath + " exist\nCleaning Local folder...\n" );
            if ( !FileUtils.delete( fLocalRepo, commandResult.getLog( ) ) )
            {
                commandResult.setError( commandResult.getLog( ).toString( ) );

            }
            commandResult.getLog( ).append( "Local repository has been cleaned\n" );
        }

        List<String> branchNameList = GitUtils.getBranchList( strRepoUrl, fLocalRepo, commandResult, strLogin, strPwd );
        branchNameList.remove( "master" );
        branchNameList.remove( "develop" );

        component.setBranches( branchNameList );
        context.setComponent( component );

        return component;
    }
    
    public Component getLastBranchVersion( Component component, String branchName, ReleaserUser user )
    {
        String strPom = null;

        // Set default release branch
        if ( !component.getBranches( ).contains( component.getBranchReleaseFrom( ) ) )
        {
            component.getBranches( ).add( component.getBranchReleaseFrom( ) );
        }
        component.setBranchReleaseFrom( branchName );
        int indx = component.getBranches( ).indexOf( branchName );
        component.getBranches( ).remove( indx );

        CommandResult commandResult = new CommandResult( );
        WorkflowReleaseContext context = new WorkflowReleaseContext( );
        commandResult.setLog( new StringBuffer( ) );
        context.setCommandResult( commandResult );
        context.setComponent( component );

        String strLocalComponentPath = ReleaserUtils.getLocalPath( context );
        File fLocalRepo = new File( strLocalComponentPath );

        ReleaserUtils.logStartAction( context, " Get versions of branch '" + branchName + "'" );

        Git git;
        try
        {
            git = Git.open( fLocalRepo );

            // Checkout branch
            GitUtils.createLocalBranch( git, branchName, commandResult );

            commandResult.getLog( ).append( "Checkout branch \"" + branchName + "\" ...\n" );
            GitUtils.checkoutRepoBranch( git, branchName, commandResult );

            // Fetch pom and Get last and next versions
            strPom = FileUtils.readFile( ReleaserUtils.getLocalPomPath( context ) );
            component = loadComponent( component, strPom, null, null );

        }
        catch( IOException e )
        {

            ReleaserUtils.addTechnicalError( commandResult, e.getMessage( ), e );
        }

        return component;
    }

}