/* * Copyright 2015 Red Hat, Inc. and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * * 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 org.guvnor.structure.backend.config; import java.util.ArrayList; import java.util.Date; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import javax.enterprise.context.ApplicationScoped; import javax.enterprise.event.Event; import javax.inject.Inject; import javax.inject.Named; import javax.naming.InitialContext; import org.guvnor.structure.backend.config.watch.AsyncConfigWatchService; import org.guvnor.structure.backend.config.watch.AsyncWatchServiceCallback; import org.guvnor.structure.backend.config.watch.ConfigServiceWatchServiceExecutor; import org.guvnor.structure.backend.config.watch.ConfigServiceWatchServiceExecutorImpl; import org.guvnor.structure.config.SystemRepositoryChangedEvent; import org.guvnor.structure.server.config.ConfigGroup; import org.guvnor.structure.server.config.ConfigType; import org.guvnor.structure.server.config.ConfigurationService; import org.jboss.errai.security.shared.api.identity.User; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.uberfire.commons.async.DescriptiveRunnable; import org.uberfire.io.IOService; import org.uberfire.java.nio.IOException; import org.uberfire.java.nio.base.WatchContext; import org.uberfire.java.nio.base.options.CommentedOption; import org.uberfire.java.nio.file.DirectoryStream; import org.uberfire.java.nio.file.FileSystem; import org.uberfire.java.nio.file.Files; import org.uberfire.java.nio.file.Path; import org.uberfire.java.nio.file.StandardWatchEventKind; import org.uberfire.java.nio.file.WatchEvent; import org.uberfire.java.nio.file.WatchKey; import org.uberfire.java.nio.file.WatchService; import static org.uberfire.backend.server.util.Paths.*; @ApplicationScoped public class ConfigurationServiceImpl implements ConfigurationService, AsyncWatchServiceCallback { private static final Logger logger = LoggerFactory.getLogger( ConfigurationServiceImpl.class ); private static final String MONITOR_DISABLED = "org.uberfire.sys.repo.monitor.disabled"; // private static final String MONITOR_CHECK_INTERVAL = "org.uberfire.sys.repo.monitor.interval"; // mainly for windows as *NIX is based on POSIX but escape always to keep it consistent private static final String INVALID_FILENAME_CHARS = "[\\,/,:,*,?,\",<,>,|]"; private org.guvnor.structure.repositories.Repository systemRepository; private ConfigGroupMarshaller marshaller; private User identity; //Cache of ConfigGroups to avoid reloading them from file private final Map<ConfigType, List<ConfigGroup>> configuration = new ConcurrentHashMap<ConfigType, List<ConfigGroup>>(); private AtomicLong localLastModifiedValue = new AtomicLong( -1 ); private IOService ioService; // monitor capabilities private Event<SystemRepositoryChangedEvent> repoChangedEvent; private Event<SystemRepositoryChangedEvent> orgUnitChangedEvent; private Event<SystemRepositoryChangedEvent> changedEvent; private final ExecutorService executorService = Executors.newSingleThreadExecutor(); private final Set<Future<?>> jobs = new CopyOnWriteArraySet<Future<?>>(); private ConfigServiceWatchServiceExecutor executor = null; private CheckConfigurationUpdates configUpdates = null; private WatchService watchService = null; private FileSystem fs; public ConfigurationServiceImpl() { } @Inject public ConfigurationServiceImpl( final @Named("system") org.guvnor.structure.repositories.Repository systemRepository, final ConfigGroupMarshaller marshaller, final User identity, final @Named("configIO") IOService ioService, final @Repository Event<SystemRepositoryChangedEvent> repoChangedEvent, final @OrgUnit Event<SystemRepositoryChangedEvent> orgUnitChangedEvent, final Event<SystemRepositoryChangedEvent> changedEvent, final @Named("systemFS") FileSystem fs ) { this.systemRepository = systemRepository; this.marshaller = marshaller; this.identity = identity; this.ioService = ioService; this.repoChangedEvent = repoChangedEvent; this.orgUnitChangedEvent = orgUnitChangedEvent; this.changedEvent = changedEvent; this.fs = fs; } @PostConstruct public void setup() { Path defaultRoot = null; for ( final Path path : fs.getRootDirectories() ) { if ( path.toUri().toString().contains( "/master@" ) ) { defaultRoot = path; break; } } if ( defaultRoot == null ) { throw new RuntimeException( "Could not resolve 'systemFS' main root directory." ); } systemRepository.setRoot( convert( defaultRoot ) ); // enable monitor by default if ( System.getProperty( MONITOR_DISABLED ) == null ) { watchService = fs.newWatchService(); configUpdates = new CheckConfigurationUpdates( watchService ); final ConfigServiceWatchServiceExecutor configServiceWatchServiceExecutor = getWatchServiceExecutor(); jobs.add( executorService.submit( new DescriptiveRunnable() { @Override public String getDescription() { return configUpdates.getDescription(); } @Override public void run() { configUpdates.execute( configServiceWatchServiceExecutor ); } } ) ); } } @PreDestroy public void shutdown() { if ( configUpdates != null ) { configUpdates.deactivate(); } if ( watchService != null ) { watchService.close(); } for ( Future<?> job : jobs ) { if ( !job.isCancelled() && !job.isDone() ) { job.cancel( true ); } } executorService.shutdown(); // Disable new tasks from being submitted try { // Wait a while for existing tasks to terminate if ( !executorService.awaitTermination( 60, TimeUnit.SECONDS ) ) { executorService.shutdownNow(); // Cancel currently executing tasks // Wait a while for tasks to respond to being cancelled if ( !executorService.awaitTermination( 60, TimeUnit.SECONDS ) ) { System.err.println( "Pool did not terminate" ); } } } catch ( InterruptedException ie ) { // (Re-)Cancel if current thread also interrupted executorService.shutdownNow(); // Preserve interrupt status Thread.currentThread().interrupt(); } } @Override public void startBatch() { ioService.startBatch( ioService.get( systemRepository.getUri() ).getFileSystem() ); } @Override public void endBatch() { ioService.endBatch(); } @Override public List<ConfigGroup> getConfiguration( final ConfigType type ) { if ( configuration.containsKey( type ) ) { return configuration.get( type ); } final List<ConfigGroup> configGroups = new ArrayList<ConfigGroup>(); final DirectoryStream<Path> foundConfigs = ioService.newDirectoryStream( ioService.get( systemRepository.getUri() ), new DirectoryStream.Filter<Path>() { @Override public boolean accept( final Path entry ) throws IOException { if ( !Files.isDirectory( entry ) && !entry.getFileName().toString().startsWith( "." ) && entry.getFileName().toString().endsWith( type.getExt() ) ) { return true; } return false; } } ); //Only load and cache if a file was found! final Iterator<Path> it = foundConfigs.iterator(); if ( it.hasNext() ) { while ( it.hasNext() ) { final String content = ioService.readAllString( it.next() ); final ConfigGroup configGroup = marshaller.unmarshall( content ); configGroups.add( configGroup ); } configuration.put( type, configGroups ); } return configGroups; } @Override public boolean addConfiguration( final ConfigGroup configGroup ) { String filename = configGroup.getName().replaceAll( INVALID_FILENAME_CHARS, "_" ); final Path filePath = ioService.get( systemRepository.getUri() ).resolve( filename + configGroup.getType().getExt() ); // avoid duplicated writes to not cause cyclic cluster sync if ( ioService.exists( filePath ) ) { return true; } final CommentedOption commentedOption = new CommentedOption( getIdentityName(), "Created config " + filePath.getFileName() ); try { ioService.startBatch( filePath.getFileSystem() ); ioService.write( filePath, marshaller.marshall( configGroup ), commentedOption ); updateLastModified(); } catch ( Exception ex ) { throw new RuntimeException( ex ); } finally { ioService.endBatch(); } //Invalidate cache if a new item has been created; otherwise cached value is stale configuration.remove( configGroup.getType() ); return true; } @Override public boolean updateConfiguration( ConfigGroup configGroup ) { String filename = configGroup.getName().replaceAll( INVALID_FILENAME_CHARS, "_" ); final Path filePath = ioService.get( systemRepository.getUri() ).resolve( filename + configGroup.getType().getExt() ); final CommentedOption commentedOption = new CommentedOption( getIdentityName(), "Updated config " + filePath.getFileName() ); try { ioService.startBatch( filePath.getFileSystem() ); ioService.write( filePath, marshaller.marshall( configGroup ), commentedOption ); updateLastModified(); } catch ( Exception ex ) { throw new RuntimeException( ex ); } finally { ioService.endBatch(); } //Invalidate cache if a new item has been created; otherwise cached value is stale configuration.remove( configGroup.getType() ); return true; } @Override public boolean removeConfiguration( final ConfigGroup configGroup ) { //Invalidate cache if an item has been removed; otherwise cached value is stale configuration.remove( configGroup.getType() ); String filename = configGroup.getName().replaceAll( INVALID_FILENAME_CHARS, "_" ); final Path filePath = ioService.get( systemRepository.getUri() ).resolve( filename + configGroup.getType().getExt() ); // avoid duplicated writes to not cause cyclic cluster sync if ( !ioService.exists( filePath ) ) { return true; } boolean result; try { ioService.startBatch( filePath.getFileSystem() ); result = ioService.deleteIfExists( filePath ); if ( result ) { updateLastModified(); } } catch ( Exception ex ) { throw new RuntimeException( ex ); } finally { ioService.endBatch(); } return result; } protected String getIdentityName() { try { return identity.getIdentifier(); } catch ( Exception e ) { return "unknown"; } } protected long getLastModified() { final Path lastModifiedPath = ioService.get( systemRepository.getUri() ).resolve( LAST_MODIFIED_MARKER_FILE ); return ioService.getLastModifiedTime( lastModifiedPath ).toMillis(); } protected void updateLastModified() { final Path lastModifiedPath = ioService.get( systemRepository.getUri() ).resolve( LAST_MODIFIED_MARKER_FILE ); final CommentedOption commentedOption = new CommentedOption( "system", "system repo updated" ); ioService.write( lastModifiedPath, new Date().toString().getBytes(), commentedOption ); // update the last value to avoid to be retriggered byt the monitor localLastModifiedValue.set( getLastModified() ); } @Override public void callback( long value ) { localLastModifiedValue.set( value ); // invalidate cached values as system repo has changed configuration.clear(); } private class CheckConfigurationUpdates implements AsyncConfigWatchService { private final WatchService ws; private boolean active = true; public CheckConfigurationUpdates( final WatchService watchService ) { this.ws = watchService; } public void deactivate() { this.active = false; } @Override public void execute( final ConfigServiceWatchServiceExecutor wsExecutor ) { while ( active ) { try { final WatchKey wk; try { wk = ws.take(); } catch ( final Exception ex ) { break; } final List<WatchEvent<?>> events = wk.pollEvents(); boolean markerFileModified = false; for ( final WatchEvent<?> event : events ) { final WatchContext context = (WatchContext) event.context(); if ( event.kind().equals( StandardWatchEventKind.ENTRY_MODIFY ) ) { if ( context.getOldPath().getFileName().toString().equals( LAST_MODIFIED_MARKER_FILE ) ) { markerFileModified = true; break; } } else if ( event.kind().equals( StandardWatchEventKind.ENTRY_CREATE ) ) { if ( context.getPath().getFileName().toString().equals( LAST_MODIFIED_MARKER_FILE ) ) { markerFileModified = true; break; } } else if ( event.kind().equals( StandardWatchEventKind.ENTRY_RENAME ) ) { if ( context.getOldPath().getFileName().toString().equals( LAST_MODIFIED_MARKER_FILE ) ) { markerFileModified = true; break; } } else if ( event.kind().equals( StandardWatchEventKind.ENTRY_DELETE ) ) { if ( context.getOldPath().getFileName().toString().equals( LAST_MODIFIED_MARKER_FILE ) ) { markerFileModified = true; break; } } } if ( markerFileModified ) { wsExecutor.execute( wk, localLastModifiedValue.get(), ConfigurationServiceImpl.this ); } boolean valid = wk.reset(); if ( !valid ) { break; } } catch ( final Exception ignored ) { } } } @Override public String getDescription() { return "Config File Watch Service"; } } protected ConfigServiceWatchServiceExecutor getWatchServiceExecutor() { if ( executor == null ) { ConfigServiceWatchServiceExecutor _executor = null; try { _executor = InitialContext.doLookup( "java:module/ConfigServiceWatchServiceExecutorImpl" ); } catch ( final Exception ignored ) { } if ( _executor == null ) { _executor = new ConfigServiceWatchServiceExecutorImpl(); ( (ConfigServiceWatchServiceExecutorImpl) _executor ).setConfig( systemRepository, ioService, repoChangedEvent, orgUnitChangedEvent, changedEvent ); } executor = _executor; } return executor; } }