/** * Copyright 2015-2017 Linagora, Université Joseph Fourier, Floralis * * The present code is developed in the scope of the joint LINAGORA - * Université Joseph Fourier - Floralis research program and is designated * as a "Result" pursuant to the terms and conditions of the LINAGORA * - Université Joseph Fourier - Floralis research program. Each copyright * holder of Results enumerated here above fully & independently holds complete * ownership of the complete Intellectual Property rights applicable to the whole * of said Results, and may freely exploit it in any manner which does not infringe * the moral rights of the other copyright holders. * * 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 net.roboconf.dm.internal.api.impl; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Properties; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Logger; import java.util.regex.Pattern; import net.roboconf.core.Constants; import net.roboconf.core.ErrorCode; import net.roboconf.core.model.TargetValidator; import net.roboconf.core.model.beans.AbstractApplication; import net.roboconf.core.model.beans.Application; import net.roboconf.core.model.beans.ApplicationTemplate; import net.roboconf.core.model.beans.Component; import net.roboconf.core.model.beans.Instance; import net.roboconf.core.model.beans.Instance.InstanceStatus; import net.roboconf.core.model.helpers.ComponentHelpers; import net.roboconf.core.model.helpers.InstanceHelpers; import net.roboconf.core.model.helpers.RoboconfErrorHelpers; import net.roboconf.core.model.runtime.TargetUsageItem; import net.roboconf.core.model.runtime.TargetWrapperDescriptor; import net.roboconf.core.utils.Utils; import net.roboconf.dm.internal.api.impl.beans.InstanceContext; import net.roboconf.dm.internal.utils.ConfigurationUtils; import net.roboconf.dm.internal.utils.TargetHelpers; import net.roboconf.dm.management.api.IConfigurationMngr; import net.roboconf.dm.management.api.ITargetsMngr; import net.roboconf.dm.management.exceptions.UnauthorizedActionException; /** * Here is the way this class stores information. * <p> * Each target has an ID, which is an integer. And each target has its * own directory, located under the DM's configuration directory. * </p> * <p> * Each target may contain... * </p> * <ul> * <li>target.properties (mandatory)</li> * <li>{@value #TARGETS_ASSOC_FILE} (mapping between this target and application instances)</li> * <li>{@value #TARGETS_HINTS_FILE} (user preferences to show or hide targets for a given application)</li> * <li>{@value #TARGETS_USAGE_FILE} (mapping indicating which instances have deployed things with this target)</li> * </ul> * * @author Vincent Zurczak - Linagora * @author Amadou Diarra - UGA */ public class TargetsMngrImpl implements ITargetsMngr { private static final String TARGETS_ASSOC_FILE = "associations.properties"; private static final String TARGETS_HINTS_FILE = "hints.properties"; private static final String TARGETS_USAGE_FILE = "usage.properties"; private static final String CREATED_BY = "created.from"; private static final Object LOCK = new Object(); private final Logger logger = Logger.getLogger( getClass().getName()); private final IConfigurationMngr configurationMngr; private final Map<InstanceContext,String> instanceToCachedId; final ConcurrentHashMap<String,Boolean> targetIds = new ConcurrentHashMap<> (); /** * Constructor. * @param configurationMngr */ public TargetsMngrImpl( IConfigurationMngr configurationMngr ) { this.configurationMngr = configurationMngr; this.instanceToCachedId = new ConcurrentHashMap<> (); // Restore the cache restoreAssociationsCache(); } // CRUD operations on targets @Override public String createTarget( String targetContent ) throws IOException { // Get the target ID TargetValidator tv = new TargetValidator( targetContent ); tv.validate(); if( RoboconfErrorHelpers.containsCriticalErrors( tv.getErrors())) throw new IOException( "There are errors in the target definition." ); // Critical section to insert a target. // Store the ID, it cannot be reused. String targetId = tv.getProperties().getProperty( Constants.TARGET_PROPERTY_ID ); String creator = tv.getProperties().getProperty( CREATED_BY ); if( this.targetIds.putIfAbsent( targetId, Boolean.TRUE ) != null ) { // No creator? Then there is a conflict. if( creator == null ) throw new IOException( "ID " + targetId + " is already used." ); // It cannot be reused, unless they were created by the same template. File createdByFile = new File( findTargetDirectory( targetId ), CREATED_BY ); String storedCreator = null; if( createdByFile.exists()) storedCreator = Utils.readFileContent( createdByFile ); // If they do not match, throw an exception if( ! Objects.equals( creator, storedCreator )) throw new IOException( "ID " + targetId + " is already used." ); // Otherwise, we can override the properties } // Rewrite the properties without the ID. // We do not want it to be modified later. // We do not serialize java.util.properties#store because it adds // a time stamp, removes user comments and looses the properties order. targetContent = targetContent.replaceAll( Constants.TARGET_PROPERTY_ID + "\\s*(:|=)[^\n]*(\n|$)", "" ); // For the same reason, we remove the "created.by" property targetContent = targetContent.replaceAll( "\n\n" + Pattern.quote( CREATED_BY ) + "\\s*(:|=)[^\n]*(\n|$)", "" ); // Write the properties File targetFile = new File( findTargetDirectory( targetId ), Constants.TARGET_PROPERTIES_FILE_NAME ); Utils.createDirectory( targetFile.getParentFile()); Utils.writeStringInto( targetContent, targetFile ); // Write the creator, if any if( creator != null ) { File createdByFile = new File( findTargetDirectory( targetId ), CREATED_BY ); Utils.writeStringInto( creator, createdByFile ); } return targetId; } @Override public String createTarget( File targetPropertiesFile, ApplicationTemplate creator ) throws IOException { String fileContent = Utils.readFileContent( targetPropertiesFile ); // So that application templates can redefine some targets (when they are redeployed), // we insert a custom property when StringBuilder sb = new StringBuilder( fileContent ); if( creator != null ) { sb.append( "\n\n" ); sb.append( CREATED_BY ); sb.append( ": " ); sb.append( creator.getName()); sb.append( " - " ); sb.append( creator.getQualifier()); } // Create a target String targetId = createTarget( sb.toString()); // Copy script files in target directory String prefix = Utils.removeFileExtension( targetPropertiesFile.getName()); File scriptsInDir = new File( targetPropertiesFile.getParentFile(), prefix ); File scriptsOutDir = new File( findTargetDirectory( targetId ), Constants.PROJECT_SUB_DIR_SCRIPTS ); List<File> scriptFiles; if( scriptsInDir.exists()) scriptFiles = Utils.listAllFiles( scriptsInDir ); else scriptFiles = Collections.emptyList(); if( ! scriptFiles.isEmpty()) { Utils.createDirectory( scriptsOutDir ); for( File inputFile : scriptFiles ) { String relativePath = Utils.computeFileRelativeLocation( scriptsInDir, inputFile ); File outputFile = new File( scriptsOutDir, relativePath ); Utils.createDirectory( outputFile.getParentFile()); Utils.copyStream( inputFile, outputFile ); } } return targetId; } @Override public void updateTarget( String targetId, String newTargetContent ) throws IOException, UnauthorizedActionException { // Validate the new content TargetValidator tv = new TargetValidator( newTargetContent ); tv.validate(); // We should ignore all the ID related errors as we do not care anymore // (only at the creation, not on updates) RoboconfErrorHelpers.filterErrors( tv.getErrors(), ErrorCode.REC_TARGET_NO_ID, ErrorCode.REC_TARGET_CONFLICTING_ID ); if( RoboconfErrorHelpers.containsCriticalErrors( tv.getErrors())) throw new IOException( "There are errors in the target definition." ); // Write it File targetFile = findTargetFile( targetId, Constants.TARGET_PROPERTIES_FILE_NAME ); if( targetFile == null ) throw new UnauthorizedActionException( "Target " + targetId + " does not exist." ); Utils.writeStringInto( newTargetContent, targetFile ); } @Override public void deleteTarget( String targetId ) throws IOException, UnauthorizedActionException { // No machine using this target can be running. boolean used = false; synchronized( LOCK ) { used = isTargetUsed( targetId ); } if( used ) throw new UnauthorizedActionException( "Deletion is not permitted." ); // Delete the files related to this target this.targetIds.remove( targetId ); File targetDirectory = findTargetDirectory( targetId ); Utils.deleteFilesRecursively( targetDirectory ); } // Associating targets and application instances / components @Override public void associateTargetWith( String targetId, AbstractApplication app, String instancePathOrComponentName ) throws IOException, UnauthorizedActionException { boolean valid = false; if( instancePathOrComponentName == null ) { valid = true; } else if( instancePathOrComponentName.startsWith( "@" )) { Component comp = ComponentHelpers.findComponent( app, instancePathOrComponentName.substring( 1 )); valid = comp != null; } else { Instance instance = InstanceHelpers.findInstanceByPath( app, instancePathOrComponentName ); valid = instance != null; if( instance != null ) { if( instance.getStatus() != InstanceStatus.NOT_DEPLOYED ) throw new UnauthorizedActionException( "Operation not allowed: " + app + " :: " + instancePathOrComponentName + " should be not deployed." ); if( ! InstanceHelpers.isTarget( instance )) throw new IllegalArgumentException( "Only scoped instances can be associated with targets. Path in error: " + instancePathOrComponentName ); } } if( valid ) saveAssociation( app, targetId, instancePathOrComponentName, true ); } @Override public void dissociateTargetFrom( AbstractApplication app, String instancePathOrComponentName ) throws IOException, UnauthorizedActionException { if( instancePathOrComponentName != null && ! instancePathOrComponentName.startsWith( "@" )) { Instance instance = InstanceHelpers.findInstanceByPath( app, instancePathOrComponentName ); if( instance != null ) { if( instance.getStatus() != InstanceStatus.NOT_DEPLOYED ) throw new UnauthorizedActionException( "Operation not allowed: " + app + " :: " + instancePathOrComponentName + " should be not deployed." ); if( ! InstanceHelpers.isTarget( instance )) throw new IllegalArgumentException( "Only scoped instances can be associated with targets. Path in error: " + instancePathOrComponentName ); } } saveAssociation( app, null, instancePathOrComponentName, false ); } @Override public void copyOriginalMapping( Application app ) throws IOException { List<InstanceContext> keys = new ArrayList<> (); // Null <=> The default for the application keys.add( new InstanceContext( app.getTemplate(), (String) null )); // We can search defaults only for the existing instances for( Instance scopedInstance : InstanceHelpers.findAllScopedInstances( app )) keys.add( new InstanceContext( app.getTemplate(), scopedInstance )); // Copy mappings for the components for( Component comp : ComponentHelpers.findAllComponents( app )) { if( ComponentHelpers.isTarget( comp )) keys.add( new InstanceContext( app.getTemplate(), "@" + comp.getName())); } // Copy the associations when they exist for the template for( InstanceContext key : keys ) { String targetId = this.instanceToCachedId.get( key ); try { if( targetId != null ) associateTargetWith( targetId, app, key.getInstancePathOrComponentName()); } catch( UnauthorizedActionException e ) { // This method could be used to reset the mappings / associations. // However, this is not possible if the current instance is already deployed. // So, we just skip it. this.logger.severe( e.getMessage()); Utils.logException( this.logger, e ); } } } @Override public void applicationWasDeleted( AbstractApplication app ) throws IOException { String name = app.getName(); String qualifier = app instanceof ApplicationTemplate ? ((ApplicationTemplate) app).getQualifier() : null; List<InstanceContext> toClean = new ArrayList<> (); Set<String> targetIds = new HashSet<> (); // Find the mapping keys and the targets to update for( Map.Entry<InstanceContext,String> entry : this.instanceToCachedId.entrySet()) { if( Objects.equals( name, entry.getKey().getName()) && Objects.equals( qualifier, entry.getKey().getQualifier())) { targetIds.add( entry.getValue()); toClean.add( entry.getKey()); } } // Once we have the target IDs, update the list of entries to remove for( Instance scopedInstance : InstanceHelpers.findAllScopedInstances( app )) { InstanceContext ctx = new InstanceContext( app, scopedInstance ); toClean.add( ctx ); } // Update the target files for( String targetId : targetIds ) { File targetDirectory = findTargetDirectory( targetId ); // Update the association and usage files File[] files = new File[] { new File( targetDirectory, TARGETS_ASSOC_FILE ), new File( targetDirectory, TARGETS_HINTS_FILE ), new File( targetDirectory, TARGETS_USAGE_FILE ) }; for( File f : files ) { for( InstanceContext key : toClean ) { Properties props = Utils.readPropertiesFileQuietly( f, this.logger ); props.remove( key.toString()); writeProperties( props, f ); } } } // Update the cache for( InstanceContext key : toClean ) this.instanceToCachedId.remove( key ); } // Finding targets @Override public Map<String,String> findRawTargetProperties( AbstractApplication app, String instancePath ) { Map<String,String> result = new HashMap<> (); String targetId = findTargetId( app, instancePath ); if( targetId != null ) { File f = findTargetFile( targetId, Constants.TARGET_PROPERTIES_FILE_NAME ); for( Map.Entry<Object,Object> entry : Utils.readPropertiesFileQuietly( f, this.logger ).entrySet()) { result.put((String) entry.getKey(), (String) entry.getValue()); } } return result; } @Override public String findRawTargetProperties( String targetId ) { File f = findTargetFile( targetId, Constants.TARGET_PROPERTIES_FILE_NAME ); String content = null; try { if( f != null ) content = Utils.readFileContent( f ); } catch( IOException e ) { this.logger.severe( "Raw properties could not be read for target " + targetId ); Utils.logException( this.logger, e ); } return content; } @Override public String findTargetId( AbstractApplication app, String instancePath, boolean strict ) { // Specific association for this instance InstanceContext key = new InstanceContext( app, instancePath ); String targetId = this.instanceToCachedId.get( key ); // Non-scoped instances cannot be associated with targets. // Such a query is a coding error! Instance inst = InstanceHelpers.findInstanceByPath( app, instancePath ); if( inst != null && ! InstanceHelpers.isTarget( inst )) throw new IllegalArgumentException( "Targets aimed at being queried for scoped instances only. Invalid path: " + instancePath ); // Association inherited from the component if( targetId == null && ! strict ) { if( inst != null ) { key = new InstanceContext( app, "@" + inst.getComponent().getName()); targetId = this.instanceToCachedId.get( key ); } } // Default target for this application if( targetId == null && ! strict ) { key = new InstanceContext( app, (String) null ); targetId = this.instanceToCachedId.get( key ); } return targetId; } @Override public String findTargetId( AbstractApplication app, String instancePath ) { return findTargetId( app, instancePath, false ); } // Finding script resources @Override public Map<String,byte[]> findScriptResourcesForAgent( String targetId ) throws IOException { Map<String,byte[]> result = new HashMap<>( 0 ); File targetDir = new File( findTargetDirectory( targetId ), Constants.PROJECT_SUB_DIR_SCRIPTS ); if( targetDir.isDirectory()){ List<String> exclusionPatterns = Arrays.asList( "(?i).*" + Pattern.quote( Constants.LOCAL_RESOURCE_PREFIX ) + ".*" ); result.putAll( Utils.storeDirectoryResourcesAsBytes( targetDir, exclusionPatterns )); } return result; } @Override public Map<String,byte[]> findScriptResourcesForAgent( AbstractApplication app, Instance scopedInstance ) throws IOException { Map<String,byte[]> result = new HashMap<> (); String targetId = findTargetId( app, InstanceHelpers.computeInstancePath( scopedInstance )); if( targetId != null ) result = findScriptResourcesForAgent( targetId ); return result; } @Override public File findScriptForDm( AbstractApplication app, Instance scopedInstance ) { File result = null; String targetId = findTargetId( app, InstanceHelpers.computeInstancePath( scopedInstance )); if( targetId != null ) { File targetDir = new File( findTargetDirectory( targetId ), Constants.PROJECT_SUB_DIR_SCRIPTS ); if( targetDir.isDirectory()){ for( File f : Utils.listAllFiles( targetDir )) { if( f.getName().toLowerCase().contains( Constants.SCOPED_SCRIPT_AT_DM_CONFIGURE_SUFFIX )) { result = f; break; } } } } return result; } @Override public List<TargetWrapperDescriptor> listAllTargets() { File dir = new File( this.configurationMngr.getWorkingDirectory(), ConfigurationUtils.TARGETS ); List<File> targetDirectories = Utils.listDirectories( dir ); return buildList( targetDirectories, null ); } @Override public TargetWrapperDescriptor findTargetById( String targetId ) { File dir = new File( this.configurationMngr.getWorkingDirectory(), ConfigurationUtils.TARGETS ); File targetDirectory = new File( dir, targetId ); return build( targetDirectory ); } // In relation with hints @Override public List<TargetWrapperDescriptor> listPossibleTargets( AbstractApplication app ) { // Find the matching targets based on registered hints String key = new InstanceContext( app ).toString(); String tplKey = null; if( app instanceof Application ) tplKey = new InstanceContext(((Application) app).getTemplate()).toString(); List<File> targetDirectories = new ArrayList<> (); File dir = new File( this.configurationMngr.getWorkingDirectory(), ConfigurationUtils.TARGETS ); for( File f : Utils.listDirectories( dir )) { // If there is no hint for this target, then it is global. // We can list it. File hintsFile = new File( f, TARGETS_HINTS_FILE ); if( ! hintsFile.exists()) { targetDirectories.add( f ); continue; } // Otherwise, the key must exist in the file Properties props = Utils.readPropertiesFileQuietly( hintsFile, this.logger ); if( props.containsKey( key )) targetDirectories.add( f ); else if( tplKey != null && props.containsKey( tplKey )) targetDirectories.add( f ); } // Build the result return buildList( targetDirectories, app ); } @Override public void addHint( String targetId, AbstractApplication app ) throws IOException { saveHint( targetId, app, true ); } @Override public void removeHint( String targetId, AbstractApplication app ) throws IOException { saveHint( targetId, app, false ); } // Atomic operations... @Override public Map<String,String> lockAndGetTarget( Application app, Instance scopedInstance ) throws IOException { String instancePath = InstanceHelpers.computeInstancePath( scopedInstance ); String targetId = findTargetId( app, instancePath ); if( targetId == null ) throw new IOException( "No target was found for " + app + " :: " + instancePath ); InstanceContext mappingKey = new InstanceContext( app, instancePath ); synchronized( LOCK ) { saveUsage( mappingKey, targetId, true ); } this.logger.fine( "Target " + targetId + "'s lock was acquired for " + instancePath ); Map<String,String> result = findRawTargetProperties( app, instancePath ); return TargetHelpers.expandProperties( scopedInstance, result ); } @Override public void unlockTarget( Application app, Instance scopedInstance ) throws IOException { String instancePath = InstanceHelpers.computeInstancePath( scopedInstance ); String targetId = findTargetId( app, instancePath ); InstanceContext mappingKey = new InstanceContext( app, instancePath ); synchronized( LOCK ) { saveUsage( mappingKey, targetId, false ); } this.logger.fine( "Target " + targetId + "'s lock was released for " + instancePath ); } // Diagnostics @Override public List<TargetUsageItem> findUsageStatistics( String targetId ) { // Get usage first List<String> appNames; synchronized( LOCK ) { appNames = applicationsThatUse( targetId ); } // Now, let's build the result Set<TargetUsageItem> result = new HashSet<> (); for( Map.Entry<InstanceContext,String> entry : this.instanceToCachedId.entrySet()) { if( ! entry.getValue().equals( targetId )) continue; String appName = entry.getKey().getName(); TargetUsageItem item = new TargetUsageItem(); item.setName( appName ); item.setQualifier( entry.getKey().getQualifier()); item.setReferencing( true ); item.setUsing( appNames.contains( appName )); result.add( item ); } return new ArrayList<>( result ); } // Package methods List<TargetWrapperDescriptor> buildList( List<File> targetDirectories, AbstractApplication app ) { List<TargetWrapperDescriptor> result = new ArrayList<> (); for( File targetDirectory : targetDirectories ) { File associationFile = new File( targetDirectory, TARGETS_ASSOC_FILE ); Properties props = Utils.readPropertiesFileQuietly( associationFile, this.logger ); boolean isDefault = false; if( app != null ) isDefault = props.containsKey( new InstanceContext( app ).toString()); TargetWrapperDescriptor tb = build( targetDirectory ); if( tb != null ) { tb.setDefault( isDefault ); result.add( tb ); } } return result; } TargetWrapperDescriptor build( File targetDirectory ) { TargetWrapperDescriptor tb = null; File targetPropertiesFile = new File( targetDirectory, Constants.TARGET_PROPERTIES_FILE_NAME ); try { Properties props = Utils.readPropertiesFile( targetPropertiesFile ); tb = new TargetWrapperDescriptor(); tb.setId( targetDirectory.getName()); tb.setName( props.getProperty( Constants.TARGET_PROPERTY_NAME )); tb.setDescription( props.getProperty( Constants.TARGET_PROPERTY_DESCRIPTION )); String handler = TargetHelpers.findTargetHandlerName( props ); tb.setHandler( handler ); } catch( IOException e ) { this.logger.severe( "Properties of the target #" + targetDirectory.getName() + " could not be read." ); Utils.logException( this.logger, e ); } return tb; } // Private methods private void saveAssociation( AbstractApplication app, String targetId, String instancePathOrComponentName, boolean add ) throws IOException { // Association means an exact mapping between an application instance // and a target ID. InstanceContext key = new InstanceContext( app, instancePathOrComponentName ); // Remove the old association, always. if( instancePathOrComponentName != null ) { String oldTargetId = this.instanceToCachedId.remove( key ); if( oldTargetId != null ) { File associationFile = findTargetFile( oldTargetId, TARGETS_ASSOC_FILE ); Properties props = Utils.readPropertiesFileQuietly( associationFile, this.logger ); props.remove( key.toString()); writeProperties( props, associationFile ); } } // Register a potential new association and update the cache. if( add ) { File associationFile = findTargetFile( targetId, TARGETS_ASSOC_FILE ); if( associationFile == null ) throw new IOException( "Target " + targetId + " does not exist." ); Properties props = Utils.readPropertiesFileQuietly( associationFile, this.logger ); props.setProperty( key.toString(), "" ); writeProperties( props, associationFile ); this.instanceToCachedId.put( key, targetId ); } } private void restoreAssociationsCache() { File dir = new File( this.configurationMngr.getWorkingDirectory(), ConfigurationUtils.TARGETS ); for( File f : Utils.listDirectories( dir )) { // Store the ID this.targetIds.put( f.getName(), Boolean.TRUE ); // Cache associations for quicker access File associationFile = new File( f, TARGETS_ASSOC_FILE ); Properties props = Utils.readPropertiesFileQuietly( associationFile, this.logger ); for( Map.Entry<Object,Object> entry : props.entrySet()) { InstanceContext key = InstanceContext.parse( entry.getKey().toString()); this.instanceToCachedId.put( key, f.getName()); } } } private void saveUsage( InstanceContext mappingKey, String targetId, boolean add ) throws IOException { // Usage means the target has been used to create a real machine. File usageFile = findTargetFile( targetId, TARGETS_USAGE_FILE ); Properties props = Utils.readPropertiesFileQuietly( usageFile, this.logger ); String key = mappingKey.toString(); if( add ) props.setProperty( key, targetId ); else props.remove( key ); writeProperties( props, usageFile ); } private boolean isTargetUsed( String targetId ) { File usageFile = findTargetFile( targetId, TARGETS_USAGE_FILE ); Properties props = Utils.readPropertiesFileQuietly( usageFile, this.logger ); boolean found = false; for( Iterator<Object> it = props.values().iterator(); it.hasNext() && ! found; ) { found = it.next().equals( targetId ); } return found; } private List<String> applicationsThatUse( String targetId ) { File usageFile = findTargetFile( targetId, TARGETS_USAGE_FILE ); Properties props = Utils.readPropertiesFileQuietly( usageFile, this.logger ); List<String> result = new ArrayList<> (); for( Object o : props.keySet()) { InstanceContext key = InstanceContext.parse((String) o); result.add( key.getName()); } return result; } private void saveHint( String targetId, AbstractApplication app, boolean add ) throws IOException { // A hint is just a preference (some kind of scope for a target). // If a hint is not respected, no exception will be thrown. File hintsFile = findTargetFile( targetId, TARGETS_HINTS_FILE ); Properties props = Utils.readPropertiesFileQuietly( hintsFile, this.logger ); String key = new InstanceContext( app ).toString(); if( add ) { props.setProperty( key, "" ); } else { props.remove( key ); } writeProperties( props, hintsFile ); } private void writeProperties( Properties props, File file ) throws IOException { if( props.isEmpty()) Utils.deleteFilesRecursivelyAndQuietly( file ); else Utils.writePropertiesFile( props, file ); } private File findTargetDirectory( String targetId ) { File dir = new File( this.configurationMngr.getWorkingDirectory(), ConfigurationUtils.TARGETS ); return new File( dir, targetId ); } private File findTargetFile( String targetId, String fileName ) { File dir = null; File result = null; if( targetId != null && (dir = findTargetDirectory( targetId )).exists()) result = new File( dir, fileName ); return result; } }