/*! * Copyright 2010 - 2015 Pentaho Corporation. All rights reserved. * * 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 com.pentaho.di.purge; import java.io.File; import java.io.FileOutputStream; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.HashMap; import java.util.Map; import javax.ws.rs.core.MediaType; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.WordUtils; import org.pentaho.platform.security.policy.rolebased.actions.AdministerSecurityAction; import org.pentaho.platform.util.RepositoryPathEncoder; import com.pentaho.di.messages.Messages; import com.sun.jersey.api.client.Client; import com.sun.jersey.api.client.ClientResponse; import com.sun.jersey.api.client.WebResource; import com.sun.jersey.api.client.config.ClientConfig; import com.sun.jersey.api.client.config.DefaultClientConfig; import com.sun.jersey.api.client.filter.HTTPBasicAuthFilter; import com.sun.jersey.api.json.JSONConfiguration; import com.sun.jersey.multipart.FormDataMultiPart; /** * Command line utility for purging files, file revisions, or shared objects. Passes through to purge services. */ public class RepositoryCleanupUtil { public static Client client; // public so can be injected by unit test public static boolean testMode; // system.exit() is disabled for unit tests // Utility parameters. private final String URL = "url"; private final String USER = "user"; private final String PASS = "password"; private final String VER_COUNT = "versionCount"; private final String DEL_DATE = "purgeBeforeDate"; private final String PURGE_FILES = "purgeFiles"; private final String PURGE_REV = "purgeRevisions"; private final String LOG_LEVEL = "logLevel"; private final String LOG_FILE = "logFileName"; private final String PURGE_SHARED = "purgeSharedObjects"; // parameters in rest call that are not in command line private final String FILE_FILTER = "fileFilter"; // Constants. private final String SERVICE_NAME = "purge"; private final String BASE_PATH = "/plugin/pur-repository-plugin/api/purge"; private final String AUTHENTICATION = "/api/authorization/action/isauthorized?authAction="; private final String purgeBeforeDateFormat = "MM/dd/yyyy"; private final String logFileNameDateFormat = "YYYYMMdd-HHmmss"; private final String DEFAULT_LOG_FILE_PREFIX = "purge-utility-log-"; private final String OPTION_PREFIX = "-"; private final String NEW_LINE = "\n"; // Class properties. private String url = null; private String username = null; private String password = null; private int verCount = -1; private String delFrom = null; private String logLevel = null; private boolean purgeFiles = false; private boolean purgeRev = false; private boolean purgeShared = false; private String logFile = null; private String fileFilter; private String repositoryPath; /** * Main method * * @param args */ public static void main( String[] args ) { try { new RepositoryCleanupUtil().purge( args ); } catch ( Exception e ) { writeOut( e ); } exit( 0 ); } /** * Create parameters and send HTTP request to purge REST endpoint * * @param options */ public void purge( String[] options ) { FormDataMultiPart form = null; try { Map<String, String> parameters = parseExecutionOptions( options ); validateParameters( parameters ); authenticateLoginCredentials(); String serviceURL = createPurgeServiceURL(); form = createParametersForm(); WebResource resource = client.resource( serviceURL ); ClientResponse response = resource.type( MediaType.MULTIPART_FORM_DATA ).post( ClientResponse.class, form ); if ( response != null && response.getStatus() == 200 ) { String resultLog = response.getEntity( String.class ); String logName = writeLog( resultLog ); writeOut( Messages.getInstance().getString( "REPOSITORY_CLEANUP_UTIL.INFO_0001.OP_SUCCESS", logName ), false ); } else { writeOut( Messages.getInstance().getString( "REPOSITORY_CLEANUP_UTIL.ERROR_0001.OP_FAILURE" ), true ); } } catch ( Exception e ) { if ( e.getMessage() != null ) { System.out.println( e.getMessage() ); } else { if ( !( e instanceof NormalExitException ) ) { e.printStackTrace(); } } } finally { if ( client != null ) { client.destroy(); } if ( form != null ) { form.cleanup(); } } } /** * Attempt to parse command line arguments and create map of values * * @param args * @return * @throws Exception */ private Map<String, String> parseExecutionOptions( String[] args ) throws Exception { Map<String, String> arguments = new HashMap<String, String>(); String param; String value; try { for ( String arg : args ) { int equalsPos = arg.indexOf( "=" ); if ( equalsPos == -1 ) { param = arg; value = "true"; } else { param = arg.substring( 0, equalsPos ); value = arg.substring( equalsPos + 1, arg.length() ); } arguments.put( param, value ); } } catch ( Exception e ) { writeOut( Messages.getInstance().getString( "REPOSITORY_CLEANUP_UTIL.ERROR_0002.ERROR_PROC_PARAMS" ), true ); } if ( arguments.size() == 0 ) { writeOut( printHelp(), true ); } return arguments; } /** * Ensure provided parameters are present and what we expect * * @param arguments * @throws Exception */ private void validateParameters( Map<String, String> arguments ) throws Exception { String aUrl = arguments.get( OPTION_PREFIX + URL ); String aUser = arguments.get( OPTION_PREFIX + USER ); String aPassword = arguments.get( OPTION_PREFIX + PASS ); String aVerCount = arguments.get( OPTION_PREFIX + VER_COUNT ); String aDelFrom = arguments.get( OPTION_PREFIX + DEL_DATE ); String aPurgeFiles = arguments.get( OPTION_PREFIX + PURGE_FILES ); String aPurgeRev = arguments.get( OPTION_PREFIX + PURGE_REV ); String aPurgeShared = arguments.get( OPTION_PREFIX + PURGE_SHARED ); String aLogLevel = arguments.get( OPTION_PREFIX + LOG_LEVEL ); String aLogFile = arguments.get( OPTION_PREFIX + LOG_FILE ); StringBuffer errors = new StringBuffer(); boolean isValidOperationSelected = false; fileFilter = "*.kjb|*.ktr"; repositoryPath = "/"; purgeShared = false; if ( aLogLevel != null && !( aLogLevel.equals( "DEBUG" ) || aLogLevel.equals( "ERROR" ) || aLogLevel.equals( "FATAL" ) || aLogLevel.equals( "INFO" ) || aLogLevel.equals( "OFF" ) || aLogLevel.equals( "TRACE" ) || aLogLevel .equals( "WARN" ) ) ) { errors.append( OPTION_PREFIX + LOG_LEVEL + "=" + aLogLevel + " " + Messages.getInstance().getString( "REPOSITORY_CLEANUP_UTIL.ERROR_0003.INVALID_LOGLEVEL" ) + "\n" ); } else { logLevel = aLogLevel; } if ( aLogFile != null ) { File f = new File( aLogFile ); if ( f.exists() && f.isDirectory() ) { errors.append( Messages.getInstance().getString( "REPOSITORY_CLEANUP_UTIL.ERROR_0004.FOLDER_EXISTS", OPTION_PREFIX + LOG_FILE ) + "\n" ); } logFile = aLogFile; } if ( aUrl == null ) { errors.append( Messages.getInstance().getString( "REPOSITORY_CLEANUP_UTIL.ERROR_0005.MISSING_PARAM", OPTION_PREFIX + URL ) + "\n" ); } else { url = aUrl; } if ( aUser == null ) { errors.append( Messages.getInstance().getString( "REPOSITORY_CLEANUP_UTIL.ERROR_0005.MISSING_PARAM", OPTION_PREFIX + USER ) + "\n" ); } else { username = aUser; } if ( aPassword == null ) { errors.append( Messages.getInstance().getString( "REPOSITORY_CLEANUP_UTIL.ERROR_0005.MISSING_PARAM", OPTION_PREFIX + PASS ) + "\n" ); } else { password = aPassword; } if ( aPurgeFiles != null ) { if ( ( aPurgeFiles.equalsIgnoreCase( Boolean.TRUE.toString() ) || aPurgeFiles.equalsIgnoreCase( Boolean.FALSE .toString() ) ) ) { purgeFiles = Boolean.parseBoolean( aPurgeFiles ); isValidOperationSelected = true; } else { errors.append( Messages.getInstance().getString( "REPOSITORY_CLEANUP_UTIL.ERROR_0006.INVALID_BOOLEAN", OPTION_PREFIX + PURGE_FILES + "=" + aPurgeFiles ) + "\n" ); } } if ( aPurgeRev != null ) { if ( aPurgeRev.equalsIgnoreCase( Boolean.TRUE.toString() ) || aPurgeRev.equalsIgnoreCase( Boolean.FALSE.toString() ) ) { if ( isValidOperationSelected ) { errors.append( Messages.getInstance().getString( "REPOSITORY_CLEANUP_UTIL.ERROR_0010.INVALID_COMBINATION_OF_PARAMS" ) + "\n" ); } else { purgeRev = Boolean.parseBoolean( aPurgeRev ); isValidOperationSelected = true; } } else { errors.append( Messages.getInstance().getString( "REPOSITORY_CLEANUP_UTIL.ERROR_0006.INVALID_BOOLEAN", OPTION_PREFIX + PURGE_REV + "=" + aPurgeRev ) + "\n" ); } } if ( aPurgeShared != null ) { if ( Boolean.parseBoolean( aPurgeFiles ) != Boolean.TRUE ) { errors.append( Messages.getInstance().getString( "REPOSITORY_CLEANUP_UTIL.ERROR_0014.INVALID_PURGE_SHARED" ) ); } else { purgeShared = Boolean.parseBoolean( aPurgeShared ); } } if ( aDelFrom != null ) { // only allow one operation if ( isValidOperationSelected ) { errors.append( Messages.getInstance().getString( "REPOSITORY_CLEANUP_UTIL.ERROR_0010.INVALID_COMBINATION_OF_PARAMS" ) + "\n" ); } else { SimpleDateFormat sdf = new SimpleDateFormat( purgeBeforeDateFormat ); sdf.setLenient( false ); try { sdf.parse( aDelFrom ); delFrom = aDelFrom; isValidOperationSelected = true; } catch ( ParseException e ) { errors.append( Messages.getInstance().getString( "REPOSITORY_CLEANUP_UTIL.ERROR_0008.IMPROPERLY_FORMATTED_DATE", OPTION_PREFIX + DEL_DATE + "=" + aDelFrom, purgeBeforeDateFormat ) + "\n" ); } } } if ( aVerCount != null ) { // only allow one operation if ( isValidOperationSelected ) { errors.append( Messages.getInstance().getString( "REPOSITORY_CLEANUP_UTIL.ERROR_0010.INVALID_COMBINATION_OF_PARAMS" ) + "\n" ); } else { try { verCount = Integer.parseInt( aVerCount ); isValidOperationSelected = true; } catch ( NumberFormatException e ) { errors.append( Messages.getInstance().getString( "REPOSITORY_CLEANUP_UTIL.ERROR_0009.INVALID_INTEGER", OPTION_PREFIX + VER_COUNT + "=" + aVerCount ) + "\n" ); } } } if ( !isValidOperationSelected ) { errors.append( Messages.getInstance().getString( "REPOSITORY_CLEANUP_UTIL.ERROR_0013.MISSING_OPERATION" ) + "\n" ); } if ( errors.length() != 0 ) { errors.insert( 0, "\n\n" + Messages.getInstance().getString( "REPOSITORY_CLEANUP_UTIL.ERROR_0011.ERRORS_HEADER" ) + "\n" ); throw new Exception( errors.toString() ); } } /** * Use REST API to authenticate provided credentials * * @throws Exception */ private void authenticateLoginCredentials() throws Exception { if ( client == null ) { ClientConfig clientConfig = new DefaultClientConfig(); clientConfig.getFeatures().put( JSONConfiguration.FEATURE_POJO_MAPPING, Boolean.TRUE ); client = Client.create( clientConfig ); client.addFilter( new HTTPBasicAuthFilter( username, password ) ); } WebResource resource = client.resource( url + AUTHENTICATION + AdministerSecurityAction.NAME ); String response = resource.get( String.class ); if ( !response.equals( "true" ) ) { throw new Exception( Messages.getInstance().getString( "REPOSITORY_CLEANUP_UTIL.ERROR_0012.ACCESS_DENIED" ) ); } } /** * Create URL to access REST API based on provided parameters * * @return * @throws Exception */ private String createPurgeServiceURL() throws Exception { StringBuffer service = new StringBuffer(); service.append( url ); service.append( BASE_PATH ); service.append( "/" ); String path = RepositoryPathEncoder.encodeRepositoryPath( repositoryPath ); path = RepositoryPathEncoder.encode( path ); service.append( path + "/" ); service.append( SERVICE_NAME ); return service.toString(); } /** * Create payload to supply with POST to REST API * * @return */ private FormDataMultiPart createParametersForm() { FormDataMultiPart form = new FormDataMultiPart(); if ( verCount != -1 && !purgeRev ) { form.field( VER_COUNT, Integer.toString( verCount ), MediaType.MULTIPART_FORM_DATA_TYPE ); } if ( delFrom != null && !purgeRev ) { form.field( DEL_DATE, delFrom, MediaType.MULTIPART_FORM_DATA_TYPE ); } if ( fileFilter != null ) { form.field( FILE_FILTER, fileFilter, MediaType.MULTIPART_FORM_DATA_TYPE ); } if ( logLevel != null ) { form.field( LOG_LEVEL, logLevel, MediaType.MULTIPART_FORM_DATA_TYPE ); } if ( purgeFiles ) { form.field( PURGE_FILES, Boolean.toString( purgeFiles ), MediaType.MULTIPART_FORM_DATA_TYPE ); } if ( purgeRev ) { form.field( PURGE_REV, Boolean.toString( purgeRev ), MediaType.MULTIPART_FORM_DATA_TYPE ); } if ( purgeShared ) { form.field( PURGE_SHARED, Boolean.toString( purgeShared ), MediaType.MULTIPART_FORM_DATA_TYPE ); } return form; } /** * Generate help output * * @return */ private String printHelp() { // TODO improve this help description.... StringBuffer help = new StringBuffer(); help.append( "\n\n" + Messages.getInstance().getString( "REPOSITORY_CLEANUP_UTIL.INFO_0003.OPTIONS_HEADER" ) ); help.append( optionHelp( URL, Messages.getInstance().getString( "REPOSITORY_CLEANUP_UTIL.INFO_0004.URL_REQUIRED", URL ) ) ); help.append( optionHelp( USER, Messages.getInstance().getString( "REPOSITORY_CLEANUP_UTIL.INFO_0005.USER_REQUIRED", USER ) ) ); help.append( optionHelp( PASS, Messages.getInstance().getString( "REPOSITORY_CLEANUP_UTIL.INFO_0006.PASS_REQUIRED", PASS ) ) ); help.append( "\n" ); help.append( indentFormat( Messages.getInstance().getString( "REPOSITORY_CLEANUP_UTIL.INFO_0008.PARAMS_HELP", PURGE_SHARED ), 0, 0 ) ); help.append( optionHelp( VER_COUNT, Messages.getInstance().getString( "REPOSITORY_CLEANUP_UTIL.INFO_0009.VERSIONCOUNT_HELP", VER_COUNT ) ) ); help.append( optionHelp( DEL_DATE, Messages.getInstance().getString( "REPOSITORY_CLEANUP_UTIL.INFO_0010.DATE_HELP", DEL_DATE ) ) ); help.append( optionHelp( PURGE_FILES, Messages.getInstance().getString( "REPOSITORY_CLEANUP_UTIL.INFO_0011.PURGE_FILES_HELP", PURGE_FILES ) ) ); help.append( optionHelp( PURGE_REV, Messages.getInstance().getString( "REPOSITORY_CLEANUP_UTIL.INFO_0012.PURGE_REVS_HELP", PURGE_REV ) ) ); help.append( "\n\n" + Messages.getInstance().getString( "REPOSITORY_CLEANUP_UTIL.INFO_0013.OPTIONAL_PARAMS" ) ); help.append( optionHelp( LOG_FILE, Messages.getInstance().getString( "REPOSITORY_CLEANUP_UTIL.INFO_0014.LOGFILE_HELP", DEFAULT_LOG_FILE_PREFIX + logFileNameDateFormat, logFileNameDateFormat ) ) ); help.append( optionHelp( LOG_LEVEL, Messages.getInstance().getString( "REPOSITORY_CLEANUP_UTIL.INFO_0015.LOGLEVEL_HELP", LOG_LEVEL ) ) ); help.append( optionHelp( PURGE_SHARED, Messages.getInstance().getString( "REPOSITORY_CLEANUP_UTIL.INFO_0007.PURGE_SHARED", PURGE_FILES ) ) ); help.append( "\n\n" + Messages.getInstance().getString( "REPOSITORY_CLEANUP_UTIL.INFO_0016.EXAMPLES" ) ); help.append( indentFormat( Messages.getInstance().getString( "REPOSITORY_CLEANUP_UTIL.INFO_0017.EXAMPLE_1", OPTION_PREFIX + URL, OPTION_PREFIX + USER, OPTION_PREFIX + PASS, OPTION_PREFIX + PURGE_FILES ), 0, 3 ) ); help.append( indentFormat( Messages.getInstance().getString( "REPOSITORY_CLEANUP_UTIL.INFO_0018.EXAMPLE_1_DESC" ), 3, 3 ) ); help.append( indentFormat( Messages.getInstance().getString( "REPOSITORY_CLEANUP_UTIL.INFO_0019.EXAMPLE_2", OPTION_PREFIX + URL, OPTION_PREFIX + USER, OPTION_PREFIX + PASS, OPTION_PREFIX + PURGE_REV ), 0, 3 ) ); help.append( indentFormat( Messages.getInstance().getString( "REPOSITORY_CLEANUP_UTIL.INFO_0020.EXAMPLE_2_DESC" ), 3, 3 ) ); help.append( indentFormat( Messages.getInstance().getString( "REPOSITORY_CLEANUP_UTIL.INFO_0021.EXAMPLE_3", OPTION_PREFIX + URL, OPTION_PREFIX + USER, OPTION_PREFIX + PASS, OPTION_PREFIX + DEL_DATE ), 0, 3 ) ); help.append( indentFormat( Messages.getInstance().getString( "REPOSITORY_CLEANUP_UTIL.INFO_0022.EXAMPLE_3_DESC" ), 3, 3 ) ); help.append( indentFormat( Messages.getInstance().getString( "REPOSITORY_CLEANUP_UTIL.INFO_0023.EXAMPLE_4", OPTION_PREFIX + URL, OPTION_PREFIX + USER, OPTION_PREFIX + PASS, OPTION_PREFIX + PURGE_FILES, OPTION_PREFIX + PURGE_SHARED ), 0, 3 ) ); help.append( indentFormat( Messages.getInstance().getString( "REPOSITORY_CLEANUP_UTIL.INFO_0024.EXAMPLE_4_DESC" ), 3, 3 ) ); return help.toString(); } /** * Format string for option help * * @param optionName * @param descriptionText * @return */ private String optionHelp( String optionName, String descriptionText ) { int indentFirstLine = 2; int indentBalance = Math.min( OPTION_PREFIX.length() + optionName.length() + 4, 10 ); return indentFormat( OPTION_PREFIX + optionName + ": " + descriptionText, indentFirstLine, indentBalance ); } /** * Format strings for command line output * * @param unformattedText * @param indentFirstLine * @param indentBalance * @return */ private String indentFormat( String unformattedText, int indentFirstLine, int indentBalance ) { final int maxWidth = 79; String leadLine = WordUtils.wrap( unformattedText, maxWidth - indentFirstLine ); StringBuilder result = new StringBuilder(); result.append( "\n" ); if ( leadLine.indexOf( NEW_LINE ) == -1 ) { result.append( NEW_LINE ).append( StringUtils.repeat( " ", indentFirstLine ) ).append( unformattedText ); } else { int lineBreakPoint = leadLine.indexOf( NEW_LINE ); String indentString = StringUtils.repeat( " ", indentBalance ); result.append( NEW_LINE ).append( StringUtils.repeat( " ", indentFirstLine ) ).append( leadLine.substring( 0, lineBreakPoint ) ); String formattedText = WordUtils.wrap( unformattedText.substring( lineBreakPoint ), maxWidth - indentBalance ); for ( String line : formattedText.split( NEW_LINE ) ) { result.append( NEW_LINE ).append( indentString ).append( line ); } } return result.toString(); } /** * Write out to log file * * @param message * @return * @throws Exception */ private String writeLog( String message ) throws Exception { String logName; if ( logFile != null ) { logName = logFile; } else { DateFormat df = new SimpleDateFormat( logFileNameDateFormat ); logName = DEFAULT_LOG_FILE_PREFIX + df.format( new Date() ) + ".txt"; } File file = new File( logName ); FileOutputStream fout = FileUtils.openOutputStream( file ); IOUtils.copy( IOUtils.toInputStream( message ), fout ); fout.close(); return logName; } /** * Print output or error message * * @param message * @param isError */ private static void writeOut( String message, boolean isError ) { if ( isError ) { System.err.println( message ); exit( 1 ); } else { System.out.println( message ); } } private static void exit( int exitCode ) { if ( !testMode ) { System.exit( exitCode ); } else { throw new NormalExitException( exitCode ); } } /** * Print stack trace on error * * @param t */ private static void writeOut( Throwable t ) { t.printStackTrace(); exit( 1 ); } /** * When the command line would normally exit, this exception is thrown instead if we are running in test mode. This * prevents the junit tests from abnormally terminating but still captures the exit code. * * @author tkafalas * */ public static class NormalExitException extends RuntimeException { public int exitCode; public NormalExitException( int exitCode ) { super(); this.exitCode = exitCode; } } }