| Author | djarvis <email> |
|---|---|
| Date | 2016-12-18 02:03:23 GMT-0800 |
| Commit | 0f9e56c549143ed729700c3eb85033befe17c3fb |
| Parent | 8d2ab30 |
| public final class Main extends Application { | ||
| - private static Application app; | ||
| + private final Options options = Services.load( Options.class ); | ||
| + private final Snitch snitch = Services.load( Snitch.class ); | ||
| + private Thread snitchThread; | ||
| + private static Application app; | ||
| private final MainWindow mainWindow = new MainWindow(); | ||
| - private final Options options = Services.load( Options.class ); | ||
| public static void main( final String[] args ) { | ||
| initStage( stage ); | ||
| initAlertService(); | ||
| + initWatchDog(); | ||
| stage.show(); | ||
| } | ||
| - /** | ||
| - * Stops the snitch service, if its running. | ||
| - */ | ||
| - @Override | ||
| - public void stop() { | ||
| - Services.load( Snitch.class ).stop(); | ||
| + public static void showDocument( final String uri ) { | ||
| + getApplication().getHostServices().showDocument( uri ); | ||
| } | ||
| private void initApplication() { | ||
| app = this; | ||
| - } | ||
| - | ||
| - private Options getOptions() { | ||
| - return this.options; | ||
| - } | ||
| - | ||
| - private String getApplicationTitle() { | ||
| - return Messages.get( "Main.title" ); | ||
| } | ||
| final AlertService service = Services.load( AlertService.class ); | ||
| service.setWindow( getScene().getWindow() ); | ||
| + } | ||
| + | ||
| + private void initWatchDog() { | ||
| + setSnitchThread( new Thread( getWatchDog() ) ); | ||
| + getSnitchThread().start(); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Stops the snitch service, if its running. | ||
| + * | ||
| + * @throws InterruptedException Couldn't stop the snitch thread. | ||
| + */ | ||
| + @Override | ||
| + public void stop() throws InterruptedException { | ||
| + getWatchDog().stop(); | ||
| + | ||
| + final Thread thread = getSnitchThread(); | ||
| + | ||
| + if( thread != null ) { | ||
| + thread.interrupt(); | ||
| + thread.join(); | ||
| + } | ||
| + } | ||
| + | ||
| + private Snitch getWatchDog() { | ||
| + return this.snitch; | ||
| + } | ||
| + | ||
| + private Thread getSnitchThread() { | ||
| + return this.snitchThread; | ||
| + } | ||
| + | ||
| + private void setSnitchThread( final Thread thread ) { | ||
| + this.snitchThread = thread; | ||
| + } | ||
| + | ||
| + private Options getOptions() { | ||
| + return this.options; | ||
| } | ||
| } | ||
| - private static Application getApplication() { | ||
| - return app; | ||
| + private String getApplicationTitle() { | ||
| + return Messages.get( "Main.title" ); | ||
| } | ||
| - public static void showDocument( String uri ) { | ||
| - getApplication().getHostServices().showDocument( uri ); | ||
| + private static Application getApplication() { | ||
| + return app; | ||
| } | ||
| VariableNameInjector.listen( tab, getDefinitionPane() ); | ||
| } | ||
| - | ||
| + | ||
| /** | ||
| * Called whenever the preview pane becomes out of sync with the file editor | ||
| * tab. This can be called when the text changes, the caret paragraph changes, | ||
| * or the file tab changes. | ||
| * | ||
| * @param tab The file editor tab that has been changed in some fashion. | ||
| */ | ||
| private void refreshSelectedTab( final FileEditorTab tab ) { | ||
| final Path path = tab.getPath(); | ||
| - | ||
| - if( path != null ) { | ||
| - System.out.println( "Tab File: " + path ); | ||
| - } | ||
| final HTMLPreviewPane preview = getPreviewPane(); |
| package com.scrivenvar; | ||
| +import java.util.HashMap; | ||
| +import java.util.Map; | ||
| import java.util.ServiceLoader; | ||
| /** | ||
| - * Responsible for loading services. | ||
| + * Responsible for loading services. The services are treated as singleton | ||
| + * instances. | ||
| * | ||
| * @author White Magic Software, Ltd. | ||
| */ | ||
| public class Services { | ||
| + | ||
| + private static final Map<Class, Object> SINGLETONS = new HashMap<>( 8 ); | ||
| /** | ||
| - * Loads a service based on its interface definition. | ||
| + * Loads a service based on its interface definition. This will return an | ||
| + * existing instance if the class has already been instantiated. | ||
| * | ||
| * @param <T> The service to load. | ||
| * @param api The interface definition for the service. | ||
| * | ||
| * @return A class that implements the interface. | ||
| */ | ||
| - public static <T> T load( Class<T> api ) { | ||
| + public static <T> T load( final Class<T> api ) { | ||
| + @SuppressWarnings( "unchecked" ) | ||
| + final T o = (T)get( api ); | ||
| + | ||
| + return o == null ? newInstance( api ) : o; | ||
| + } | ||
| + | ||
| + private static <T> T newInstance( final Class<T> api ) { | ||
| final ServiceLoader<T> services = ServiceLoader.load( api ); | ||
| + | ||
| T result = null; | ||
| - for( T service : services ) { | ||
| + for( final T service : services ) { | ||
| result = service; | ||
| throw new RuntimeException( "No implementation for: " + api ); | ||
| } | ||
| + | ||
| + // Re-use the same instance the next time the class is loaded. | ||
| + put( api, result ); | ||
| return result; | ||
| + } | ||
| + | ||
| + private static void put( Class key, Object value ) { | ||
| + SINGLETONS.put( key, value ); | ||
| + } | ||
| + | ||
| + private static Object get( Class api ) { | ||
| + return SINGLETONS.get( api ); | ||
| } | ||
| } | ||
| */ | ||
| public class XMLProcessor extends AbstractProcessor<String> { | ||
| - | ||
| + | ||
| private final Snitch snitch = Services.load( Snitch.class ); | ||
| - | ||
| + | ||
| private XMLInputFactory xmlInputFactory; | ||
| private TransformerFactory transformerFactory; | ||
| - | ||
| - private String href; | ||
| + | ||
| private Path path; | ||
| // Extract the XML stylesheet processing instruction. | ||
| final String template = getXsltFilename( text ); | ||
| + final Path xsl = getXslPath( template ); | ||
| + | ||
| + // Listen for external file modification events. | ||
| + getWatchDog().listen( xsl ); | ||
| try( | ||
| - final StringWriter output = new StringWriter(); | ||
| + final StringWriter output = new StringWriter( text.length() ); | ||
| final StringReader input = new StringReader( text ) ) { | ||
| - | ||
| - final Source xml = new StreamSource( input ); | ||
| - final Path xsl = getXslPath( template ); | ||
| - final StreamResult sr = new StreamResult( output ); | ||
| - getTransformer( xsl ).transform( xml, sr ); | ||
| - | ||
| + | ||
| + getTransformer( xsl ).transform( | ||
| + new StreamSource( input ), | ||
| + new StreamResult( output ) | ||
| + ); | ||
| + | ||
| return output.toString(); | ||
| } | ||
| private Transformer getTransformer( final Path path ) | ||
| throws TransformerConfigurationException { | ||
| - | ||
| + | ||
| final TransformerFactory factory = getTransformerFactory(); | ||
| final Source xslt = new StreamSource( path.toFile() ); | ||
| return factory.newTransformer( xslt ); | ||
| } | ||
| - | ||
| + | ||
| private Path getXslPath( final String filename ) { | ||
| final Path xmlPath = getPath(); | ||
| final File xmlDirectory = xmlPath.toFile().getParentFile(); | ||
| - | ||
| + | ||
| return Paths.get( xmlDirectory.getPath(), filename ); | ||
| } | ||
| private String getXsltFilename( final String xml ) | ||
| throws XMLStreamException, ParseException { | ||
| - | ||
| + | ||
| String result = ""; | ||
| - | ||
| + | ||
| try( final StringReader sr = new StringReader( xml ) ) { | ||
| boolean found = false; | ||
| int count = 0; | ||
| final XMLEventReader reader = createXMLEventReader( sr ); | ||
| // If the processing instruction wasn't found in the first 10 lines, | ||
| // fail fast. This should iterate twice through the loop. | ||
| while( !found && reader.hasNext() && count++ < 10 ) { | ||
| final XMLEvent event = reader.nextEvent(); | ||
| - | ||
| + | ||
| if( event.isProcessingInstruction() ) { | ||
| final ProcessingInstruction pi = (ProcessingInstruction)event; | ||
| final String target = pi.getTarget(); | ||
| - | ||
| + | ||
| if( "xml-stylesheet".equalsIgnoreCase( target ) ) { | ||
| result = getPseudoAttribute( pi.getData(), "href" ); | ||
| found = true; | ||
| } | ||
| } | ||
| } | ||
| - | ||
| + | ||
| sr.close(); | ||
| } | ||
| - | ||
| + | ||
| return result; | ||
| } | ||
| - | ||
| + | ||
| private XMLEventReader createXMLEventReader( final Reader reader ) | ||
| throws XMLStreamException { | ||
| return getXMLInputFactory().createXMLEventReader( reader ); | ||
| } | ||
| - | ||
| + | ||
| private synchronized XMLInputFactory getXMLInputFactory() { | ||
| if( this.xmlInputFactory == null ) { | ||
| this.xmlInputFactory = createXMLInputFactory(); | ||
| } | ||
| - | ||
| + | ||
| return this.xmlInputFactory; | ||
| } | ||
| - | ||
| + | ||
| private XMLInputFactory createXMLInputFactory() { | ||
| return XMLInputFactory.newInstance(); | ||
| } | ||
| - | ||
| + | ||
| private synchronized TransformerFactory getTransformerFactory() { | ||
| if( this.transformerFactory == null ) { | ||
| this.transformerFactory = createTransformerFactory(); | ||
| } | ||
| - | ||
| + | ||
| return this.transformerFactory; | ||
| } | ||
| /** | ||
| * Returns a high-performance XSLT 2 transformation engine. | ||
| - * | ||
| + * | ||
| * @return An XSL transforming engine. | ||
| */ | ||
| private TransformerFactory createTransformerFactory() { | ||
| return new TransformerFactoryImpl(); | ||
| - //return TransformerFactory.newInstance(); | ||
| } | ||
| - | ||
| + | ||
| private void setPath( final Path path ) { | ||
| this.path = path; | ||
| } | ||
| - | ||
| + | ||
| private Path getPath() { | ||
| return this.path; | ||
| + } | ||
| + | ||
| + private Snitch getWatchDog() { | ||
| + return this.snitch; | ||
| } | ||
| } | ||
| -/* | ||
| - * Copyright 2016 White Magic Software, Ltd. | ||
| - * | ||
| - * All rights reserved. | ||
| - * | ||
| - * Redistribution and use in source and binary forms, with or without | ||
| - * modification, are permitted provided that the following conditions are met: | ||
| - * | ||
| - * o Redistributions of source code must retain the above copyright | ||
| - * notice, this list of conditions and the following disclaimer. | ||
| - * | ||
| - * o Redistributions in binary form must reproduce the above copyright | ||
| - * notice, this list of conditions and the following disclaimer in the | ||
| - * documentation and/or other materials provided with the distribution. | ||
| - * | ||
| - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | ||
| - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | ||
| - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | ||
| - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | ||
| - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | ||
| - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | ||
| - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | ||
| - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | ||
| - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
| - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | ||
| - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||
| - */ | ||
| -package com.scrivenvar.service; | ||
| - | ||
| -/** | ||
| - * | ||
| - * @author White Magic Software, Ltd. | ||
| - */ | ||
| -public interface Configuration extends Service { | ||
| - | ||
| - public Settings getSettings(); | ||
| - | ||
| - public Options getOptions(); | ||
| -} | ||
| * @author White Magic Software, Ltd. | ||
| */ | ||
| -public interface Options { | ||
| +public interface Options extends Service { | ||
| public Preferences getState(); |
| * @author White Magic Software, Ltd. | ||
| */ | ||
| -public interface Snitch { | ||
| +public interface Snitch extends Service, Runnable { | ||
| /** | ||
| * Listens for changes to the path. If the path specifies a file, then only | ||
| * notifications pertaining to that file are sent. Otherwise, change events | ||
| - * for the directory that contains the file are sent. | ||
| + * for the directory that contains the file are sent. This method must allow | ||
| + * for multiple calls to the same file without incurring additional listeners | ||
| + * or events. | ||
| * | ||
| * @param file Send notifications when this file changes. | ||
| * | ||
| * @throws IOException Couldn't create a watcher for the given file. | ||
| */ | ||
| public void listen( Path file ) throws IOException; | ||
| /** | ||
| * Removes the given file from the notifications list. | ||
| - * | ||
| + * | ||
| * @param file The file to stop monitoring for any changes. | ||
| */ |
| package com.scrivenvar.service.impl; | ||
| +import static com.scrivenvar.Constants.PREFS_OPTIONS; | ||
| import static com.scrivenvar.Constants.PREFS_ROOT; | ||
| +import static com.scrivenvar.Constants.PREFS_STATE; | ||
| import com.scrivenvar.service.Options; | ||
| import java.util.prefs.Preferences; | ||
| import static java.util.prefs.Preferences.userRoot; | ||
| -import static com.scrivenvar.Constants.PREFS_STATE; | ||
| -import static com.scrivenvar.Constants.PREFS_OPTIONS; | ||
| /** | ||
| * Persistent options user can change at runtime. | ||
| * | ||
| * @author Karl Tauber and White Magic Software, Ltd. | ||
| */ | ||
| public class DefaultOptions implements Options { | ||
| + | ||
| private Preferences preferences; | ||
| - | ||
| + | ||
| public DefaultOptions() { | ||
| - setPreferences(getRootPreferences().node(PREFS_OPTIONS ) ); | ||
| + setPreferences( getRootPreferences().node( PREFS_OPTIONS ) ); | ||
| } | ||
| @Override | ||
| public void put( final String key, final String value ) { | ||
| getPreferences().put( key, value ); | ||
| } | ||
| - | ||
| + | ||
| @Override | ||
| public String get( final String key, final String defalutValue ) { | ||
| return getPreferences().get( key, defalutValue ); | ||
| } | ||
| - | ||
| + | ||
| private void setPreferences( final Preferences preferences ) { | ||
| this.preferences = preferences; | ||
| @Override | ||
| public Preferences getState() { | ||
| - return getRootPreferences().node(PREFS_STATE ); | ||
| + return getRootPreferences().node( PREFS_STATE ); | ||
| } | ||
| * @author White Magic Software, Ltd. | ||
| */ | ||
| -public class DefaultSnitch implements Snitch, Runnable { | ||
| +public class DefaultSnitch implements Snitch { | ||
| /** | ||
| * Set to true when running; set to false to stop listening. | ||
| */ | ||
| - private boolean listening; | ||
| + private volatile boolean listening; | ||
| public DefaultSnitch() { | ||
| /** | ||
| - * Adds a listener to the list of files to watch for changes. | ||
| + * Adds a listener to the list of files to watch for changes. If the file is | ||
| + * already in the monitored list, this will return immediately. | ||
| * | ||
| * @param file Path to a file to watch for changes. | ||
| * | ||
| * @throws IOException The file could not be monitored. | ||
| */ | ||
| @Override | ||
| public void listen( final Path file ) throws IOException { | ||
| - // This will fail if the file is stored in the root folder. | ||
| - final Path path = Files.isDirectory( file ) ? file : file.getParent(); | ||
| - final WatchKey key = path.register( getWatchService(), ENTRY_MODIFY ); | ||
| + if( getEavesdropped().add( file ) ) { | ||
| + final Path dir = toDirectory( file ); | ||
| + final WatchKey key = dir.register( getWatchService(), ENTRY_MODIFY ); | ||
| - getWatchMap().put( key, path ); | ||
| - getEavesdropped().add( file ); | ||
| + getWatchMap().put( key, dir ); | ||
| + } | ||
| + } | ||
| + | ||
| + /** | ||
| + * Returns the given path to a file (or directory) as a directory. If the | ||
| + * given path is already a directory, it is returned. Otherwise, this returns | ||
| + * the directory that contains the file. This will fail if the file is stored | ||
| + * in the root folder. | ||
| + * | ||
| + * @param path The file to return as a directory, which should always be the | ||
| + * case. | ||
| + * | ||
| + * @return The given path as a directory, if a file, otherwise the path | ||
| + * itself. | ||
| + */ | ||
| + private Path toDirectory( final Path path ) { | ||
| + return Files.isDirectory( path ) | ||
| + ? path | ||
| + : path.toFile().getParentFile().toPath(); | ||
| } | ||
| /** | ||
| * Stop listening to the given file for change events. This fails silently. | ||
| * | ||
| * @param file The file to no longer monitor for changes. | ||
| */ | ||
| @Override | ||
| public void ignore( final Path file ) { | ||
| - // Remove all occurrences. | ||
| - getWatchMap().values().removeAll( Collections.singleton( file ) ); | ||
| + final Path directory = toDirectory( file ); | ||
| + | ||
| + // Remove all occurrences (there should be only one). | ||
| + getWatchMap().values().removeAll( Collections.singleton( directory ) ); | ||
| + | ||
| + // Remove all occurrences (there can be only one). | ||
| + getEavesdropped().remove( file ); | ||
| } | ||
| /** | ||
| - * Loops until isRunning is set to false. | ||
| + * Loops until stop is called, or the application is terminated. | ||
| */ | ||
| @Override | ||
| + @SuppressWarnings( "SleepWhileInLoop" ) | ||
| public void run() { | ||
| setListening( true ); | ||
| while( isListening() ) { | ||
| try { | ||
| final WatchKey key = getWatchService().take(); | ||
| final Path path = get( key ); | ||
| - for( WatchEvent<?> event : key.pollEvents() ) { | ||
| - final Path changed = (Path)event.context(); | ||
| + // Prevent receiving two separate ENTRY_MODIFY events: file modified | ||
| + // and timestamp updated. Instead, receive one ENTRY_MODIFY event | ||
| + // with two counts. | ||
| + Thread.sleep( 50 ); | ||
| - for( final Path file : getEavesdropped() ) { | ||
| - System.out.println( "Changed: " + changed ); | ||
| - System.out.println( "Monitored: " + file ); | ||
| + for( final WatchEvent<?> event : key.pollEvents() ) { | ||
| + final Path changed = path.resolve( (Path)event.context() ); | ||
| + | ||
| + if( event.kind() == ENTRY_MODIFY && isListening( changed ) ) { | ||
| + System.out.println( "RELOAD XSL: " + changed ); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| + } | ||
| + | ||
| + private boolean isListening( final Path path ) { | ||
| + return getEavesdropped().contains( path ); | ||
| } | ||
| + /** | ||
| + * Returns a path for a given watch key. | ||
| + * | ||
| + * @param key The key to lookup its corresponding path. | ||
| + * | ||
| + * @return The path for the given key. | ||
| + */ | ||
| private Path get( final WatchKey key ) { | ||
| return getWatchMap().get( key ); | ||
| Delta | 185 lines added, 125 lines removed, 60-line increase |
|---|