Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/keenwrite.git

File watcher detects changes to XSLT file while editing XML file.

Authordjarvis <email>
Date2016-12-18 02:03:23 GMT-0800
Commit0f9e56c549143ed729700c3eb85033befe17c3fb
Parent8d2ab30
src/main/java/com/scrivenvar/Main.java
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;
}
src/main/java/com/scrivenvar/MainWindow.java
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();
src/main/java/com/scrivenvar/Services.java
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 );
}
}
src/main/java/com/scrivenvar/processors/XMLProcessor.java
*/
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;
}
}
src/main/java/com/scrivenvar/service/Configuration.java
-/*
- * 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();
-}
src/main/java/com/scrivenvar/service/Options.java
* @author White Magic Software, Ltd.
*/
-public interface Options {
+public interface Options extends Service {
public Preferences getState();
src/main/java/com/scrivenvar/service/Snitch.java
* @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.
*/
src/main/java/com/scrivenvar/service/impl/DefaultOptions.java
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 );
}
src/main/java/com/scrivenvar/service/impl/DefaultSnitch.java
* @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 );
Delta185 lines added, 125 lines removed, 60-line increase