Dave Jarvis' Repositories

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

Added WatcherService for monitoring when XSLT files change. Minor performance improvements to XML processor code through object reuse.

Authordjarvis <email>
Date2016-12-17 23:10:42 GMT-0800
Commit8d2ab3010db6d95af22e83096d21dbc2783edeb1
Parent9cabd39
src/main/java/com/scrivenvar/Main.java
import static com.scrivenvar.Constants.*;
import com.scrivenvar.service.Options;
+import com.scrivenvar.service.Snitch;
import com.scrivenvar.service.events.AlertService;
import com.scrivenvar.util.StageState;
stage.show();
+ }
+
+ /**
+ * Stops the snitch service, if its running.
+ */
+ @Override
+ public void stop() {
+ Services.load( Snitch.class ).stop();
}
src/main/java/com/scrivenvar/MainWindow.java
import com.scrivenvar.processors.VariableProcessor;
import com.scrivenvar.processors.XMLCaretInsertionProcessor;
+import com.scrivenvar.processors.XMLProcessor;
import com.scrivenvar.service.Options;
import com.scrivenvar.util.Action;
import javafx.collections.ListChangeListener.Change;
import javafx.collections.ObservableList;
-import javafx.event.Event;
+import static javafx.event.Event.fireEvent;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.stage.Window;
import javafx.stage.WindowEvent;
+import static javafx.stage.WindowEvent.WINDOW_CLOSE_REQUEST;
/**
System.out.println( "Tab File: " + path );
}
-
+
final HTMLPreviewPane preview = getPreviewPane();
preview.setPath( tab.getPath() );
final Processor<String> hpp = new HTMLPreviewProcessor( preview );
final Processor<String> mcrp = new CaretReplacementProcessor( hpp );
final Processor<String> mp = new MarkdownProcessor( mcrp );
// final Processor<String> mcip = new MarkdownCaretInsertionProcessor( mp, tab.getCaretPosition() );
- final Processor<String> xmlp = new XMLCaretInsertionProcessor( mp, tab.getCaretPosition() );
- final Processor<String> vp = new VariableProcessor( xmlp, getResolvedMap() );
+ final Processor<String> xmlp = new XMLProcessor( mp, tab.getPath() );
+ final Processor<String> xcip = new XMLCaretInsertionProcessor( xmlp, tab.getCaretPosition() );
+ final Processor<String> vp = new VariableProcessor( xcip, getResolvedMap() );
vp.processChain( tab.getEditorText() );
private void fileExit() {
final Window window = getWindow();
- Event.fireEvent( window,
- new WindowEvent( window, WindowEvent.WINDOW_CLOSE_REQUEST ) );
+ fireEvent( window, new WindowEvent( window, WINDOW_CLOSE_REQUEST ) );
}
src/main/java/com/scrivenvar/processors/XMLCaretInsertionProcessor.java
@Override
public String processLink( final String t ) {
- final int caretOffset = getCaretPosition();
+ final int caret = getCaretPosition();
int insertOffset = -1;
if( t.length() > 0 ) {
try {
final VTDNav vn = getNavigator( t );
-
final int tokens = vn.getTokenCount();
int currTokenIndex = 0;
int prevTokenIndex = currTokenIndex;
- int currTokenOffset = 0;
-
- boolean found = false;
+ int currOffset = 0;
// To find the insertion spot even faster, the algorithm could
// use a binary search or interpolation search algorithm. This
// would reduce the worst-case iterations to O(log n) from O(n).
- while( currTokenIndex < tokens && !found ) {
+ while( currTokenIndex < tokens ) {
if( vn.getTokenType( currTokenIndex ) == TOKEN_CHARACTER_DATA ) {
- final int prevTokenOffset = currTokenOffset;
- currTokenOffset = vn.getTokenOffset( currTokenIndex );
-
- if( currTokenOffset > caretOffset ) {
- found = true;
+ final int prevOffset = currOffset;
+ currOffset = vn.getTokenOffset( currTokenIndex );
- final int prevTokenLength = vn.getTokenLength( prevTokenIndex );
+ if( currOffset > caret ) {
+ final int prevLength = vn.getTokenLength( prevTokenIndex );
// If the caret falls within the limits of the previous token, then
// insert the caret position marker at the caret offset.
- if( isBetween( caretOffset, prevTokenOffset, prevTokenOffset + prevTokenLength ) ) {
- insertOffset = caretOffset;
+ if( isBetween( caret, prevOffset, prevOffset + prevLength ) ) {
+ insertOffset = caret;
} else {
// The caret position is outside the previous token's text
// boundaries, but not inside the current text token. The
// caret should be positioned into the closer text token.
// For now, the cursor is positioned at the start of the
// current text token.
- insertOffset = currTokenOffset;
+ insertOffset = currOffset;
}
-
- // Done.
- continue;
+
+ break;
}
} catch( final Exception ex ) {
throw new RuntimeException(
- new ParseException( ex.getMessage(), caretOffset )
+ new ParseException( ex.getMessage(), caret )
);
}
* instance for scanning through the XML elements.
*
- * @param xml
+ * @param xml The XML document to parse.
*
- * @return
+ * @return A document navigator instance.
*/
private VTDNav getNavigator( final String xml ) throws VTDException {
final VTDGen vg = getParser();
+ // TODO: Use the document's encoding...
vg.setDoc( xml.getBytes() );
vg.parse( true );
src/main/java/com/scrivenvar/processors/XMLProcessor.java
package com.scrivenvar.processors;
+import com.scrivenvar.Services;
+import com.scrivenvar.service.Snitch;
import java.io.File;
+import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.nio.file.Path;
import java.nio.file.Paths;
-import javax.xml.parsers.SAXParser;
-import javax.xml.parsers.SAXParserFactory;
+import java.text.ParseException;
+import javax.xml.stream.XMLEventReader;
+import javax.xml.stream.XMLInputFactory;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.events.ProcessingInstruction;
+import javax.xml.stream.events.XMLEvent;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
+import net.sf.saxon.TransformerFactoryImpl;
import static net.sf.saxon.tree.util.ProcInstParser.getPseudoAttribute;
-import org.xml.sax.InputSource;
-import org.xml.sax.helpers.DefaultHandler;
/**
public class XMLProcessor extends AbstractProcessor<String> {
- private final ProcessingInstructionHandler handler = new ProcessingInstructionHandler();
+ private final Snitch snitch = Services.load( Snitch.class );
+
+ private XMLInputFactory xmlInputFactory;
+ private TransformerFactory transformerFactory;
+
private String href;
private Path path;
}
+ /**
+ * Transforms the given XML text into another form (typically Markdown).
+ *
+ * @param text The text to transform, can be empty, cannot be null.
+ *
+ * @return The transformed text, or empty if text is empty.
+ */
@Override
- public String processLink( final String t ) {
- String result = t;
+ public String processLink( final String text ) {
+ try {
+ return text.isEmpty() ? text : transform( text );
+ } catch( Exception e ) {
+ throw new RuntimeException( e );
+ }
+ }
- try( final StringReader sr = new StringReader( t ) ) {
- SAXParserFactory saxFactory = SAXParserFactory.newInstance();
- SAXParser saxParser = saxFactory.newSAXParser();
+ /**
+ * Performs an XSL transformation on the given XML text. The XML text must
+ * have a processing instruction that points to the XSL template file to use
+ * for the transformation.
+ *
+ * @param text The text to transform.
+ *
+ * @return The transformed text.
+ */
+ private String transform( final String text ) throws Exception {
+ // Extract the XML stylesheet processing instruction.
+ final String template = getXsltFilename( text );
- final InputSource is = new InputSource( sr );
- saxParser.parse( is, getHandler() );
+ try(
+ final StringWriter output = new StringWriter();
+ final StringReader input = new StringReader( text ) ) {
- } catch( Exception ex ) {
- System.out.println( ex.getMessage() );
+ final Source xml = new StreamSource( input );
+ final Path xsl = getXslPath( template );
+ final StreamResult sr = new StreamResult( output );
+ getTransformer( xsl ).transform( xml, sr );
+
+ return output.toString();
}
+ }
- try(
- final StringReader input = new StringReader( t );
- final StringWriter output = new StringWriter(); ) {
- final Source source = new StreamSource( input );
+ /**
+ * Returns an XSL transformer ready to transform an XML document using the
+ * XSLT file specified by the given path. If the path is already known then
+ * this will return the associated transformer.
+ *
+ * @param path The path to an XSLT file.
+ *
+ * @return A transformer that will transform XML documents using the given
+ * XSLT file.
+ *
+ * @throws TransformerConfigurationException Could not instantiate the
+ * transformer.
+ */
+ private Transformer getTransformer( final Path path )
+ throws TransformerConfigurationException {
- final TransformerFactory factory = TransformerFactory.newInstance();
- final Path xmlPath = getPath();
- final File xmlDirectory = xmlPath.toFile().getParentFile();
+ final TransformerFactory factory = getTransformerFactory();
+ final Source xslt = new StreamSource( path.toFile() );
+ return factory.newTransformer( xslt );
+ }
- final Path xslPath = Paths.get( xmlDirectory.getPath(), getHref() );
+ private Path getXslPath( final String filename ) {
+ final Path xmlPath = getPath();
+ final File xmlDirectory = xmlPath.toFile().getParentFile();
- final Source xslt = new StreamSource( xslPath.toFile() );
- final Transformer transformer = factory.newTransformer( xslt );
+ return Paths.get( xmlDirectory.getPath(), filename );
+ }
- final StreamResult sr = new StreamResult( output );
+ /**
+ * Given XML text, this will use a StAX pull reader to obtain the XML
+ * stylesheet processing instruction. This will throw a parse exception if the
+ * href pseudo-attribute filename value cannot be found.
+ *
+ * @param xml The XML containing an xml-stylesheet processing instruction.
+ *
+ * @return The href pseudo-attribute value.
+ *
+ * @throws XMLStreamException Could not parse the XML file.
+ * @throws ParseException Could not find a non-empty HREF attribute value.
+ */
+ private String getXsltFilename( final String xml )
+ throws XMLStreamException, ParseException {
- transformer.transform( source, sr );
+ String result = "";
- result = output.toString();
+ try( final StringReader sr = new StringReader( xml ) ) {
+ boolean found = false;
+ int count = 0;
+ final XMLEventReader reader = createXMLEventReader( sr );
- input.close();
- output.close();
- } catch( Exception e ) {
- System.out.println( e.getMessage() );
+ // 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 ProcessingInstructionHandler getHandler() {
- return this.handler;
+ private XMLEventReader createXMLEventReader( final Reader reader )
+ throws XMLStreamException {
+ return getXMLInputFactory().createXMLEventReader( reader );
}
- private String getHref() {
- return this.href;
+ private synchronized XMLInputFactory getXMLInputFactory() {
+ if( this.xmlInputFactory == null ) {
+ this.xmlInputFactory = createXMLInputFactory();
+ }
+
+ return this.xmlInputFactory;
}
- private void setHref( final String href ) {
- this.href = href;
+ 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 class ProcessingInstructionHandler extends DefaultHandler {
-
- @Override
- public void processingInstruction( final String target, final String data ) {
- if( "xml-stylesheet".equalsIgnoreCase( target ) ) {
- setHref( getPseudoAttribute( data, "href" ) );
- }
- }
}
}
src/main/java/com/scrivenvar/service/Snitch.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;
+
+import java.io.IOException;
+import java.nio.file.Path;
+
+/**
+ * Listens for changes to file system files and directories.
+ *
+ * @author White Magic Software, Ltd.
+ */
+public interface Snitch {
+
+ /**
+ * 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.
+ *
+ * @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.
+ */
+ public void ignore( final Path file );
+
+ /**
+ * Stop listening for events.
+ */
+ public void stop();
+}
src/main/java/com/scrivenvar/service/impl/DefaultSnitch.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.impl;
+
+import com.scrivenvar.service.Snitch;
+import java.io.IOException;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
+import java.nio.file.WatchEvent;
+import java.nio.file.WatchKey;
+import java.nio.file.WatchService;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Listens for file changes.
+ *
+ * @author White Magic Software, Ltd.
+ */
+public class DefaultSnitch implements Snitch, Runnable {
+
+ /**
+ * Service for listening to directories for modifications.
+ */
+ private WatchService watchService;
+
+ /**
+ * Directories being monitored for changes.
+ */
+ private Map<WatchKey, Path> keys;
+
+ /**
+ * Files that will kick off notification events if modified.
+ */
+ private Set<Path> eavesdropped;
+
+ /**
+ * Set to true when running; set to false to stop listening.
+ */
+ private boolean listening;
+
+ public DefaultSnitch() {
+ }
+
+ @Override
+ public void stop() {
+ setListening( false );
+ }
+
+ /**
+ * Adds a listener to the list of files to watch for changes.
+ *
+ * @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 );
+
+ getWatchMap().put( key, path );
+ getEavesdropped().add( file );
+ }
+
+ /**
+ * 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 ) );
+ }
+
+ /**
+ * Loops until isRunning is set to false.
+ */
+ @Override
+ 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();
+
+ for( final Path file : getEavesdropped() ) {
+ System.out.println( "Changed: " + changed );
+ System.out.println( "Monitored: " + file );
+ }
+ }
+
+ if( !key.reset() ) {
+ ignore( path );
+ }
+ } catch( IOException | InterruptedException ex ) {
+ // Stop eavesdropping.
+ setListening( false );
+ }
+ }
+ }
+
+ private Path get( final WatchKey key ) {
+ return getWatchMap().get( key );
+ }
+
+ private synchronized Map<WatchKey, Path> getWatchMap() {
+ if( this.keys == null ) {
+ this.keys = createWatchKeys();
+ }
+
+ return this.keys;
+ }
+
+ protected Map<WatchKey, Path> createWatchKeys() {
+ return new HashMap<>();
+ }
+
+ /**
+ * Returns a list of files that, when changed, will kick off a notification.
+ *
+ * @return A non-null, possibly empty, list of files.
+ */
+ private synchronized Set<Path> getEavesdropped() {
+ if( this.eavesdropped == null ) {
+ this.eavesdropped = createEavesdropped();
+ }
+
+ return this.eavesdropped;
+ }
+
+ protected Set<Path> createEavesdropped() {
+ return new HashSet<>();
+ }
+
+ /**
+ * The existing watch service, or a new instance if null.
+ *
+ * @return A valid WatchService instance, never null.
+ *
+ * @throws IOException Could not create a new watch service.
+ */
+ private synchronized WatchService getWatchService() throws IOException {
+ if( this.watchService == null ) {
+ this.watchService = createWatchService();
+ }
+
+ return this.watchService;
+ }
+
+ protected WatchService createWatchService() throws IOException {
+ final FileSystem fileSystem = FileSystems.getDefault();
+ return fileSystem.newWatchService();
+ }
+
+ /**
+ * Answers whether the loop should continue executing.
+ *
+ * @return true The internal listening loop should continue listening for file
+ * modification events.
+ */
+ protected boolean isListening() {
+ return this.listening;
+ }
+
+ /**
+ * Requests the snitch to stop eavesdropping on file changes.
+ *
+ * @param listening Use true to indicate the service should stop running.
+ */
+ private void setListening( final boolean listening ) {
+ this.listening = listening;
+ }
+}
src/main/resources/META-INF/services/com.scrivenvar.service.Snitch
-
+com.scrivenvar.service.impl.DefaultSnitch
Delta448 lines added, 75 lines removed, 373-line increase