/** * 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.configuration.application; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.apache.shiro.subject.Subject; import org.codehaus.plexus.component.annotations.Component; import org.codehaus.plexus.component.annotations.Requirement; import org.codehaus.plexus.logging.Logger; import org.codehaus.plexus.util.StringUtils; import org.sonatype.configuration.ConfigurationException; import org.sonatype.configuration.validation.InvalidConfigurationException; import org.sonatype.configuration.validation.ValidationRequest; import org.sonatype.configuration.validation.ValidationResponse; import org.sonatype.nexus.NexusStreamResponse; import org.sonatype.nexus.configuration.ConfigurationChangeEvent; import org.sonatype.nexus.configuration.ConfigurationCommitEvent; import org.sonatype.nexus.configuration.ConfigurationLoadEvent; import org.sonatype.nexus.configuration.ConfigurationPrepareForLoadEvent; import org.sonatype.nexus.configuration.ConfigurationPrepareForSaveEvent; import org.sonatype.nexus.configuration.ConfigurationRollbackEvent; import org.sonatype.nexus.configuration.ConfigurationSaveEvent; import org.sonatype.nexus.configuration.application.runtime.ApplicationRuntimeConfigurationBuilder; import org.sonatype.nexus.configuration.model.CPathMappingItem; import org.sonatype.nexus.configuration.model.CRemoteNexusInstance; import org.sonatype.nexus.configuration.model.CRepository; import org.sonatype.nexus.configuration.model.Configuration; import org.sonatype.nexus.configuration.source.ApplicationConfigurationSource; import org.sonatype.nexus.configuration.validator.ApplicationConfigurationValidator; import org.sonatype.nexus.configuration.validator.ApplicationValidationContext; import org.sonatype.nexus.plugins.RepositoryType; import org.sonatype.nexus.proxy.NoSuchRepositoryException; import org.sonatype.nexus.proxy.events.VetoFormatter; import org.sonatype.nexus.proxy.events.VetoFormatterRequest; import org.sonatype.nexus.proxy.registry.RepositoryRegistry; import org.sonatype.nexus.proxy.registry.RepositoryTypeDescriptor; import org.sonatype.nexus.proxy.registry.RepositoryTypeRegistry; import org.sonatype.nexus.proxy.repository.GroupRepository; import org.sonatype.nexus.proxy.repository.LocalStatus; import org.sonatype.nexus.proxy.repository.Repository; import org.sonatype.nexus.proxy.repository.ShadowRepository; import org.sonatype.nexus.proxy.storage.local.DefaultLocalStorageContext; import org.sonatype.nexus.proxy.storage.local.LocalStorageContext; import org.sonatype.nexus.proxy.storage.remote.DefaultRemoteStorageContext; import org.sonatype.nexus.proxy.storage.remote.RemoteStorageContext; import org.sonatype.nexus.tasks.descriptors.ScheduledTaskDescriptor; import org.sonatype.plexus.appevents.ApplicationEventMulticaster; import org.sonatype.security.SecuritySystem; /** * The class DefaultNexusConfiguration is responsible for config management. It actually keeps in sync Nexus internal * state with p ersisted user configuration. All changes incoming thru its iface is reflect/maintained in Nexus current * state and Nexus user config. * * @author cstamas */ @Component( role = NexusConfiguration.class ) public class DefaultNexusConfiguration implements NexusConfiguration { @Requirement private Logger logger; @Requirement private ApplicationEventMulticaster applicationEventMulticaster; @Requirement( hint = "file" ) private ApplicationConfigurationSource configurationSource; /** The global local storage context. */ private LocalStorageContext globalLocalStorageContext; /** The global remote storage context. */ private RemoteStorageContext globalRemoteStorageContext; @Requirement private GlobalRemoteConnectionSettings globalRemoteConnectionSettings; @Requirement private GlobalHttpProxySettings globalHttpProxySettings; /** * The config validator. */ @Requirement private ApplicationConfigurationValidator configurationValidator; /** * The runtime configuration builder. */ @Requirement private ApplicationRuntimeConfigurationBuilder runtimeConfigurationBuilder; @Requirement private RepositoryTypeRegistry repositoryTypeRegistry; @Requirement private RepositoryRegistry repositoryRegistry; @Requirement( role = ScheduledTaskDescriptor.class ) private List<ScheduledTaskDescriptor> scheduledTaskDescriptors; @Requirement private SecuritySystem securitySystem; @org.codehaus.plexus.component.annotations.Configuration( value = "${nexus-work}" ) private File workingDirectory; @Requirement private VetoFormatter vetoFormatter; /** The config dir */ private File configurationDirectory; /** The temp dir */ private File temporaryDirectory; /** Names of the conf files */ private Map<String, String> configurationFiles; /** The default maxInstance count */ private int defaultRepositoryMaxInstanceCountLimit = Integer.MAX_VALUE; /** The map with per-repotype limitations */ private Map<RepositoryTypeDescriptor, Integer> repositoryMaxInstanceCountLimits; // == protected Logger getLogger() { return logger; } // == public void loadConfiguration() throws ConfigurationException, IOException { loadConfiguration( false ); } public void loadConfiguration( boolean force ) throws ConfigurationException, IOException { if ( force || configurationSource.getConfiguration() == null ) { getLogger().info( "Loading Nexus Configuration..." ); configurationSource.loadConfiguration(); configurationDirectory = null; temporaryDirectory = null; globalLocalStorageContext = new DefaultLocalStorageContext( null ); // create global remote ctx // this one has no parent globalRemoteStorageContext = new DefaultRemoteStorageContext( null ); globalRemoteConnectionSettings.configure( this ); globalRemoteStorageContext.setRemoteConnectionSettings( globalRemoteConnectionSettings ); globalHttpProxySettings.configure( this ); globalRemoteStorageContext.setRemoteProxySettings( globalHttpProxySettings ); ConfigurationPrepareForLoadEvent loadEvent = new ConfigurationPrepareForLoadEvent( this ); applicationEventMulticaster.notifyEventListeners( loadEvent ); if ( loadEvent.isVetoed() ) { getLogger().info( vetoFormatter.format( new VetoFormatterRequest( loadEvent, getLogger().isDebugEnabled() ) ) ); throw new ConfigurationException( "The Nexus configuration is invalid!" ); } applyConfiguration(); // we succesfully loaded config applicationEventMulticaster.notifyEventListeners( new ConfigurationLoadEvent( this ) ); } } public boolean applyConfiguration() { getLogger().info( "Applying Nexus Configuration..." ); ConfigurationPrepareForSaveEvent prepare = new ConfigurationPrepareForSaveEvent( this ); applicationEventMulticaster.notifyEventListeners( prepare ); if ( !prepare.isVetoed() ) { configurationDirectory = null; temporaryDirectory = null; applicationEventMulticaster.notifyEventListeners( new ConfigurationCommitEvent( this ) ); String userId = null; Subject subject = securitySystem.getSubject(); if ( subject != null && subject.getPrincipal() != null ) { userId = subject.getPrincipal().toString(); } applicationEventMulticaster.notifyEventListeners( new ConfigurationChangeEvent( this, prepare.getChanges(), userId ) ); return true; } else { getLogger().info( vetoFormatter.format( new VetoFormatterRequest( prepare, getLogger().isDebugEnabled() ) ) ); applicationEventMulticaster.notifyEventListeners( new ConfigurationRollbackEvent( this ) ); return false; } } public synchronized void saveConfiguration() throws IOException { if ( applyConfiguration() ) { // TODO: when NEXUS-2215 is fixed, this should be remove/moved/cleaned // START <<< // validate before we do anything ValidationRequest request = new ValidationRequest( configurationSource.getConfiguration() ); ValidationResponse response = configurationValidator.validateModel( request ); if ( !response.isValid() ) { this.getLogger().error( "Saving nexus configuration caused unexpected error:\n" + response.toString() ); throw new IOException( "Saving nexus configuration caused unexpected error:\n" + response.toString() ); } // END <<< configurationSource.storeConfiguration(); // we succesfully saved config applicationEventMulticaster.notifyEventListeners( new ConfigurationSaveEvent( this ) ); } } @Deprecated // see above protected void applyAndSaveConfiguration() throws IOException { saveConfiguration(); } @Deprecated public Configuration getConfigurationModel() { return configurationSource.getConfiguration(); } public ApplicationConfigurationSource getConfigurationSource() { return configurationSource; } public boolean isInstanceUpgraded() { // TODO: this is not quite true: we might keep model ver but upgrade JARs of Nexus only in a release // we should store the nexus version somewhere in working storage and trigger some household stuff // if version changes. return configurationSource.isConfigurationUpgraded(); } public boolean isConfigurationUpgraded() { return configurationSource.isConfigurationUpgraded(); } public boolean isConfigurationDefaulted() { return configurationSource.isConfigurationDefaulted(); } public LocalStorageContext getGlobalLocalStorageContext() { return globalLocalStorageContext; } public RemoteStorageContext getGlobalRemoteStorageContext() { return globalRemoteStorageContext; } public File getWorkingDirectory() { // Create the dir if doesn't exist, throw runtime exception on failure // bad bad bad if ( !workingDirectory.exists() && !workingDirectory.mkdirs() ) { String message = "\r\n******************************************************************************\r\n" + "* Could not create work directory [ " + workingDirectory.toString() + "]!!!! *\r\n" + "* Nexus cannot start properly until the process has read+write permissions to this folder *\r\n" + "******************************************************************************"; getLogger().fatalError( message ); } return workingDirectory; } public File getWorkingDirectory( String key ) { File keyedDirectory = new File( getWorkingDirectory(), key ); if ( !keyedDirectory.isDirectory() && !keyedDirectory.mkdirs() ) { String message = "\r\n******************************************************************************\r\n" + "* Could not create work directory [ " + keyedDirectory.toString() + "]!!!! *\r\n" + "* Nexus cannot start properly until the process has read+write permissions to this folder *\r\n" + "******************************************************************************"; getLogger().fatalError( message ); } return keyedDirectory; } public File getTemporaryDirectory() { if ( temporaryDirectory == null ) { temporaryDirectory = new File( System.getProperty( "java.io.tmpdir" ) ); if ( !temporaryDirectory.exists() ) { temporaryDirectory.mkdirs(); } } return temporaryDirectory; } public File getConfigurationDirectory() { if ( configurationDirectory == null ) { configurationDirectory = new File( getWorkingDirectory(), "conf" ); if ( !configurationDirectory.exists() ) { configurationDirectory.mkdirs(); } } return configurationDirectory; } @Deprecated public Repository createRepositoryFromModel( CRepository repository ) throws ConfigurationException { return runtimeConfigurationBuilder.createRepositoryFromModel( getConfigurationModel(), repository ); } public List<ScheduledTaskDescriptor> listScheduledTaskDescriptors() { return Collections.unmodifiableList( scheduledTaskDescriptors ); } public ScheduledTaskDescriptor getScheduledTaskDescriptor( String id ) { for ( ScheduledTaskDescriptor descriptor : scheduledTaskDescriptors ) { if ( descriptor.getId().equals( id ) ) { return descriptor; } } return null; } // ------------------------------------------------------------------ // Security public boolean isSecurityEnabled() { return getSecuritySystem() != null && getSecuritySystem().isSecurityEnabled(); } public void setSecurityEnabled( boolean enabled ) throws IOException { getSecuritySystem().setSecurityEnabled( enabled ); } public void setRealms( List<String> realms ) throws org.sonatype.configuration.validation.InvalidConfigurationException { getSecuritySystem().setRealms( realms ); } public boolean isAnonymousAccessEnabled() { return getSecuritySystem() != null && getSecuritySystem().isAnonymousAccessEnabled(); } public void setAnonymousAccessEnabled( boolean enabled ) { getSecuritySystem().setAnonymousAccessEnabled( enabled ); } public String getAnonymousUsername() { return getSecuritySystem().getAnonymousUsername(); } public void setAnonymousUsername( String val ) throws org.sonatype.configuration.validation.InvalidConfigurationException { getSecuritySystem().setAnonymousUsername( val ); } public String getAnonymousPassword() { return getSecuritySystem().getAnonymousPassword(); } public void setAnonymousPassword( String val ) throws org.sonatype.configuration.validation.InvalidConfigurationException { getSecuritySystem().setAnonymousPassword( val ); } public List<String> getRealms() { return getSecuritySystem().getRealms(); } // ------------------------------------------------------------------ // Booting public void createInternals() throws ConfigurationException { createRepositories(); } public void dropInternals() { dropRepositories(); } protected void createRepositories() throws ConfigurationException { List<CRepository> reposes = getConfigurationModel().getRepositories(); for ( CRepository repo : reposes ) { if ( !repo.getProviderRole().equals( GroupRepository.class.getName() ) ) { instantiateRepository( getConfigurationModel(), repo ); } } for ( CRepository repo : reposes ) { if ( repo.getProviderRole().equals( GroupRepository.class.getName() ) ) { instantiateRepository( getConfigurationModel(), repo ); } } } protected void dropRepositories() { for ( Repository repository : repositoryRegistry.getRepositories() ) { try { repositoryRegistry.removeRepositorySilently( repository.getId() ); } catch ( NoSuchRepositoryException e ) { // will not happen } } } protected Repository instantiateRepository( Configuration configuration, CRepository repositoryModel ) throws ConfigurationException { checkRepositoryMaxInstanceCountForCreation( repositoryModel ); // create it, will do runtime validation Repository repository = runtimeConfigurationBuilder.createRepositoryFromModel( configuration, repositoryModel ); // register with repoRegistry repositoryRegistry.addRepository( repository ); // give it back return repository; } // ------------------------------------------------------------------ // CRUD-like ops on config sections // Globals are mandatory: RU // CRepository and CreposioryShadow helper private ApplicationValidationContext getRepositoryValidationContext() { ApplicationValidationContext result = new ApplicationValidationContext(); fillValidationContextRepositoryIds( result ); return result; } private void fillValidationContextRepositoryIds( ApplicationValidationContext context ) { context.addExistingRepositoryIds(); List<CRepository> repositories = getConfigurationModel().getRepositories(); if ( repositories != null ) { for ( CRepository repo : repositories ) { context.getExistingRepositoryIds().add( repo.getId() ); } } } // ---------------------------------------------------------------------------------------------------------- // Repositories // ---------------------------------------------------------------------------------------------------------- protected Map<RepositoryTypeDescriptor, Integer> getRepositoryMaxInstanceCountLimits() { if ( repositoryMaxInstanceCountLimits == null ) { repositoryMaxInstanceCountLimits = new ConcurrentHashMap<RepositoryTypeDescriptor, Integer>(); } return repositoryMaxInstanceCountLimits; } public void setDefaultRepositoryMaxInstanceCount( int count ) { if ( count < 0 ) { getLogger().info( "Default repository maximal instance limit set to UNLIMITED." ); this.defaultRepositoryMaxInstanceCountLimit = Integer.MAX_VALUE; } else { getLogger().info( "Default repository maximal instance limit set to " + count + "." ); this.defaultRepositoryMaxInstanceCountLimit = count; } } public void setRepositoryMaxInstanceCount( RepositoryTypeDescriptor rtd, int count ) { if ( count < 0 ) { getLogger().info( "Repository type " + rtd.toString() + " maximal instance limit set to UNLIMITED." ); getRepositoryMaxInstanceCountLimits().remove( rtd ); } else { getLogger().info( "Repository type " + rtd.toString() + " maximal instance limit set to " + count + "." ); getRepositoryMaxInstanceCountLimits().put( rtd, count ); } } public int getRepositoryMaxInstanceCount( RepositoryTypeDescriptor rtd ) { Integer limit = getRepositoryMaxInstanceCountLimits().get( rtd ); if ( null != limit ) { return limit; } else { return defaultRepositoryMaxInstanceCountLimit; } } protected void checkRepositoryMaxInstanceCountForCreation( CRepository repositoryModel ) throws ConfigurationException { RepositoryTypeDescriptor rtd = repositoryTypeRegistry.getRepositoryTypeDescriptor( repositoryModel.getProviderRole(), repositoryModel.getProviderHint() ); int maxCount; if ( null == rtd ) { rtd = new RepositoryTypeDescriptor( repositoryModel.getProviderRole(), repositoryModel.getProviderHint(), "repositories", RepositoryType.UNLIMITED_INSTANCES ); getLogger().warn( "Your Nexus instance contains a plugin, that contributes repository type " + rtd.toString() + " to Nexus, " + "but fails to properly register it (and it is not using Nexus Plugin API annotations either). " + "Please contact plugin developers to update their plugin! Registering the type on-the-fly." ); repositoryTypeRegistry.registerRepositoryTypeDescriptors( rtd ); } if ( rtd.getRepositoryMaxInstanceCount() != RepositoryType.UNLIMITED_INSTANCES ) { maxCount = rtd.getRepositoryMaxInstanceCount(); } else { maxCount = getRepositoryMaxInstanceCount( rtd ); } if ( rtd.getInstanceCount() >= maxCount ) { String msg = "Repository \"" + repositoryModel.getName() + "\" (id=" + repositoryModel.getId() + ") cannot be created. It's repository type " + rtd.toString() + " is limited to " + maxCount + " instances, and it already has " + String.valueOf( rtd.getInstanceCount() ) + " of them."; getLogger().warn( msg ); throw new ConfigurationException( msg ); } } // CRepository: CRUD protected void validateRepository( CRepository settings, boolean create ) throws ConfigurationException { ApplicationValidationContext ctx = getRepositoryValidationContext(); if ( !create && !StringUtils.isEmpty( settings.getId() ) ) { // remove "itself" from the list to avoid hitting "duplicate repo" problem ctx.getExistingRepositoryIds().remove( settings.getId() ); } ValidationResponse vr = configurationValidator.validateRepository( ctx, settings ); if ( !vr.isValid() ) { throw new InvalidConfigurationException( vr ); } } public Repository createRepository( CRepository settings ) throws ConfigurationException, IOException { validateRepository( settings, true ); // create it, will do runtime validation Repository repository = instantiateRepository( getConfigurationModel(), settings ); // now add it to config, since it is validated and succesfully created getConfigurationModel().addRepository( settings ); // save saveConfiguration(); return repository; } public void deleteRepository( String id ) throws NoSuchRepositoryException, IOException, ConfigurationException { Repository repository = repositoryRegistry.getRepository( id ); // put out of service so wont be accessed any longer repository.setLocalStatus( LocalStatus.OUT_OF_SERVICE ); // disable indexing for same purpose repository.setIndexable( false ); repository.setSearchable( false ); // remove dependants too // ======= // shadows // (fail if any repo references the currently processing one) List<ShadowRepository> shadows = repositoryRegistry.getRepositoriesWithFacet( ShadowRepository.class ); for ( Iterator<ShadowRepository> i = shadows.iterator(); i.hasNext(); ) { ShadowRepository shadow = i.next(); if ( repository.getId().equals( shadow.getMasterRepository().getId() ) ) { throw new ConfigurationException( "The repository with ID " + id + " is not deletable, it has dependant repositories!" ); } } // ====== // groups // (correction in config only, registry DOES handle it) // since NEXUS-1770, groups are "self maintaining" // =========== // pahMappings // (correction, since registry is completely unaware of this component) List<CPathMappingItem> pathMappings = getConfigurationModel().getRepositoryGrouping().getPathMappings(); for ( Iterator<CPathMappingItem> i = pathMappings.iterator(); i.hasNext(); ) { CPathMappingItem item = i.next(); item.removeRepository( id ); } // =========== // and finally // this cleans it properly from the registry (from reposes and repo groups) repositoryRegistry.removeRepository( id ); List<CRepository> reposes = getConfigurationModel().getRepositories(); for ( Iterator<CRepository> i = reposes.iterator(); i.hasNext(); ) { CRepository repo = i.next(); if ( repo.getId().equals( id ) ) { i.remove(); applyAndSaveConfiguration(); return; } } throw new NoSuchRepositoryException( id ); } // === public Collection<CRemoteNexusInstance> listRemoteNexusInstances() { List<CRemoteNexusInstance> result = null; if ( getConfigurationModel().getRemoteNexusInstances() != null ) { result = Collections.unmodifiableList( getConfigurationModel().getRemoteNexusInstances() ); } return result; } public CRemoteNexusInstance readRemoteNexusInstance( String alias ) throws IOException { List<CRemoteNexusInstance> knownInstances = getConfigurationModel().getRemoteNexusInstances(); for ( Iterator<CRemoteNexusInstance> i = knownInstances.iterator(); i.hasNext(); ) { CRemoteNexusInstance nexusInstance = i.next(); if ( nexusInstance.getAlias().equals( alias ) ) { return nexusInstance; } } return null; } public void createRemoteNexusInstance( CRemoteNexusInstance settings ) throws IOException { getConfigurationModel().addRemoteNexusInstance( settings ); applyAndSaveConfiguration(); } public void deleteRemoteNexusInstance( String alias ) throws IOException { List<CRemoteNexusInstance> knownInstances = getConfigurationModel().getRemoteNexusInstances(); for ( Iterator<CRemoteNexusInstance> i = knownInstances.iterator(); i.hasNext(); ) { CRemoteNexusInstance nexusInstance = i.next(); if ( nexusInstance.getAlias().equals( alias ) ) { i.remove(); } } applyAndSaveConfiguration(); } public Map<String, String> getConfigurationFiles() { if ( configurationFiles == null ) { configurationFiles = new HashMap<String, String>(); File configDirectory = getConfigurationDirectory(); int key = 1; // Tamas: // configDirectory.listFiles() may be returning null... in this case, it is 99.9% not true (otherwise nexus // would not start at all), but in general, be more explicit about checks. if ( configDirectory.isDirectory() && configDirectory.listFiles() != null ) { for ( File file : configDirectory.listFiles() ) { if ( file.exists() && file.isFile() ) { configurationFiles.put( Integer.toString( key ), file.getName() ); key++; } } } } return configurationFiles; } public NexusStreamResponse getConfigurationAsStreamByKey( String key ) throws IOException { String fileName = getConfigurationFiles().get( key ); if ( fileName != null ) { File configFile = new File( getConfigurationDirectory(), fileName ); if ( configFile.canRead() && configFile.isFile() ) { NexusStreamResponse response = new NexusStreamResponse(); response.setName( fileName ); if ( fileName.endsWith( ".xml" ) ) { response.setMimeType( "text/xml" ); } else { response.setMimeType( "text/plain" ); } response.setSize( configFile.length() ); response.setFromByte( 0 ); response.setBytesCount( configFile.length() ); response.setInputStream( new FileInputStream( configFile ) ); return response; } else { return null; } } else { return null; } } protected SecuritySystem getSecuritySystem() { return this.securitySystem; } }