/** * Copyright (c) 2008-2011 Sonatype, Inc. * All rights reserved. Includes the third-party code listed at http://www.sonatype.com/products/nexus/attributions. * * This program is free software: you can redistribute it and/or modify it only under the terms of the GNU Affero General * Public License Version 3 as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License Version 3 * for more details. * * You should have received a copy of the GNU Affero General Public License Version 3 along with this program. If not, see * http://www.gnu.org/licenses. * * Sonatype Nexus (TM) Open Source Version is available from Sonatype, Inc. Sonatype and Sonatype Nexus are trademarks of * Sonatype, Inc. Apache Maven is a trademark of the Apache Foundation. M2Eclipse is a trademark of the Eclipse Foundation. * All other trademarks are the property of their respective owners. */ package org.sonatype.nexus.restlight.stage; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import org.codehaus.plexus.util.StringUtils; import org.jdom.Document; import org.jdom.Element; import org.jdom.JDOMException; import org.jdom.Text; import org.jdom.xpath.XPath; import org.sonatype.nexus.restlight.common.AbstractRESTLightClient; import org.sonatype.nexus.restlight.common.RESTLightClientException; /** * REST client to access the functions of the nexus-staging-plugin, available in Nexus Professional. */ public class StageClient extends AbstractRESTLightClient { public static final String PROFILES_PATH = SVC_BASE + "/staging/profiles"; public static final String PROFILE_REPOS_PATH_PREFIX = SVC_BASE + "/staging/profile_repositories/"; public static final String STAGE_REPO_FINISH_ACTION = "/finish"; public static final String STAGE_REPO_DROP_ACTION = "/drop"; public static final String STAGE_REPO_PROMOTE_ACTION = "/promote"; public static final String STAGE_REPO_BULK_PROMOTE = SVC_BASE + "/staging/bulk/promote"; private static final String STAGE_REPO_ID_PARAM = "stagedRepositoryId"; private static final String PROFILE_ID_ELEMENT = "id"; private static final String PROFILE_NAME_ELEMENT = "name"; private static final String PROFILE_MODE_ELEMENT = "mode"; private static final String REPO_ID_ELEMENT = "repositoryId"; private static final String REPO_URI_ELEMENT = "repositoryURI"; private static final String REPO_DESCRIPTION_ELEMENT = "description"; private static final String REPO_IP_ADDRESS_ELEMENT = "ipAddress"; private static final String REPO_USER_AGENT_ELEMENT = "userAgent"; private static final String USER_ID_ELEMENT = "userId"; private static final String OPEN_STAGE_REPOS_XPATH = "stagingRepositoryIds/string/text()"; private static final String CLOSED_STAGE_REPOS_XPATH = "stagedRepositoryIds/string/text()"; private static final String STAGE_REPO_LIST_XPATH = "//stagingProfile"; private static final String STAGE_REPO_XPATH = "//stagingProfile"; private static final String STAGE_REPO_DETAIL_XPATH = "//stagingProfileRepository"; private static final String BUILD_PROMOTION_PROFILES_XPATH = "//stagingProfile[mode=\"GROUP\"]"; public StageClient( final String baseUrl, final String user, final String password ) throws RESTLightClientException { super( baseUrl, user, password, "stage/" ); } /** * Retrieve the list of all open staging repositories (not finished) in all available profiles that are opened for * the current user (the one specified in this client's constructor). * * @return details about each open repository */ public List<StageRepository> getOpenStageRepositoriesForUser() throws RESTLightClientException { Document doc = get( PROFILES_PATH ); return parseStageRepositories( doc, STAGE_REPO_LIST_XPATH, true, true ); } /** * Retrieve the list of all closed (finished) staging repositories that may house artifacts with the specified * groupId, artifactId, and version for the current user. * * @return details about each closed repository */ public List<StageRepository> getOpenStageRepositoriesForUser( final String groupId, final String artifactId, final String version ) throws RESTLightClientException { Map<String, String> params = new HashMap<String, String>(); mapCoord( groupId, artifactId, version, params ); Document doc = get( PROFILES_PATH, params ); return parseStageRepositories( doc, STAGE_REPO_XPATH, true, true ); } /** * Retrieve the details for the open staging repository which would be used for an artifact with the specified * groupId, artifactId, and version if the current user deployed it. In the event Nexus returns multiple open * staging repositories for the given user and GAV, this call will return details for the FIRST repository in that * list. */ public StageRepository getOpenStageRepositoryForUser( final String groupId, final String artifactId, final String version ) throws RESTLightClientException { Map<String, String> params = new HashMap<String, String>(); mapCoord( groupId, artifactId, version, params ); Document doc = get( PROFILES_PATH, params ); List<StageRepository> ids = parseStageRepositories( doc, STAGE_REPO_XPATH, true, true ); if ( ids == null || ids.isEmpty() ) { return null; } else { return ids.get( 0 ); } } /** * Retrieve the list of all closed (finished) staging repositories in all available profiles that are opened for the * current user (the one specified in this client's constructor). * * @return details about each closed repository */ public List<StageRepository> getClosedStageRepositoriesForUser() throws RESTLightClientException { Document doc = get( PROFILES_PATH ); return parseStageRepositories( doc, STAGE_REPO_LIST_XPATH, false, true ); } /** * Retrieve the list of all closed (finished) staging repositories that may house artifacts with the specified * groupId, artifactId, and version for the current user. * * @return details about each closed repository */ public List<StageRepository> getClosedStageRepositoriesForUser( final String groupId, final String artifactId, final String version ) throws RESTLightClientException { Map<String, String> params = new HashMap<String, String>(); mapCoord( groupId, artifactId, version, params ); Document doc = get( PROFILES_PATH, params ); return parseStageRepositories( doc, STAGE_REPO_XPATH, false, true ); } /** * Find the details for the open staging repository for the given groupId, artifactId, version, and the current * user, using the same algorithm as {@link StageClient#getOpenStageRepositoryForUser(String, String, String)}. Once * we have the details for this repository, submit those details to Nexus to convert the open repository to closed * (finished) status. This will make the artifacts in the repository available for use in Maven, etc. */ public void finishRepositoryForUser( final String groupId, final String artifactId, final String version, final String description ) throws RESTLightClientException { StageRepository repo = getOpenStageRepositoryForUser( groupId, artifactId, version ); finishRepository( repo, description ); } /** * Assuming the user has already queried Nexus for a valid {@link StageRepository} instance (details for an open * staging repository), submit those details to Nexus to convert the open repository to closed (finished) status. * This will make the artifacts in the repository available for use in Maven, etc. */ public void finishRepository( final StageRepository repo, final String description ) throws RESTLightClientException { Element extras = processDescription( description ); performStagingAction( repo, STAGE_REPO_FINISH_ACTION, Arrays.asList( extras ) ); } private Element processDescription( final String description ) { if ( description == null ) { return null; } String descElementName = getVocabulary().getProperty( VocabularyKeys.PROMOTE_STAGE_REPO_DESCRIPTION_ELEMENT, VocabularyKeys.SUPPRESS_ELEMENT_VALUE ); if ( !VocabularyKeys.SUPPRESS_ELEMENT_VALUE.equals( descElementName ) ) { Element desc = new Element( REPO_DESCRIPTION_ELEMENT ).setText( description ); return desc; } else { return null; } } /** * Assuming the user has already queried Nexus for a valid {@link StageRepository} instance (details for a staging * repository), submit those details to Nexus to drop the repository. */ public void dropRepository( final StageRepository repo, final String description ) throws RESTLightClientException { Element extras = processDescription( description ); performStagingAction( repo, STAGE_REPO_DROP_ACTION, Arrays.asList( extras ) ); } /** * Assuming the user has already queried Nexus for a valid {@link StageRepository} instance (details for a staging * repository), submit those details to Nexus to promote the repository into the permanent repository with the * specified targetRepositoryId. * * @param description */ public void promoteRepository( final StageRepository repo, final String targetRepositoryId, String description ) throws RESTLightClientException { Element target = new Element( "targetRepositoryId" ).setText( targetRepositoryId ); Element extras = processDescription( description ); performStagingAction( repo, STAGE_REPO_PROMOTE_ACTION, Arrays.asList( extras, target ) ); } /** * Promotes a set of repositories to a group profile. * * @param groupProfileId The group profile to promote to. * @param repositoryIds A list of repositoryIds to be promoted. * @throws RESTLightClientException */ public void promoteRepositories( String stagingProfileGroup, String description, List<String> stagedRepositoryIds ) throws RESTLightClientException { if ( stagedRepositoryIds == null || stagedRepositoryIds.isEmpty() ) { throw new RESTLightClientException( "No staging repositories specified. Please provide a valid staged repository ids." ); } if ( StringUtils.isEmpty( stagingProfileGroup ) ) { throw new RESTLightClientException( "No build promotion profile specified. Please provide a build promotion profile." ); } String rootElement = this.getVocabulary().getProperty( VocabularyKeys.BULK_ACTION_REQUEST_ROOT_ELEMENT ); Document body = new Document().setRootElement( new Element( rootElement ) ); Element data = new Element( "data" ); body.getRootElement().addContent( data ); if ( StringUtils.isNotEmpty( description ) ) { data.addContent( new Element( "description" ).setText( description ) ); } data.addContent( new Element( "stagingProfileGroup" ).setText( stagingProfileGroup ) ); Element staedRepoIds = new Element( "stagedRepositoryIds" ); data.addContent( staedRepoIds ); for ( String repoId : stagedRepositoryIds ) { staedRepoIds.addContent( new Element( "string" ).setText( repoId ) ); } post( STAGE_REPO_BULK_PROMOTE, null, body ); } /** * Returns a list of all the build promotion profile Ids. * * @return * @throws RESTLightClientException */ @SuppressWarnings( "unchecked" ) public List<StageProfile> getBuildPromotionProfiles() throws RESTLightClientException { Document doc = get( PROFILES_PATH ); // heavy lifting is done with xpath XPath profileXp = newXPath( BUILD_PROMOTION_PROFILES_XPATH ); List<Element> profiles; try { profiles = profileXp.selectNodes( doc.getRootElement() ); } catch ( JDOMException e ) { throw new RESTLightClientException( "XPath selection failed: '" + BUILD_PROMOTION_PROFILES_XPATH + "' (Root node: " + doc.getRootElement().getName() + ").", e ); } List<StageProfile> result = new ArrayList<StageProfile>(); if ( profiles != null ) { for ( Element profile : profiles ) { // just pull out the id and name. String profileId = profile.getChild( PROFILE_ID_ELEMENT ).getText(); String name = profile.getChild( PROFILE_NAME_ELEMENT ).getText(); result.add( new StageProfile( profileId, name ) ); } } return result; } @SuppressWarnings( "unchecked" ) private List<StageRepository> parseStageRepositories( final Document doc, final String profileXpath, final Boolean findOpen, boolean filterUser ) throws RESTLightClientException { // System.out.println( new XMLOutputter().outputString( doc ) ); XPath profileXp = newXPath( profileXpath ); List<Element> profiles; try { profiles = profileXp.selectNodes( doc.getRootElement() ); } catch ( JDOMException e ) { throw new RESTLightClientException( "XPath selection failed: '" + profileXpath + "' (Root node: " + doc.getRootElement().getName() + ").", e ); } List<StageRepository> result = new ArrayList<StageRepository>(); if ( profiles != null ) { XPath openRepoIdXPath = newXPath( OPEN_STAGE_REPOS_XPATH ); XPath closedRepoIdXPath = newXPath( CLOSED_STAGE_REPOS_XPATH ); for ( Element profile : profiles ) { // System.out.println( new XMLOutputter().outputString( profile ) ); String profileId = profile.getChild( PROFILE_ID_ELEMENT ).getText(); String profileName = profile.getChild( PROFILE_NAME_ELEMENT ).getText(); Map<String, StageRepository> matchingRepoStubs = new LinkedHashMap<String, StageRepository>(); if ( !Boolean.FALSE.equals( findOpen ) ) { try { List<Text> repoIds = openRepoIdXPath.selectNodes( profile ); if ( repoIds != null && !repoIds.isEmpty() ) { for ( Text txt : repoIds ) { matchingRepoStubs.put( profileId + "/" + txt.getText(), new StageRepository( profileId, txt.getText(), findOpen ).setProfileName( profileName ) ); } } } catch ( JDOMException e ) { throw new RESTLightClientException( "XPath selection failed: '" + OPEN_STAGE_REPOS_XPATH + "' (Node: " + profile.getName() + ").", e ); } } if ( !Boolean.TRUE.equals( findOpen ) ) { try { List<Text> repoIds = closedRepoIdXPath.selectNodes( profile ); if ( repoIds != null && !repoIds.isEmpty() ) { for ( Text txt : repoIds ) { matchingRepoStubs.put( profileId + "/" + txt.getText(), new StageRepository( profileId, txt.getText(), findOpen ).setProfileName( profileName ) ); } } } catch ( JDOMException e ) { throw new RESTLightClientException( "XPath selection failed: '" + CLOSED_STAGE_REPOS_XPATH + "' (Node: " + profile.getName() + ").", e ); } } if ( !matchingRepoStubs.isEmpty() ) { parseStageRepositoryDetails( profileId, matchingRepoStubs, filterUser ); result.addAll( matchingRepoStubs.values() ); } } } return result; } @SuppressWarnings( "unchecked" ) private void parseStageRepositoryDetails( final String profileId, final Map<String, StageRepository> repoStubs, boolean filterUser ) throws RESTLightClientException { // System.out.println( repoStubs ); Document doc = get( PROFILE_REPOS_PATH_PREFIX + profileId ); // System.out.println( new XMLOutputter().outputString( doc ) ); XPath repoXPath = newXPath( STAGE_REPO_DETAIL_XPATH ); List<Element> repoDetails; try { repoDetails = repoXPath.selectNodes( doc.getRootElement() ); } catch ( JDOMException e ) { throw new RESTLightClientException( "Failed to select detail sections for staging-profile repositories.", e ); } if ( repoDetails != null && !repoDetails.isEmpty() ) { for ( Element detail : repoDetails ) { String repoId = detail.getChild( REPO_ID_ELEMENT ).getText(); String key = profileId + "/" + repoId; StageRepository repo = repoStubs.get( key ); if ( repo == null ) { continue; } Element uid = detail.getChild( USER_ID_ELEMENT ); if ( uid != null && getUser() != null && getUser().equals( uid.getText().trim() ) ) { repo.setUser( uid.getText().trim() ); } else { if ( filterUser ) { repoStubs.remove( key ); } } Element url = detail.getChild( REPO_URI_ELEMENT ); if ( url != null ) { repo.setUrl( url.getText() ); } Element desc = detail.getChild( REPO_DESCRIPTION_ELEMENT ); if ( desc != null ) { repo.setDescription( desc.getText() ); } Element ipAddress = detail.getChild( REPO_IP_ADDRESS_ELEMENT ); if ( ipAddress != null ) { repo.setIpAddress( ipAddress.getText() ); } Element userAgent = detail.getChild( REPO_USER_AGENT_ELEMENT ); if ( userAgent != null ) { repo.setUserAgent( userAgent.getText() ); } } } } private XPath newXPath( final String xpath ) throws RESTLightClientException { try { return XPath.newInstance( xpath ); } catch ( JDOMException e ) { throw new RESTLightClientException( "Failed to build xpath: '" + xpath + "'.", e ); } } private void performStagingAction( final StageRepository repo, final String actionSubpath, final List<Element> extraData ) throws RESTLightClientException { if ( repo == null ) { throw new RESTLightClientException( "No staging-repository details specified. Please provide a valid StageRepository instance." ); } Map<String, String> params = new HashMap<String, String>(); params.put( STAGE_REPO_ID_PARAM, repo.getRepositoryId() ); String rootElement = getVocabulary().getProperty( VocabularyKeys.PROMOTE_STAGE_REPO_ROOT_ELEMENT ); Document body = new Document().setRootElement( new Element( rootElement ) ); Element data = new Element( "data" ); body.getRootElement().addContent( data ); data.addContent( new Element( "stagedRepositoryId" ).setText( repo.getRepositoryId() ) ); if ( extraData != null && !extraData.isEmpty() ) { for ( Element extra : extraData ) { data.addContent( extra ); } } post( PROFILES_PATH + "/" + repo.getProfileId() + actionSubpath, null, body ); } public List<StageRepository> getOpenStageRepositories() throws RESTLightClientException { Document doc = get( PROFILES_PATH ); return parseStageRepositories( doc, STAGE_REPO_LIST_XPATH, true, false ); } public List<StageRepository> getClosedStageRepositories() throws RESTLightClientException { Document doc = get( PROFILES_PATH ); return parseStageRepositories( doc, STAGE_REPO_LIST_XPATH, false, false ); } /** * Returns a list of all the staging profile Ids. * * @return * @throws RESTLightClientException */ @SuppressWarnings( "unchecked" ) public List<StageProfile> getStageProfiles() throws RESTLightClientException { Document doc = get( PROFILES_PATH ); // heavy lifting is done with xpath XPath profileXp = newXPath( STAGE_REPO_XPATH ); List<Element> profiles; try { profiles = profileXp.selectNodes( doc.getRootElement() ); } catch ( JDOMException e ) { throw new RESTLightClientException( "XPath selection failed: '" + STAGE_REPO_XPATH + "' (Root node: " + doc.getRootElement().getName() + ").", e ); } List<StageProfile> result = new ArrayList<StageProfile>(); if ( profiles != null ) { for ( Element profile : profiles ) { // just pull out the id and name. String profileId = profile.getChild( PROFILE_ID_ELEMENT ).getText(); String name = profile.getChild( PROFILE_NAME_ELEMENT ).getText(); String mode = profile.getChild( PROFILE_MODE_ELEMENT ).getText(); result.add( new StageProfile( profileId, name, mode ) ); } } return result; } }