/*
* This program is free software; you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software
* Foundation.
*
* You should have received a copy of the GNU Lesser General Public License along with this
* program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
* or from the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
* 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 Lesser General Public License for more details.
*
* Copyright 2016 Pentaho Corporation. All rights reserved.
*/
package org.pentaho.platform.osgi;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.filefilter.AbstractFileFilter;
import org.apache.commons.lang.StringUtils;
import org.apache.karaf.main.Main;
import org.pentaho.di.core.KettleClientEnvironment;
import org.pentaho.platform.api.engine.IPentahoSession;
import org.pentaho.platform.api.engine.IPentahoSystemListener;
import org.pentaho.platform.engine.core.system.PentahoSystem;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileLock;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashSet;
import java.util.Properties;
import java.util.Set;
import java.util.UUID;
/**
* This Pentaho SystemListener starts the Embedded Karaf framework to support OSGI in the platform.
* <p/>
* Created by nbaker on 7/29/14.
*/
public class KarafBoot implements IPentahoSystemListener {
public static final String CLEAN_KARAF_CACHE = "org.pentaho.clean.karaf.cache";
private Main main;
private KarafInstance karafInstance;
Logger logger = LoggerFactory.getLogger( getClass() );
public static final String PENTAHO_KARAF_ROOT_COPY_DEST_FOLDER = "pentaho.karaf.root.copy.dest.folder";
public static final String PENTAHO_KARAF_ROOT_TRANSIENT = "pentaho.karaf.root.transient";
public static final String PENTAHO_KARAF_ROOT_TRANSIENT_DIRECTORY_ATTEMPTS =
"pentaho.karaf.root.transient.directory.attempts";
public static final String PENTAHO_KARAF_ROOT_COPY_FOLDER_SYMLINK_FILES =
"pentaho.karaf.root.copy.folder.symlink.files";
public static final String PENTAHO_KARAF_ROOT_COPY_FOLDER_EXCLUDE_FILES =
"pentaho.karaf.root.copy.folder.exclude.files";
public static final String ORG_OSGI_FRAMEWORK_SYSTEM_PACKAGES_EXTRA = "org.osgi.framework.system.packages.extra";
public static final String PENTAHO_KARAF_INSTANCE_RESOLVER_CLASS = "pentaho.karaf.instance.resolver.class";
private static final String SYSTEM_PROP_OSX_APP_ROOT_DIR = "osx.app.root.dir";
protected static final String KARAF_DIR = "/system/karaf";
private static FileFilter directoryFilter = new FileFilter() {
@Override public boolean accept( File pathname ) {
return pathname.isDirectory();
}
};;
@Override public boolean startup( IPentahoSession session ) {
try {
String solutionRootPath = PentahoSystem.getApplicationContext().getSolutionRootPath();
File karafDir = new File( solutionRootPath + KARAF_DIR );
if ( !karafDir.exists() ) {
logger.warn( "Karaf not found in standard dir of '" + ( solutionRootPath + KARAF_DIR ) + "' " );
String osxAppRootDir = System.getProperty( SYSTEM_PROP_OSX_APP_ROOT_DIR );
if ( !StringUtils.isEmpty( osxAppRootDir ) ) {
logger.warn( "Given that the system property '" + SYSTEM_PROP_OSX_APP_ROOT_DIR + "' is set, we are in "
+ "a OSX .app context; we'll try looking for Karaf in the app's root dir '" + osxAppRootDir + "' " );
File osxAppKarafDir = new File( osxAppRootDir + KARAF_DIR );
if ( osxAppKarafDir.exists() ) {
karafDir = osxAppKarafDir; // karaf found in the app's root dir
}
}
}
String root = karafDir.toURI().getPath();
// See if user specified a karaf folder they would like to use
String rootCopyFolderString = System.getProperty( PENTAHO_KARAF_ROOT_COPY_DEST_FOLDER );
// Use a transient folder (will be deleted on exit) if user says to or cannot open config.properties
boolean transientRoot = Boolean.parseBoolean( System.getProperty( PENTAHO_KARAF_ROOT_TRANSIENT, "false" ) );
if ( rootCopyFolderString == null && !canOpenConfigPropertiesForEdit( root ) ) {
transientRoot = true;
}
final File destDir;
if ( transientRoot ) {
if ( rootCopyFolderString == null ) {
destDir = Files.createTempDirectory( "karaf" ).toFile();
} else {
int directoryAttempts =
Integer.parseInt( System.getProperty( PENTAHO_KARAF_ROOT_TRANSIENT_DIRECTORY_ATTEMPTS, "250" ) );
File candidate = new File( rootCopyFolderString );
int i = 1;
while ( candidate.exists() || !candidate.mkdirs() ) {
if ( i > directoryAttempts ) {
candidate = Files.createTempDirectory( "karaf" ).toFile();
logger.warn( "Unable to create " + rootCopyFolderString + " after " + i + " attempts, using temp dir "
+ candidate );
break;
}
candidate = new File( rootCopyFolderString + ( i++ ) );
}
destDir = candidate;
}
Runtime.getRuntime().addShutdownHook( new Thread( new Runnable() {
@Override public void run() {
try {
if ( main != null ) {
main.destroy();
}
//release lock
if ( karafInstance != null ) {
karafInstance.close();
}
deleteRecursiveIfExists( destDir );
} catch ( IOException e ) {
logger.error( "Unable to delete karaf directory " + destDir, e );
} catch ( Exception e ) {
logger.error( "Error stopping Karaf", e );
}
}
} ) );
} else if ( rootCopyFolderString != null ) {
destDir = new File( rootCopyFolderString );
} else {
destDir = null;
}
// Copy karaf (symlinking allowed files/folders if possible)
if ( destDir != null && ( transientRoot || !destDir.exists() ) ) {
final Set<String> symlinks = new HashSet<>();
String symlinkFiles = System.getProperty( PENTAHO_KARAF_ROOT_COPY_FOLDER_SYMLINK_FILES, "lib,system" );
if ( symlinkFiles != null ) {
for ( String symlink : symlinkFiles.split( "," ) ) {
symlinks.add( symlink.trim() );
}
}
final Set<String> excludes = new HashSet<>();
String excludeFiles = System.getProperty( PENTAHO_KARAF_ROOT_COPY_FOLDER_EXCLUDE_FILES, "caches" );
if ( excludeFiles != null ) {
for ( String exclude : excludeFiles.split( "," ) ) {
excludes.add( exclude.trim() );
}
}
final Path karafDirPath = Paths.get( karafDir.toURI() );
FileUtils.copyDirectory( karafDir, destDir, new AbstractFileFilter() {
@Override public boolean accept( File file ) {
Path filePath = Paths.get( file.toURI() );
String relativePath = karafDirPath.relativize( filePath ).toString();
if ( excludes.contains( relativePath ) ) {
return false;
} else if ( symlinks.contains( relativePath ) ) {
File linkFile = new File( destDir, relativePath );
linkFile.getParentFile().mkdirs();
Path link = Paths.get( linkFile.toURI() );
try {
// Try to create a symlink and skip the copy if successful
if ( Files.createSymbolicLink( link, filePath ) != null ) {
return false;
}
} catch ( IOException e ) {
logger
.warn( "Unable to create symlink " + linkFile.getAbsolutePath() + " -> " + file.getAbsolutePath() );
}
}
return true;
}
} );
}
if ( destDir != null ) {
root = destDir.toURI().getPath();
}
configureSystemProperties( solutionRootPath, root );
String customLocation = root + "/etc/custom.properties";
expandSystemPackages( customLocation );
cleanCachesIfFlagSet( root );
// Setup karaf instance configuration
karafInstance = createAndProcessKarafInstance( root );
// Wrap the startup of Karaf in a child thread which has explicitly set a bogus authentication. This is
// work-around and issue with Karaf inheriting the Authenticaiton set on the main system thread due to the
// InheritableThreadLocal backing the SecurityContext. By setting a fake authentication, calls to the
// org.pentaho.platform.osgi.SpringSecurityLoginModule always challenge the user.
Thread karafThread = new Thread( new Runnable() {
@Override public void run() {
// Bogus authentication
SecurityContextHolder.getContext().setAuthentication( new UsernamePasswordAuthenticationToken(
UUID.randomUUID().toString(), "" ) );
main = new Main( new String[ 0 ] );
try {
main.launch();
} catch ( Exception e ) {
main = null;
logger.error( "Error starting Karaf", e );
}
}
} );
karafThread.setDaemon( true );
karafThread.run();
karafThread.join();
} catch ( Exception e ) {
main = null;
logger.error( "Error starting Karaf", e );
}
return main != null;
}
void cleanCachesIfFlagSet( String root ) throws IOException {
String customLocation = root + "/etc/custom.properties";
// Check to see if the clean cache property is set. If so delete data and recreate before launching.
logger.info( "Checking to see if " + CLEAN_KARAF_CACHE + " is enabled" );
Properties customProps = new Properties();
FileInputStream fileInputStream = null;
try {
fileInputStream = new FileInputStream( new File( customLocation ) );
customProps.load( fileInputStream );
} finally {
if ( fileInputStream != null ) {
IOUtils.closeQuietly( fileInputStream );
}
}
String cleanCache = customProps.getProperty( CLEAN_KARAF_CACHE, "false" );
if ( "true".equals( cleanCache ) ) {
logger.info( CLEAN_KARAF_CACHE + " is enabled. Karaf data directories will be deleted" );
File cacheParent = new File( root + "/caches" );
File[] clientCacheFolders = cacheParent.listFiles( directoryFilter );
for ( File clientCacheFolder : clientCacheFolders ) {
File[] cacheDirs = clientCacheFolder.listFiles( directoryFilter );
for ( File cacheDir : cacheDirs ) {
File lockFile = new File( cacheDir, ".lock" );
FileOutputStream fileOutputStream = new FileOutputStream( lockFile );
try {
FileLock fileLock = fileOutputStream.getChannel().tryLock();
fileLock.release();
IOUtils.closeQuietly( fileOutputStream );
FileUtils.deleteDirectory( cacheDir );
} catch ( Exception ignored ) {
// lock active by another program
} finally {
IOUtils.closeQuietly( fileOutputStream );
}
}
}
customProps.setProperty( CLEAN_KARAF_CACHE, "false" );
FileOutputStream out = null;
try {
out = new FileOutputStream( customLocation );
logger.info( "Setting " + CLEAN_KARAF_CACHE + " back to false as this is a one-time action" );
customProps.store( out, "Turning of one-time cache clean setting" );
} finally {
if ( out != null ) {
IOUtils.closeQuietly( out );
}
}
}
}
public static void deleteRecursiveIfExists( File item ) {
if ( !item.exists() ) {
return;
}
if ( !Files.isSymbolicLink( item.toPath() ) && item.isDirectory() ) {
File[] subitems = item.listFiles();
for ( File subitem : subitems ) {
deleteRecursiveIfExists( subitem );
}
}
item.delete();
return;
}
protected KarafInstance createAndProcessKarafInstance( String root )
throws FileNotFoundException, KarafInstanceResolverException {
String clientType = new ExceptionBasedClientTypeProvider().getClientType();
KarafInstance karafInstance = new KarafInstance( root, clientType );
karafInstance.assignPortsAndCreateCache();
return karafInstance;
}
protected void configureSystemProperties( String solutionRootPath, String root ) {
fillMissedSystemProperty( "karaf.home", root );
fillMissedSystemProperty( "karaf.base", root );
fillMissedSystemProperty( "karaf.history", root + "/data/history.txt" );
fillMissedSystemProperty( "karaf.instances", root + "/instances" );
fillMissedSystemProperty( "karaf.startLocalConsole", "false" );
fillMissedSystemProperty( "karaf.startRemoteShell", "true" );
fillMissedSystemProperty( "karaf.lock", "false" );
fillMissedSystemProperty( "karaf.etc", root + "/etc" );
// When running in the PDI-Clients there are separate etc directories so that features can be customized for
// the particular execution needs (Carte, Spoon, Pan, Kitchen)
KettleClientEnvironment.ClientType clientType = getClientType();
String extraKettleEtc = translateToExtraKettleEtc( clientType );
// If clientType is null, the extraKettleEtc will return 'etc-default'
// Added a check to see if the folder exist before setting the system property
if ( extraKettleEtc != null && new File( root + extraKettleEtc ).exists() ) {
System.setProperty( "felix.fileinstall.dir", root + "/etc" + "," + root + extraKettleEtc );
} else {
System.setProperty( "felix.fileinstall.dir", root + "/etc" );
}
// Tell others like the pdi-osgi-bridge that there's already a karaf instance running so they don't start
// their own.
System.setProperty( "embedded.karaf.mode", "true" );
// set the location of the log4j config file, since OSGI won't pick up the one in webapp
File file = new File( solutionRootPath + "/system/osgi/log4j.xml" );
if ( file.exists() ) {
System.setProperty( "log4j.configuration", file.toURI().toString() );
} else {
logger.warn( file.toURI().toString() + " file not exist" );
}
// Setting ignoreTCL to true such that the OSGI classloader used to initialize log4j will be the
// same one used when instatiating appenders.
System.setProperty( "log4j.ignoreTCL", "true" );
}
/**
* If property with propertyName does not exist, than set property with value propertyValue
*
* @param propertyName - property for check
* @param propertyValue - value to set if property null
*/
protected void fillMissedSystemProperty( String propertyName, String propertyValue ) {
if ( System.getProperty( propertyName ) == null ) {
System.setProperty( propertyName, propertyValue );
}
}
protected String translateToExtraKettleEtc( KettleClientEnvironment.ClientType clientType ) {
if ( clientType != null ) {
switch ( clientType ) {
case SPOON:
case PAN:
case KITCHEN:
case CARTE:
case OTHER:
return "/etc-" + clientType.getID().toLowerCase();
}
}
return "/etc-default";
}
protected KettleClientEnvironment.ClientType getClientType() {
return KettleClientEnvironment.getInstance().getClient();
}
boolean canOpenConfigPropertiesForEdit( String directory ) {
String testFile = directory + "/etc/config.properties";
FileOutputStream fileOutputStream = null;
try {
fileOutputStream = new FileOutputStream( testFile, true );
} catch ( IOException e ) {
return false;
} finally {
if ( fileOutputStream != null ) {
try {
fileOutputStream.close();
} catch ( IOException e ) {
// Ignore
}
}
}
return true;
}
void expandSystemPackages( String s ) {
File customFile = new File( s );
if ( !customFile.exists() ) {
logger.warn( "No custom.properties file for in karaf distribution." );
return;
}
Properties properties = new Properties();
FileInputStream inStream = null;
try {
inStream = new FileInputStream( customFile );
properties.load( inStream );
} catch ( IOException e ) {
logger
.error( "Not able to expand system.packages.extra properties due to an error loading custom.properties", e );
return;
} finally {
IOUtils.closeQuietly( inStream );
}
properties = new SystemPackageExtrapolator().expandProperties( properties );
System.setProperty( ORG_OSGI_FRAMEWORK_SYSTEM_PACKAGES_EXTRA,
properties.getProperty( ORG_OSGI_FRAMEWORK_SYSTEM_PACKAGES_EXTRA ) );
}
@Override public void shutdown() {
try {
if ( main != null ) {
main.destroy();
}
} catch ( Exception e ) {
logger.error( "Error stopping Karaf", e );
}
}
}