View Javadoc
1   /*
2    * Copyright (c) 2002-2020, City of Paris
3    * All rights reserved.
4    *
5    * Redistribution and use in source and binary forms, with or without
6    * modification, are permitted provided that the following conditions
7    * are met:
8    *
9    *  1. Redistributions of source code must retain the above copyright notice
10   *     and the following disclaimer.
11   *
12   *  2. Redistributions in binary form must reproduce the above copyright notice
13   *     and the following disclaimer in the documentation and/or other materials
14   *     provided with the distribution.
15   *
16   *  3. Neither the name of 'Mairie de Paris' nor 'Lutece' nor the names of its
17   *     contributors may be used to endorse or promote products derived from
18   *     this software without specific prior written permission.
19   *
20   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21   * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22   * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
23   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
24   * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
25   * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
26   * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
27   * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
28   * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
29   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30   * POSSIBILITY OF SUCH DAMAGE.
31   *
32   * License 1.0
33   */
34  package fr.paris.lutece.plugins.lutecetools.service;
35  
36  import java.io.FileNotFoundException;
37  import java.io.IOException;
38  import java.net.InetSocketAddress;
39  import java.net.Proxy;
40  import java.net.SocketAddress;
41  import java.util.ArrayList;
42  import java.util.List;
43  import java.util.Map;
44  import java.util.concurrent.ConcurrentHashMap;
45  
46  import org.apache.commons.lang.StringUtils;
47  import org.kohsuke.github.GHBranch;
48  import org.kohsuke.github.GHIssueState;
49  import org.kohsuke.github.GHOrganization;
50  import org.kohsuke.github.GHPullRequest;
51  import org.kohsuke.github.GHRepository;
52  import org.kohsuke.github.GitHub;
53  import org.kohsuke.github.GitHubBuilder;
54  
55  import fr.paris.lutece.plugins.lutecetools.business.Component;
56  import fr.paris.lutece.portal.service.datastore.DatastoreService;
57  import fr.paris.lutece.portal.service.util.AppLogService;
58  import fr.paris.lutece.portal.service.util.AppPropertiesService;
59  
60  /**
61   * GitHub Service
62   */
63  public class GitHubService extends AbstractGitPlatformService
64  {
65      private static final String SERVICE_NAME = "GitHub Info filler service registered";
66      private static final String PROPERTY_GITHUB_ACCOUNT_NAME = "lutecetools.github.account.name";
67      private static final String PROPERTY_GITHUB_ACCOUNT_TOKEN = "lutecetools.github.account.token";
68      private static final String PROPERTY_GITHUB_ORGANIZATIONS = "lutecetools.github.organization";
69      private static final String DSKEY_PARENT_POM_VERSION = "lutecetools.site_property.globalPom.version";
70  
71      private static final String SITE_INDEX_PATH_PART1 = "/raw/develop/src/site/";
72      private static final String SITE_INDEX_PATH_PART2 = "xdoc/index.xml";
73  
74      private static String _strParentPomVersion;
75      private static Map<String, GHRepository> _mapRepositories;
76  
77      /**
78       * Initialization
79       */
80      public GitHubService( )
81      {
82          _strParentPomVersion = DatastoreService.getDataValue( DSKEY_PARENT_POM_VERSION, "3.0.3" );
83      }
84  
85      /**
86       * {@inheritDoc }
87       */
88      @Override
89      public String getName( )
90      {
91          return SERVICE_NAME;
92      }
93  
94      /**
95       * Update repositories info from GitHub
96       */
97      public static void updateGitHubRepositoriesList( )
98      {
99          _mapRepositories = getRepositories( );
100     }
101 
102     /**
103      * {@inheritDoc }
104      */
105     @Override
106     public void fill( Component component, StringBuilder sbLogs )
107     {
108         String strRepository = getGitHubRepository( component );
109         if ( strRepository == null )
110         {
111             return;
112         }
113         component.set( Component.IS_GIT_REPO, true );
114 
115         GHRepository repo = _mapRepositories.get( strRepository );
116 
117         try
118         {
119             component.set( GIT_GROUP, repo.getOwner( ).getLogin( ) );
120             component.set( GIT_PLATFORM, getGitPlatform( ) );
121             Map<String, GHBranch> mapBranches = repo.getBranches( );
122             List<String> listBranches = new ArrayList<>( );
123 
124             for ( String strBranch : mapBranches.keySet( ) )
125             {
126                 listBranches.add( strBranch );
127             }
128 
129             component.set( BRANCHES_LIST, listBranches );
130         }
131         catch ( Exception ex )
132         {
133             sbLogs.append( "\n*** ERROR *** Retrieving GitHub infos (branches , readme, ...) for component " )
134                     .append( component.getArtifactId( ) ).append( " : " ).append( ex.getMessage( ) );
135         }
136         try
137         {
138             repo.getReadme( );
139             component.set( HAS_README, true );
140         }
141         catch ( Exception e )
142         {
143             if ( e instanceof FileNotFoundException )
144             {
145                 component.set( HAS_README, false );
146             }
147         }
148         try
149         {
150             List<GHPullRequest> prs = repo.getPullRequests( GHIssueState.OPEN );
151             component.set( PULL_REQUEST_COUNT, prs.size( ) );
152             long oldest = Long.MAX_VALUE;
153             for ( GHPullRequest pr : prs )
154             {
155                 if ( pr.getUpdatedAt( ).getTime( ) < oldest )
156                 {
157                     oldest = pr.getUpdatedAt( ).getTime( );
158                 }
159             }
160             component.set( OLDEST_PULL_REQUEST, oldest );
161         }
162         catch ( IOException e )
163         {
164             sbLogs.append( "\n*** ERROR *** Retreiving Github pull requests for component " )
165                     .append( component.getArtifactId( ) ).append( " : " ).append( e.getMessage( ) );
166         }
167         fillGitHubStatus( component );
168         fillGitHubErrors( component );
169 
170         fillSiteInfos( component, sbLogs );
171     }
172 
173     private static String getGitHubRepository( Component component )
174     {
175         if ( _mapRepositories == null )
176         {
177             _mapRepositories = getRepositories( );
178         }
179         for ( String strRepository : _mapRepositories.keySet( ) )
180         {
181             if ( strRepository.endsWith( component.getArtifactId( ) ) )
182             {
183                 return strRepository;
184             }
185         }
186 
187         return null;
188 
189     }
190 
191     /**
192      * Gets all repositories of a given organization
193      *
194      * @return A map that contains repositories
195      */
196     static Map<String, GHRepository> getRepositories( )
197     {
198         String strOrganizations = AppPropertiesService.getProperty( PROPERTY_GITHUB_ORGANIZATIONS );
199 
200         String[] organizations = strOrganizations.split( "," );
201 
202         Map<String, GHRepository> mapRepositories = new ConcurrentHashMap<>( );
203 
204         for ( String strOrganization : organizations )
205         {
206             strOrganization = strOrganization.trim( );
207             try
208             {
209                 GitHub github = getGitHub( );
210                 GHOrganization organization = github.getOrganization( strOrganization );
211                 mapRepositories.putAll( organization.getRepositories( ) );
212                 int nSize = organization.getRepositories( ).size( );
213                 AppLogService.info( "LuteceTools : GitHub Service initialized - " + nSize
214                         + " repositories found for organization " + strOrganization );
215             }
216             catch ( IOException ex )
217             {
218                 AppLogService.error( "LuteceTools : Unable to access GitHub repositories", ex );
219             }
220         }
221         return mapRepositories;
222     }
223 
224     /**
225      * Gets a GitHub object to request repositories
226      * 
227      * @return GitHub object
228      * @throws IOException if an exception occurs
229      */
230     private static GitHub getGitHub( ) throws IOException
231     {
232         GitHub github;
233 
234         String strAccount = AppPropertiesService.getProperty( PROPERTY_GITHUB_ACCOUNT_NAME );
235         String strToken = AppPropertiesService.getProperty( PROPERTY_GITHUB_ACCOUNT_TOKEN );
236         String strProxyHost = AppPropertiesService.getProperty( "httpAccess.proxyHost" );
237         int nProxyPort = AppPropertiesService.getPropertyInt( "httpAccess.proxyPort", 80 );
238         if ( !StringUtils.isEmpty( strProxyHost ) )
239         {
240             GitHubBuilder builder = new GitHubBuilder( );
241             SocketAddress address = new InetSocketAddress( strProxyHost, nProxyPort );
242             Proxy proxy = new Proxy( Proxy.Type.HTTP, address );
243             builder.withProxy( proxy );
244             builder.withOAuthToken( strToken, strAccount );
245             github = builder.build( );
246             AppLogService.info( "LuteceTools : Using httpaccess.properties defined proxy to connect to GitHub." );
247         }
248         else
249         {
250             github = GitHub.connect( strAccount, strToken );
251         }
252 
253         return github;
254 
255     }
256 
257     /**
258      * Returns GitHub errors
259      *
260      * @param component The component
261      */
262     private void fillGitHubErrors( Component component )
263     {
264         StringBuilder sbErrors = new StringBuilder( "" );
265 
266         if ( Boolean.TRUE.equals( component.getBoolean( Component.IS_GIT_REPO ) ) )
267         {
268             String strScmUrl = component.get( Component.SCM_URL );
269             if ( strScmUrl != null && strScmUrl.contains( "github" ) )
270             {
271                 sbErrors.append( "Bad SCM info in the released POM. \n" );
272             }
273 
274             String strSnapshotScmUrl = component.get( Component.SNAPSHOT_SCM_URL );
275             if ( strSnapshotScmUrl != null && strSnapshotScmUrl.contains( "github" ) )
276             {
277                 sbErrors.append( "Bad SCM info in the snapshot POM. \n" );
278             }
279 
280             if ( !_strParentPomVersion.equals( component.get( Component.PARENT_POM_VERSION ) ) )
281             {
282                 sbErrors.append( "Bad parent POM in release POM. should be global-pom version " )
283                         .append( _strParentPomVersion ).append( '\n' );
284             }
285 
286             if ( !_strParentPomVersion.equals( component.get( Component.SNAPSHOT_PARENT_POM_VERSION ) ) )
287             {
288                 sbErrors.append( "Bad parent POM in snapshot POM. should be global-pom version " )
289                         .append( _strParentPomVersion ).append( '\n' );
290             }
291 
292             List listBranches = (List) component.getObject( BRANCHES_LIST );
293             if ( ( listBranches != null ) && ( listBranches.contains( "develop" ) ) )
294             {
295                 sbErrors.append( "Branch 'develop' is missing. \n" );
296             }
297         }
298 
299         component.set( GIT_REPO_ERRORS, sbErrors.toString( ) );
300     }
301 
302     /**
303      * Calculate GitHub status
304      *
305      * @param component The component
306      */
307     private void fillGitHubStatus( Component component )
308     {
309         int nStatus = 0;
310 
311         if ( Boolean.TRUE.equals( component.getBoolean( Component.IS_GIT_REPO ) ) )
312         {
313             nStatus++;
314         }
315 
316         String strScmUrl = component.get( Component.SCM_URL );
317         if ( strScmUrl != null && strScmUrl.contains( "github" ) )
318         {
319             nStatus++;
320         }
321 
322         String strSnapshotScmUrl = component.get( Component.SNAPSHOT_SCM_URL );
323         if ( strSnapshotScmUrl != null && strSnapshotScmUrl.contains( "github" ) )
324         {
325             nStatus++;
326         }
327 
328         List listBranches = (List) component.getObject( BRANCHES_LIST );
329         if ( ( listBranches != null ) && ( listBranches.contains( "develop" ) ) )
330         {
331             nStatus++;
332         }
333         component.set( GIT_REPO_STATUS, nStatus );
334     }
335 
336     /**
337      * fill site infos from xdoc site index
338      *
339      * @param component The component
340      */
341     private void fillSiteInfos( Component component, StringBuilder sbLogs )
342     {
343         String strScmUrl = component.get( Component.SCM_URL );
344         if ( strScmUrl != null )
345         {
346             if ( strScmUrl.endsWith( ".git" ) )
347             {
348                 strScmUrl = strScmUrl.substring( 0, strScmUrl.length( ) - 4 );
349             }
350 
351             String strXdocSiteIndexUrl = strScmUrl + SITE_INDEX_PATH_PART1 + SITE_INDEX_PATH_PART2;
352             SiteInfoService.instance( ).getSiteInfos( component, strXdocSiteIndexUrl, "en", sbLogs );
353 
354             strXdocSiteIndexUrl = strScmUrl + SITE_INDEX_PATH_PART1 + "fr/" + SITE_INDEX_PATH_PART2;
355             SiteInfoService.instance( ).getSiteInfos( component, strXdocSiteIndexUrl, "fr", sbLogs );
356 
357         }
358     }
359 }