| 1 | # Change Log | |
| 2 | ||
| 3 | ## 0.9 | |
| 4 | ||
| 5 | - Associate variable name injector with all tabs. | |
| 6 | ||
| 7 | ## 0.8 | |
| 8 | ||
| 9 | - Load YAML variables from files | |
| 10 | - Upgraded to Apache Commons Configuration 2.1 | |
| 11 | - Fixed bug with settings using comma-separated file extensions | |
| 12 | ||
| 13 | ## 0.7 | |
| 14 | ||
| 15 | - Added cursor to the preview pane | |
| 16 | - Reconfigured constants to use settings | |
| 17 | - Organized MainWindow code by similar method calls | |
| 18 | - Added single entry point for refreshing file editor tab | |
| 19 | ||
| 20 | ## 0.6 | |
| 21 | ||
| 22 | - Revised synchronized scrolling with preview panel | |
| 23 | - Added universal character encoding detection | |
| 24 | - Removed options panel | |
| 25 | - Decoupled Editor Tab and Preview Pane | |
| 26 | ||
| 27 | ## 0.5 | |
| 28 | ||
| 29 | - Added document processors for Markdown and Variables | |
| 30 | - Simplified code base | |
| 31 | - Added `Ctrl+Space` hot key for quick variable injection | |
| 32 | - Replaced commonmark-java with flexmark | |
| 33 | - Insert `CARETPOSITION` into document for preview pane scroll position reference | |
| 34 | ||
| 35 | ## 0.4 | |
| 36 | ||
| 37 | - Changed name to Scrivenvar | |
| 38 | - Added hot-keys for variable mode and autocomplete | |
| 39 | - Replaced pegdown with commonmark-java | |
| 40 | - Started document processors to provide XSLT and variable dereferencing | |
| 41 | ||
| 42 | ## 0.3 | |
| 43 | ||
| 44 | - Changed name to Scrivendor | |
| 45 | - Changed logo to match | |
| 46 | - Started to implement service-oriented architecture | |
| 47 | ||
| 48 | ## 0.2 | |
| 49 | ||
| 50 | - RichTextFX (and dependencies) updated to version 0.6.10 (fixes bugs) | |
| 51 | - pegdown Markdown parser updated to version 1.6 | |
| 52 | - Added five new pegdown 1.6 extension flags to Markdown Options tab | |
| 53 | - Minor improvements | |
| 54 | ||
| 55 | ## 0.1 | |
| 56 | 1 | |
| 57 | - Initial release |
| 1 | version = '1.0.2' | |
| 1 | version = '1.0.3' | |
| 2 | 2 | |
| 3 | 3 | apply plugin: 'java' |
| 30 | 30 | import static com.scrivenvar.Constants.*; |
| 31 | 31 | import static com.scrivenvar.Messages.get; |
| 32 | import com.scrivenvar.definition.DefinitionFactory; | |
| 33 | import com.scrivenvar.definition.DefinitionPane; | |
| 34 | import com.scrivenvar.definition.DefinitionSource; | |
| 35 | import com.scrivenvar.definition.EmptyDefinitionSource; | |
| 36 | import com.scrivenvar.editors.EditorPane; | |
| 37 | import com.scrivenvar.editors.VariableNameInjector; | |
| 38 | import com.scrivenvar.editors.markdown.MarkdownEditorPane; | |
| 39 | import com.scrivenvar.preview.HTMLPreviewPane; | |
| 40 | import com.scrivenvar.processors.Processor; | |
| 41 | import com.scrivenvar.processors.ProcessorFactory; | |
| 42 | import com.scrivenvar.service.Options; | |
| 43 | import com.scrivenvar.service.Snitch; | |
| 44 | import com.scrivenvar.util.Action; | |
| 45 | import com.scrivenvar.util.ActionUtils; | |
| 46 | import static com.scrivenvar.util.StageState.*; | |
| 47 | import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.*; | |
| 48 | import java.net.MalformedURLException; | |
| 49 | import java.nio.file.Path; | |
| 50 | import java.util.HashMap; | |
| 51 | import java.util.Map; | |
| 52 | import java.util.Observable; | |
| 53 | import java.util.Observer; | |
| 54 | import java.util.function.Function; | |
| 55 | import java.util.prefs.Preferences; | |
| 56 | import javafx.application.Platform; | |
| 57 | import javafx.beans.binding.Bindings; | |
| 58 | import javafx.beans.binding.BooleanBinding; | |
| 59 | import javafx.beans.property.BooleanProperty; | |
| 60 | import javafx.beans.property.SimpleBooleanProperty; | |
| 61 | import javafx.beans.value.ObservableBooleanValue; | |
| 62 | import javafx.beans.value.ObservableValue; | |
| 63 | import javafx.collections.ListChangeListener.Change; | |
| 64 | import javafx.collections.ObservableList; | |
| 65 | import static javafx.event.Event.fireEvent; | |
| 66 | import javafx.scene.Node; | |
| 67 | import javafx.scene.Scene; | |
| 68 | import javafx.scene.control.Alert; | |
| 69 | import javafx.scene.control.Alert.AlertType; | |
| 70 | import javafx.scene.control.Menu; | |
| 71 | import javafx.scene.control.MenuBar; | |
| 72 | import javafx.scene.control.SplitPane; | |
| 73 | import javafx.scene.control.Tab; | |
| 74 | import javafx.scene.control.ToolBar; | |
| 75 | import javafx.scene.control.TreeView; | |
| 76 | import javafx.scene.image.Image; | |
| 77 | import javafx.scene.image.ImageView; | |
| 78 | import static javafx.scene.input.KeyCode.ESCAPE; | |
| 79 | import javafx.scene.input.KeyEvent; | |
| 80 | import static javafx.scene.input.KeyEvent.CHAR_UNDEFINED; | |
| 81 | import static javafx.scene.input.KeyEvent.KEY_PRESSED; | |
| 82 | import javafx.scene.layout.BorderPane; | |
| 83 | import javafx.scene.layout.VBox; | |
| 84 | import javafx.stage.Window; | |
| 85 | import javafx.stage.WindowEvent; | |
| 86 | import static javafx.stage.WindowEvent.WINDOW_CLOSE_REQUEST; | |
| 87 | ||
| 88 | /** | |
| 89 | * Main window containing a tab pane in the center for file editors. | |
| 90 | * | |
| 91 | * @author Karl Tauber and White Magic Software, Ltd. | |
| 92 | */ | |
| 93 | public class MainWindow implements Observer { | |
| 94 | ||
| 95 | private final Options options = Services.load( Options.class ); | |
| 96 | private final Snitch snitch = Services.load( Snitch.class ); | |
| 97 | ||
| 98 | private Scene scene; | |
| 99 | private MenuBar menuBar; | |
| 100 | ||
| 101 | private DefinitionSource definitionSource; | |
| 102 | private DefinitionPane definitionPane; | |
| 103 | private FileEditorTabPane fileEditorPane; | |
| 104 | private HTMLPreviewPane previewPane; | |
| 105 | ||
| 106 | /** | |
| 107 | * Prevent re-instantiation processing classes. | |
| 108 | */ | |
| 109 | private Map<FileEditorTab, Processor<String>> processors; | |
| 110 | private ProcessorFactory processorFactory; | |
| 111 | ||
| 112 | ||
| 113 | public MainWindow() { | |
| 114 | initLayout(); | |
| 115 | initOpenDefinitionListener(); | |
| 116 | initTabAddedListener(); | |
| 117 | initTabChangedListener(); | |
| 118 | initPreferences(); | |
| 119 | initWatchDog(); | |
| 120 | } | |
| 121 | ||
| 122 | /** | |
| 123 | * Listen for file editor tab pane to receive an open definition source event. | |
| 124 | */ | |
| 125 | private void initOpenDefinitionListener() { | |
| 126 | getFileEditorPane().onOpenDefinitionFileProperty().addListener( | |
| 127 | (ObservableValue<? extends Path> definitionFile, | |
| 128 | final Path oldPath, final Path newPath) -> { | |
| 129 | openDefinition( newPath ); | |
| 130 | refreshSelectedTab( getActiveFileEditor() ); | |
| 131 | } ); | |
| 132 | } | |
| 133 | ||
| 134 | /** | |
| 135 | * When tabs are added, hook the various change listeners onto the new tab so | |
| 136 | * that the preview pane refreshes as necessary. | |
| 137 | */ | |
| 138 | private void initTabAddedListener() { | |
| 139 | final FileEditorTabPane editorPane = getFileEditorPane(); | |
| 140 | ||
| 141 | // Make sure the text processor kicks off when new files are opened. | |
| 142 | final ObservableList<Tab> tabs = editorPane.getTabs(); | |
| 143 | ||
| 144 | // Update the preview pane on tab changes. | |
| 145 | tabs.addListener( | |
| 146 | (final Change<? extends Tab> change) -> { | |
| 147 | while( change.next() ) { | |
| 148 | if( change.wasAdded() ) { | |
| 149 | // Multiple tabs can be added simultaneously. | |
| 150 | for( final Tab newTab : change.getAddedSubList() ) { | |
| 151 | final FileEditorTab tab = (FileEditorTab)newTab; | |
| 152 | ||
| 153 | initTextChangeListener( tab ); | |
| 154 | initCaretParagraphListener( tab ); | |
| 155 | initVariableNameInjector( tab ); | |
| 156 | } | |
| 157 | } | |
| 158 | } | |
| 159 | } | |
| 160 | ); | |
| 161 | } | |
| 162 | ||
| 163 | /** | |
| 164 | * Reloads the preferences from the previous load. | |
| 165 | */ | |
| 166 | private void initPreferences() { | |
| 167 | getFileEditorPane().restorePreferences(); | |
| 168 | restoreDefinitionSource(); | |
| 169 | } | |
| 170 | ||
| 171 | /** | |
| 172 | * Listen for new tab selection events. | |
| 173 | */ | |
| 174 | private void initTabChangedListener() { | |
| 175 | final FileEditorTabPane editorPane = getFileEditorPane(); | |
| 176 | ||
| 177 | // Update the preview pane changing tabs. | |
| 178 | editorPane.addTabSelectionListener( | |
| 179 | (ObservableValue<? extends Tab> tabPane, | |
| 180 | final Tab oldTab, final Tab newTab) -> { | |
| 181 | ||
| 182 | // If there was no old tab, then this is a first time load, which | |
| 183 | // can be ignored. | |
| 184 | if( oldTab != null ) { | |
| 185 | if( newTab == null ) { | |
| 186 | closeRemainingTab(); | |
| 187 | } else { | |
| 188 | // Update the preview with the edited text. | |
| 189 | refreshSelectedTab( (FileEditorTab)newTab ); | |
| 190 | } | |
| 191 | } | |
| 192 | } | |
| 193 | ); | |
| 194 | } | |
| 195 | ||
| 196 | private void initTextChangeListener( final FileEditorTab tab ) { | |
| 197 | tab.addTextChangeListener( | |
| 198 | (ObservableValue<? extends String> editor, | |
| 199 | final String oldValue, final String newValue) -> { | |
| 200 | refreshSelectedTab( tab ); | |
| 201 | } | |
| 202 | ); | |
| 203 | } | |
| 204 | ||
| 205 | private void initCaretParagraphListener( final FileEditorTab tab ) { | |
| 206 | tab.addCaretParagraphListener( | |
| 207 | (ObservableValue<? extends Integer> editor, | |
| 208 | final Integer oldValue, final Integer newValue) -> { | |
| 209 | refreshSelectedTab( tab ); | |
| 210 | } | |
| 211 | ); | |
| 212 | } | |
| 213 | ||
| 214 | private void initVariableNameInjector( final FileEditorTab tab ) { | |
| 215 | VariableNameInjector.listen( tab, getDefinitionPane() ); | |
| 216 | } | |
| 217 | ||
| 218 | private void initWatchDog() { | |
| 219 | getSnitch().addObserver( this ); | |
| 220 | } | |
| 221 | ||
| 222 | /** | |
| 223 | * Called whenever the preview pane becomes out of sync with the file editor | |
| 224 | * tab. This can be called when the text changes, the caret paragraph changes, | |
| 225 | * or the file tab changes. | |
| 226 | * | |
| 227 | * @param tab The file editor tab that has been changed in some fashion. | |
| 228 | */ | |
| 229 | private void refreshSelectedTab( final FileEditorTab tab ) { | |
| 230 | getPreviewPane().setPath( tab.getPath() ); | |
| 231 | ||
| 232 | Processor<String> processor = getProcessors().get( tab ); | |
| 233 | ||
| 234 | if( processor == null ) { | |
| 235 | processor = createProcessor( tab ); | |
| 236 | getProcessors().put( tab, processor ); | |
| 237 | } | |
| 238 | ||
| 239 | processor.processChain( tab.getEditorText() ); | |
| 240 | } | |
| 241 | ||
| 242 | /** | |
| 243 | * Returns the variable map of interpolated definitions. | |
| 244 | * | |
| 245 | * @return A map to help dereference variables. | |
| 246 | */ | |
| 247 | private Map<String, String> getResolvedMap() { | |
| 248 | return getDefinitionSource().getResolvedMap(); | |
| 249 | } | |
| 250 | ||
| 251 | /** | |
| 252 | * Returns the root node for the hierarchical definition source. | |
| 253 | * | |
| 254 | * @return Data to display in the definition pane. | |
| 255 | */ | |
| 256 | private TreeView<String> getTreeView() { | |
| 257 | try { | |
| 258 | return getDefinitionSource().asTreeView(); | |
| 259 | } catch( Exception e ) { | |
| 260 | alert( e ); | |
| 261 | } | |
| 262 | ||
| 263 | return new TreeView<>(); | |
| 264 | } | |
| 265 | ||
| 266 | private void openDefinition( final Path path ) { | |
| 267 | openDefinition( path.toString() ); | |
| 268 | } | |
| 269 | ||
| 270 | private void openDefinition( final String path ) { | |
| 271 | try { | |
| 272 | final DefinitionSource ds = createDefinitionSource( path ); | |
| 273 | setDefinitionSource( ds ); | |
| 274 | storeDefinitionSource(); | |
| 275 | ||
| 276 | getDefinitionPane().setRoot( ds.asTreeView() ); | |
| 277 | } catch( Exception e ) { | |
| 278 | alert( e ); | |
| 279 | } | |
| 280 | } | |
| 281 | ||
| 282 | private void restoreDefinitionSource() { | |
| 283 | final Preferences preferences = getPreferences(); | |
| 284 | final String source = preferences.get( PREFS_DEFINITION_SOURCE, null ); | |
| 285 | ||
| 286 | if( source != null ) { | |
| 287 | openDefinition( source ); | |
| 288 | } | |
| 289 | } | |
| 290 | ||
| 291 | private void storeDefinitionSource() { | |
| 292 | final Preferences preferences = getPreferences(); | |
| 293 | final DefinitionSource ds = getDefinitionSource(); | |
| 294 | ||
| 295 | preferences.put( PREFS_DEFINITION_SOURCE, ds.toString() ); | |
| 296 | } | |
| 297 | ||
| 298 | /** | |
| 299 | * Called when the last open tab is closed. This clears out the preview pane | |
| 300 | * and the definition pane. | |
| 301 | */ | |
| 302 | private void closeRemainingTab() { | |
| 303 | getPreviewPane().clear(); | |
| 304 | getDefinitionPane().clear(); | |
| 305 | } | |
| 306 | ||
| 307 | /** | |
| 308 | * Called when an exception occurs that warrants the user's attention. | |
| 309 | * | |
| 310 | * @param e The exception with a message that the user should know about. | |
| 311 | */ | |
| 312 | private void alert( final Exception e ) { | |
| 313 | // TODO: Update the status bar. | |
| 314 | } | |
| 315 | ||
| 316 | //---- File actions ------------------------------------------------------- | |
| 317 | /** | |
| 318 | * Called when a file has been modified. | |
| 319 | * | |
| 320 | * @param snitch The watchdog file monitoring instance. | |
| 321 | * @param file The file that was modified. | |
| 322 | */ | |
| 323 | @Override | |
| 324 | public void update( final Observable snitch, final Object file ) { | |
| 325 | if( file instanceof Path ) { | |
| 326 | update( (Path)file ); | |
| 327 | } | |
| 328 | } | |
| 329 | ||
| 330 | /** | |
| 331 | * Called when a file has been modified. | |
| 332 | * | |
| 333 | * @param file Path to the modified file. | |
| 334 | */ | |
| 335 | private void update( final Path file ) { | |
| 336 | // Avoid throwing IllegalStateException by running from a non-JavaFX thread. | |
| 337 | Platform.runLater( | |
| 338 | () -> { | |
| 339 | // Brute-force XSLT file reload by re-instantiating all processors. | |
| 340 | getProcessors().clear(); | |
| 341 | refreshSelectedTab( getActiveFileEditor() ); | |
| 342 | } | |
| 343 | ); | |
| 344 | } | |
| 345 | ||
| 346 | //---- File actions ------------------------------------------------------- | |
| 347 | private void fileNew() { | |
| 348 | getFileEditorPane().newEditor(); | |
| 349 | } | |
| 350 | ||
| 351 | private void fileOpen() { | |
| 352 | getFileEditorPane().openFileDialog(); | |
| 353 | } | |
| 354 | ||
| 355 | private void fileClose() { | |
| 356 | getFileEditorPane().closeEditor( getActiveFileEditor(), true ); | |
| 357 | } | |
| 358 | ||
| 359 | private void fileCloseAll() { | |
| 360 | getFileEditorPane().closeAllEditors(); | |
| 361 | } | |
| 362 | ||
| 363 | private void fileSave() { | |
| 364 | getFileEditorPane().saveEditor( getActiveFileEditor() ); | |
| 365 | } | |
| 366 | ||
| 367 | private void fileSaveAll() { | |
| 368 | getFileEditorPane().saveAllEditors(); | |
| 369 | } | |
| 370 | ||
| 371 | private void fileExit() { | |
| 372 | final Window window = getWindow(); | |
| 373 | fireEvent( window, new WindowEvent( window, WINDOW_CLOSE_REQUEST ) ); | |
| 374 | } | |
| 375 | ||
| 376 | //---- Help actions ------------------------------------------------------- | |
| 377 | private void helpAbout() { | |
| 378 | Alert alert = new Alert( AlertType.INFORMATION ); | |
| 379 | alert.setTitle( get( "Dialog.about.title" ) ); | |
| 380 | alert.setHeaderText( get( "Dialog.about.header" ) ); | |
| 381 | alert.setContentText( get( "Dialog.about.content" ) ); | |
| 382 | alert.setGraphic( new ImageView( new Image( FILE_LOGO_32 ) ) ); | |
| 383 | alert.initOwner( getWindow() ); | |
| 384 | ||
| 385 | alert.showAndWait(); | |
| 386 | } | |
| 387 | ||
| 388 | //---- Convenience accessors ---------------------------------------------- | |
| 389 | private float getFloat( final String key, final float defaultValue ) { | |
| 390 | return getPreferences().getFloat( key, defaultValue ); | |
| 391 | } | |
| 392 | ||
| 393 | private Preferences getPreferences() { | |
| 394 | return getOptions().getState(); | |
| 395 | } | |
| 396 | ||
| 397 | private Window getWindow() { | |
| 398 | return getScene().getWindow(); | |
| 399 | } | |
| 400 | ||
| 401 | private MarkdownEditorPane getActiveEditor() { | |
| 402 | final EditorPane pane = getActiveFileEditor().getEditorPane(); | |
| 403 | ||
| 404 | return pane instanceof MarkdownEditorPane ? (MarkdownEditorPane)pane : null; | |
| 405 | } | |
| 406 | ||
| 407 | private FileEditorTab getActiveFileEditor() { | |
| 408 | return getFileEditorPane().getActiveFileEditor(); | |
| 409 | } | |
| 410 | ||
| 411 | //---- Member accessors --------------------------------------------------- | |
| 412 | public Scene getScene() { | |
| 413 | return this.scene; | |
| 414 | } | |
| 415 | ||
| 416 | private void setScene( Scene scene ) { | |
| 417 | this.scene = scene; | |
| 418 | } | |
| 419 | ||
| 420 | private Map<FileEditorTab, Processor<String>> getProcessors() { | |
| 421 | if( this.processors == null ) { | |
| 422 | this.processors = new HashMap<>(); | |
| 423 | } | |
| 424 | ||
| 425 | return this.processors; | |
| 426 | } | |
| 427 | ||
| 428 | private ProcessorFactory getProcessorFactory() { | |
| 429 | if( this.processorFactory == null ) { | |
| 430 | this.processorFactory = createProcessorFactory(); | |
| 431 | } | |
| 432 | ||
| 433 | return this.processorFactory; | |
| 434 | } | |
| 435 | ||
| 436 | private FileEditorTabPane getFileEditorPane() { | |
| 437 | if( this.fileEditorPane == null ) { | |
| 438 | this.fileEditorPane = createFileEditorPane(); | |
| 439 | } | |
| 440 | ||
| 441 | return this.fileEditorPane; | |
| 442 | } | |
| 443 | ||
| 444 | private HTMLPreviewPane getPreviewPane() { | |
| 445 | if( this.previewPane == null ) { | |
| 446 | this.previewPane = createPreviewPane(); | |
| 447 | } | |
| 448 | ||
| 449 | return this.previewPane; | |
| 450 | } | |
| 451 | ||
| 452 | private void setDefinitionSource( final DefinitionSource definitionSource ) { | |
| 453 | this.definitionSource = definitionSource; | |
| 454 | } | |
| 455 | ||
| 456 | private DefinitionSource getDefinitionSource() { | |
| 457 | if( this.definitionSource == null ) { | |
| 458 | this.definitionSource = new EmptyDefinitionSource(); | |
| 459 | } | |
| 460 | ||
| 461 | return this.definitionSource; | |
| 462 | } | |
| 463 | ||
| 464 | private DefinitionPane getDefinitionPane() { | |
| 465 | if( this.definitionPane == null ) { | |
| 466 | this.definitionPane = createDefinitionPane(); | |
| 467 | } | |
| 468 | ||
| 469 | return this.definitionPane; | |
| 470 | } | |
| 471 | ||
| 472 | private Options getOptions() { | |
| 473 | return this.options; | |
| 474 | } | |
| 475 | ||
| 476 | private Snitch getSnitch() { | |
| 477 | return this.snitch; | |
| 478 | } | |
| 479 | ||
| 480 | public MenuBar getMenuBar() { | |
| 481 | return this.menuBar; | |
| 482 | } | |
| 483 | ||
| 484 | public void setMenuBar( MenuBar menuBar ) { | |
| 485 | this.menuBar = menuBar; | |
| 486 | } | |
| 487 | ||
| 488 | //---- Member creators ---------------------------------------------------- | |
| 489 | /** | |
| 490 | * Factory to create processors that are suited to different file types. | |
| 491 | * | |
| 492 | * @param tab The tab that is subjected to processing. | |
| 493 | * | |
| 494 | * @return A processor suited to the file type specified by the tab's path. | |
| 495 | */ | |
| 496 | private Processor<String> createProcessor( final FileEditorTab tab ) { | |
| 497 | return getProcessorFactory().createProcessor( tab ); | |
| 498 | } | |
| 499 | ||
| 500 | private ProcessorFactory createProcessorFactory() { | |
| 501 | return new ProcessorFactory( getPreviewPane(), getResolvedMap() ); | |
| 502 | } | |
| 503 | ||
| 504 | private DefinitionSource createDefinitionSource( final String path ) | |
| 505 | throws MalformedURLException { | |
| 506 | return createDefinitionFactory().createDefinitionSource( path ); | |
| 507 | } | |
| 508 | ||
| 509 | /** | |
| 510 | * Create an editor pane to hold file editor tabs. | |
| 511 | * | |
| 512 | * @return A new instance, never null. | |
| 513 | */ | |
| 514 | private FileEditorTabPane createFileEditorPane() { | |
| 515 | return new FileEditorTabPane(); | |
| 516 | } | |
| 517 | ||
| 518 | private HTMLPreviewPane createPreviewPane() { | |
| 519 | return new HTMLPreviewPane(); | |
| 520 | } | |
| 521 | ||
| 522 | private DefinitionPane createDefinitionPane() { | |
| 523 | return new DefinitionPane( getTreeView() ); | |
| 524 | } | |
| 525 | ||
| 526 | private DefinitionFactory createDefinitionFactory() { | |
| 527 | return new DefinitionFactory(); | |
| 528 | } | |
| 529 | ||
| 530 | private Node createMenuBar() { | |
| 531 | final BooleanBinding activeFileEditorIsNull = getFileEditorPane().activeFileEditorProperty().isNull(); | |
| 532 | ||
| 533 | // File actions | |
| 534 | Action fileNewAction = new Action( get( "Main.menu.file.new" ), "Shortcut+N", FILE_ALT, e -> fileNew() ); | |
| 535 | Action fileOpenAction = new Action( get( "Main.menu.file.open" ), "Shortcut+O", FOLDER_OPEN_ALT, e -> fileOpen() ); | |
| 536 | Action fileCloseAction = new Action( get( "Main.menu.file.close" ), "Shortcut+W", null, e -> fileClose(), activeFileEditorIsNull ); | |
| 537 | Action fileCloseAllAction = new Action( get( "Main.menu.file.close_all" ), null, null, e -> fileCloseAll(), activeFileEditorIsNull ); | |
| 538 | Action fileSaveAction = new Action( get( "Main.menu.file.save" ), "Shortcut+S", FLOPPY_ALT, e -> fileSave(), | |
| 539 | createActiveBooleanProperty( FileEditorTab::modifiedProperty ).not() ); | |
| 540 | Action fileSaveAllAction = new Action( get( "Main.menu.file.save_all" ), "Shortcut+Shift+S", null, e -> fileSaveAll(), | |
| 541 | Bindings.not( getFileEditorPane().anyFileEditorModifiedProperty() ) ); | |
| 542 | Action fileExitAction = new Action( get( "Main.menu.file.exit" ), null, null, e -> fileExit() ); | |
| 543 | ||
| 544 | // Edit actions | |
| 545 | Action editUndoAction = new Action( get( "Main.menu.edit.undo" ), "Shortcut+Z", UNDO, | |
| 546 | e -> getActiveEditor().undo(), | |
| 547 | createActiveBooleanProperty( FileEditorTab::canUndoProperty ).not() ); | |
| 548 | Action editRedoAction = new Action( get( "Main.menu.edit.redo" ), "Shortcut+Y", REPEAT, | |
| 549 | e -> getActiveEditor().redo(), | |
| 550 | createActiveBooleanProperty( FileEditorTab::canRedoProperty ).not() ); | |
| 551 | ||
| 552 | // Insert actions | |
| 553 | Action insertBoldAction = new Action( get( "Main.menu.insert.bold" ), "Shortcut+B", BOLD, | |
| 554 | e -> getActiveEditor().surroundSelection( "**", "**" ), | |
| 555 | activeFileEditorIsNull ); | |
| 556 | Action insertItalicAction = new Action( get( "Main.menu.insert.italic" ), "Shortcut+I", ITALIC, | |
| 557 | e -> getActiveEditor().surroundSelection( "*", "*" ), | |
| 558 | activeFileEditorIsNull ); | |
| 559 | Action insertStrikethroughAction = new Action( get( "Main.menu.insert.strikethrough" ), "Shortcut+T", STRIKETHROUGH, | |
| 560 | e -> getActiveEditor().surroundSelection( "~~", "~~" ), | |
| 561 | activeFileEditorIsNull ); | |
| 562 | Action insertBlockquoteAction = new Action( get( "Main.menu.insert.blockquote" ), "Ctrl+Q", QUOTE_LEFT, // not Shortcut+Q because of conflict on Mac | |
| 563 | e -> getActiveEditor().surroundSelection( "\n\n> ", "" ), | |
| 564 | activeFileEditorIsNull ); | |
| 565 | Action insertCodeAction = new Action( get( "Main.menu.insert.code" ), "Shortcut+K", CODE, | |
| 566 | e -> getActiveEditor().surroundSelection( "`", "`" ), | |
| 567 | activeFileEditorIsNull ); | |
| 568 | Action insertFencedCodeBlockAction = new Action( get( "Main.menu.insert.fenced_code_block" ), "Shortcut+Shift+K", FILE_CODE_ALT, | |
| 569 | e -> getActiveEditor().surroundSelection( "\n\n```\n", "\n```\n\n", get( "Main.menu.insert.fenced_code_block.prompt" ) ), | |
| 570 | activeFileEditorIsNull ); | |
| 571 | ||
| 572 | Action insertLinkAction = new Action( get( "Main.menu.insert.link" ), "Shortcut+L", LINK, | |
| 573 | e -> getActiveEditor().insertLink(), | |
| 574 | activeFileEditorIsNull ); | |
| 575 | Action insertImageAction = new Action( get( "Main.menu.insert.image" ), "Shortcut+G", PICTURE_ALT, | |
| 576 | e -> getActiveEditor().insertImage(), | |
| 577 | activeFileEditorIsNull ); | |
| 578 | ||
| 579 | final Action[] headers = new Action[ 6 ]; | |
| 580 | ||
| 581 | // Insert header actions (H1 ... H6) | |
| 582 | for( int i = 1; i <= 6; i++ ) { | |
| 583 | final String hashes = new String( new char[ i ] ).replace( "\0", "#" ); | |
| 584 | final String markup = String.format( "%n%n%s ", hashes ); | |
| 585 | final String text = get( "Main.menu.insert.header_" + i ); | |
| 586 | final String accelerator = "Shortcut+" + i; | |
| 587 | final String prompt = get( "Main.menu.insert.header_" + i + ".prompt" ); | |
| 588 | ||
| 589 | headers[ i - 1 ] = new Action( text, accelerator, HEADER, | |
| 590 | e -> getActiveEditor().surroundSelection( markup, "", prompt ), | |
| 591 | activeFileEditorIsNull ); | |
| 592 | } | |
| 593 | ||
| 594 | Action insertUnorderedListAction = new Action( get( "Main.menu.insert.unordered_list" ), "Shortcut+U", LIST_UL, | |
| 595 | e -> getActiveEditor().surroundSelection( "\n\n* ", "" ), | |
| 596 | activeFileEditorIsNull ); | |
| 597 | Action insertOrderedListAction = new Action( get( "Main.menu.insert.ordered_list" ), "Shortcut+Shift+O", LIST_OL, | |
| 598 | e -> getActiveEditor().surroundSelection( "\n\n1. ", "" ), | |
| 599 | activeFileEditorIsNull ); | |
| 600 | Action insertHorizontalRuleAction = new Action( get( "Main.menu.insert.horizontal_rule" ), "Shortcut+H", null, | |
| 601 | e -> getActiveEditor().surroundSelection( "\n\n---\n\n", "" ), | |
| 602 | activeFileEditorIsNull ); | |
| 603 | ||
| 604 | // Help actions | |
| 605 | Action helpAboutAction = new Action( get( "Main.menu.help.about" ), null, null, e -> helpAbout() ); | |
| 606 | ||
| 607 | //---- MenuBar ---- | |
| 608 | Menu fileMenu = ActionUtils.createMenu( get( "Main.menu.file" ), | |
| 609 | fileNewAction, | |
| 610 | fileOpenAction, | |
| 611 | null, | |
| 612 | fileCloseAction, | |
| 613 | fileCloseAllAction, | |
| 614 | null, | |
| 615 | fileSaveAction, | |
| 616 | fileSaveAllAction, | |
| 617 | null, | |
| 618 | fileExitAction ); | |
| 619 | ||
| 620 | Menu editMenu = ActionUtils.createMenu( get( "Main.menu.edit" ), | |
| 621 | editUndoAction, | |
| 622 | editRedoAction ); | |
| 623 | ||
| 624 | Menu insertMenu = ActionUtils.createMenu( get( "Main.menu.insert" ), | |
| 625 | insertBoldAction, | |
| 626 | insertItalicAction, | |
| 627 | insertStrikethroughAction, | |
| 628 | insertBlockquoteAction, | |
| 629 | insertCodeAction, | |
| 630 | insertFencedCodeBlockAction, | |
| 631 | null, | |
| 632 | insertLinkAction, | |
| 633 | insertImageAction, | |
| 634 | null, | |
| 635 | headers[ 0 ], | |
| 636 | headers[ 1 ], | |
| 637 | headers[ 2 ], | |
| 638 | headers[ 3 ], | |
| 639 | headers[ 4 ], | |
| 640 | headers[ 5 ], | |
| 641 | null, | |
| 642 | insertUnorderedListAction, | |
| 643 | insertOrderedListAction, | |
| 644 | insertHorizontalRuleAction ); | |
| 645 | ||
| 646 | Menu helpMenu = ActionUtils.createMenu( get( "Main.menu.help" ), | |
| 647 | helpAboutAction ); | |
| 648 | ||
| 649 | menuBar = new MenuBar( fileMenu, editMenu, insertMenu, helpMenu ); | |
| 650 | ||
| 651 | //---- ToolBar ---- | |
| 652 | ToolBar toolBar = ActionUtils.createToolBar( | |
| 653 | fileNewAction, | |
| 654 | fileOpenAction, | |
| 655 | fileSaveAction, | |
| 656 | null, | |
| 657 | editUndoAction, | |
| 658 | editRedoAction, | |
| 659 | null, | |
| 660 | insertBoldAction, | |
| 661 | insertItalicAction, | |
| 662 | insertBlockquoteAction, | |
| 663 | insertCodeAction, | |
| 664 | insertFencedCodeBlockAction, | |
| 665 | null, | |
| 666 | insertLinkAction, | |
| 667 | insertImageAction, | |
| 668 | null, | |
| 669 | headers[ 0 ], | |
| 670 | null, | |
| 671 | insertUnorderedListAction, | |
| 672 | insertOrderedListAction ); | |
| 673 | ||
| 674 | return new VBox( menuBar, toolBar ); | |
| 675 | } | |
| 676 | ||
| 677 | /** | |
| 678 | * Creates a boolean property that is bound to another boolean value of the | |
| 679 | * active editor. | |
| 680 | */ | |
| 681 | private BooleanProperty createActiveBooleanProperty( | |
| 682 | final Function<FileEditorTab, ObservableBooleanValue> func ) { | |
| 683 | ||
| 684 | final BooleanProperty b = new SimpleBooleanProperty(); | |
| 685 | final FileEditorTab tab = getActiveFileEditor(); | |
| 686 | ||
| 687 | if( tab != null ) { | |
| 688 | b.bind( func.apply( tab ) ); | |
| 689 | } | |
| 690 | ||
| 691 | getFileEditorPane().activeFileEditorProperty().addListener( | |
| 692 | (observable, oldFileEditor, newFileEditor) -> { | |
| 693 | b.unbind(); | |
| 694 | ||
| 695 | if( newFileEditor != null ) { | |
| 696 | b.bind( func.apply( newFileEditor ) ); | |
| 697 | } else { | |
| 698 | b.set( false ); | |
| 699 | } | |
| 700 | } | |
| 701 | ); | |
| 702 | ||
| 703 | return b; | |
| 704 | } | |
| 705 | ||
| 706 | private void initLayout() { | |
| 707 | final SplitPane splitPane = new SplitPane( | |
| 708 | getDefinitionPane().getNode(), | |
| 709 | getFileEditorPane().getNode(), | |
| 710 | getPreviewPane().getNode() ); | |
| 711 | ||
| 712 | splitPane.setDividerPositions( | |
| 713 | getFloat( K_PANE_SPLIT_DEFINITION, .10f ), | |
| 714 | getFloat( K_PANE_SPLIT_EDITOR, .45f ), | |
| 715 | getFloat( K_PANE_SPLIT_PREVIEW, .45f ) ); | |
| 716 | ||
| 717 | // See: http://broadlyapplicable.blogspot.ca/2015/03/javafx-capture-restorePreferences-splitpane.html | |
| 718 | final BorderPane borderPane = new BorderPane(); | |
| 719 | borderPane.setPrefSize( 1024, 800 ); | |
| 720 | borderPane.setTop( createMenuBar() ); | |
| 721 | borderPane.setCenter( splitPane ); | |
| 722 | ||
| 32 | import com.scrivenvar.definition.*; | |
| 33 | import com.scrivenvar.editors.EditorPane; | |
| 34 | import com.scrivenvar.editors.VariableNameInjector; | |
| 35 | import com.scrivenvar.editors.markdown.MarkdownEditorPane; | |
| 36 | import com.scrivenvar.preview.HTMLPreviewPane; | |
| 37 | import com.scrivenvar.processors.Processor; | |
| 38 | import com.scrivenvar.processors.ProcessorFactory; | |
| 39 | import com.scrivenvar.service.Options; | |
| 40 | import com.scrivenvar.service.Snitch; | |
| 41 | import com.scrivenvar.util.Action; | |
| 42 | import com.scrivenvar.util.ActionUtils; | |
| 43 | import static com.scrivenvar.util.StageState.*; | |
| 44 | import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.*; | |
| 45 | import java.nio.file.Path; | |
| 46 | import java.util.HashMap; | |
| 47 | import java.util.Map; | |
| 48 | import java.util.Observable; | |
| 49 | import java.util.Observer; | |
| 50 | import java.util.function.Function; | |
| 51 | import java.util.prefs.Preferences; | |
| 52 | import javafx.application.Platform; | |
| 53 | import javafx.beans.binding.Bindings; | |
| 54 | import javafx.beans.binding.BooleanBinding; | |
| 55 | import javafx.beans.property.BooleanProperty; | |
| 56 | import javafx.beans.property.SimpleBooleanProperty; | |
| 57 | import javafx.beans.value.ObservableBooleanValue; | |
| 58 | import javafx.beans.value.ObservableValue; | |
| 59 | import javafx.collections.ListChangeListener.Change; | |
| 60 | import javafx.collections.ObservableList; | |
| 61 | import static javafx.event.Event.fireEvent; | |
| 62 | import javafx.scene.Node; | |
| 63 | import javafx.scene.Scene; | |
| 64 | import javafx.scene.control.Alert; | |
| 65 | import javafx.scene.control.Alert.AlertType; | |
| 66 | import javafx.scene.control.Menu; | |
| 67 | import javafx.scene.control.MenuBar; | |
| 68 | import javafx.scene.control.SplitPane; | |
| 69 | import javafx.scene.control.Tab; | |
| 70 | import javafx.scene.control.ToolBar; | |
| 71 | import javafx.scene.control.TreeView; | |
| 72 | import javafx.scene.image.Image; | |
| 73 | import javafx.scene.image.ImageView; | |
| 74 | import static javafx.scene.input.KeyCode.ESCAPE; | |
| 75 | import javafx.scene.input.KeyEvent; | |
| 76 | import static javafx.scene.input.KeyEvent.CHAR_UNDEFINED; | |
| 77 | import static javafx.scene.input.KeyEvent.KEY_PRESSED; | |
| 78 | import javafx.scene.layout.BorderPane; | |
| 79 | import javafx.scene.layout.VBox; | |
| 80 | import javafx.stage.Window; | |
| 81 | import javafx.stage.WindowEvent; | |
| 82 | import static javafx.stage.WindowEvent.WINDOW_CLOSE_REQUEST; | |
| 83 | ||
| 84 | /** | |
| 85 | * Main window containing a tab pane in the center for file editors. | |
| 86 | * | |
| 87 | * @author Karl Tauber and White Magic Software, Ltd. | |
| 88 | */ | |
| 89 | public class MainWindow implements Observer { | |
| 90 | ||
| 91 | private final Options options = Services.load( Options.class ); | |
| 92 | private final Snitch snitch = Services.load( Snitch.class ); | |
| 93 | ||
| 94 | private Scene scene; | |
| 95 | private MenuBar menuBar; | |
| 96 | ||
| 97 | private DefinitionSource definitionSource; | |
| 98 | private DefinitionPane definitionPane; | |
| 99 | private FileEditorTabPane fileEditorPane; | |
| 100 | private HTMLPreviewPane previewPane; | |
| 101 | ||
| 102 | /** | |
| 103 | * Prevent re-instantiation processing classes. | |
| 104 | */ | |
| 105 | private Map<FileEditorTab, Processor<String>> processors; | |
| 106 | ||
| 107 | public MainWindow() { | |
| 108 | initLayout(); | |
| 109 | initOpenDefinitionListener(); | |
| 110 | initTabAddedListener(); | |
| 111 | initTabChangedListener(); | |
| 112 | initPreferences(); | |
| 113 | initWatchDog(); | |
| 114 | } | |
| 115 | ||
| 116 | /** | |
| 117 | * Listen for file editor tab pane to receive an open definition source event. | |
| 118 | */ | |
| 119 | private void initOpenDefinitionListener() { | |
| 120 | getFileEditorPane().onOpenDefinitionFileProperty().addListener( | |
| 121 | (ObservableValue<? extends Path> definitionFile, | |
| 122 | final Path oldPath, final Path newPath) -> { | |
| 123 | openDefinition( newPath ); | |
| 124 | setProcessors( null ); | |
| 125 | refreshSelectedTab( getActiveFileEditor() ); | |
| 126 | } | |
| 127 | ); | |
| 128 | } | |
| 129 | ||
| 130 | /** | |
| 131 | * When tabs are added, hook the various change listeners onto the new tab so | |
| 132 | * that the preview pane refreshes as necessary. | |
| 133 | */ | |
| 134 | private void initTabAddedListener() { | |
| 135 | final FileEditorTabPane editorPane = getFileEditorPane(); | |
| 136 | ||
| 137 | // Make sure the text processor kicks off when new files are opened. | |
| 138 | final ObservableList<Tab> tabs = editorPane.getTabs(); | |
| 139 | ||
| 140 | // Update the preview pane on tab changes. | |
| 141 | tabs.addListener( | |
| 142 | (final Change<? extends Tab> change) -> { | |
| 143 | while( change.next() ) { | |
| 144 | if( change.wasAdded() ) { | |
| 145 | // Multiple tabs can be added simultaneously. | |
| 146 | for( final Tab newTab : change.getAddedSubList() ) { | |
| 147 | final FileEditorTab tab = (FileEditorTab)newTab; | |
| 148 | ||
| 149 | initTextChangeListener( tab ); | |
| 150 | initCaretParagraphListener( tab ); | |
| 151 | initVariableNameInjector( tab ); | |
| 152 | } | |
| 153 | } | |
| 154 | } | |
| 155 | } | |
| 156 | ); | |
| 157 | } | |
| 158 | ||
| 159 | /** | |
| 160 | * Reloads the preferences from the previous load. | |
| 161 | */ | |
| 162 | private void initPreferences() { | |
| 163 | restoreDefinitionSource(); | |
| 164 | getFileEditorPane().restorePreferences(); | |
| 165 | updateDefinitionPane(); | |
| 166 | } | |
| 167 | ||
| 168 | /** | |
| 169 | * Listen for new tab selection events. | |
| 170 | */ | |
| 171 | private void initTabChangedListener() { | |
| 172 | final FileEditorTabPane editorPane = getFileEditorPane(); | |
| 173 | ||
| 174 | // Update the preview pane changing tabs. | |
| 175 | editorPane.addTabSelectionListener( | |
| 176 | (ObservableValue<? extends Tab> tabPane, | |
| 177 | final Tab oldTab, final Tab newTab) -> { | |
| 178 | ||
| 179 | // If there was no old tab, then this is a first time load, which | |
| 180 | // can be ignored. | |
| 181 | if( oldTab != null ) { | |
| 182 | if( newTab == null ) { | |
| 183 | closeRemainingTab(); | |
| 184 | } else { | |
| 185 | // Update the preview with the edited text. | |
| 186 | refreshSelectedTab( (FileEditorTab)newTab ); | |
| 187 | } | |
| 188 | } | |
| 189 | } | |
| 190 | ); | |
| 191 | } | |
| 192 | ||
| 193 | private void initTextChangeListener( final FileEditorTab tab ) { | |
| 194 | tab.addTextChangeListener( | |
| 195 | (ObservableValue<? extends String> editor, | |
| 196 | final String oldValue, final String newValue) -> { | |
| 197 | refreshSelectedTab( tab ); | |
| 198 | } | |
| 199 | ); | |
| 200 | } | |
| 201 | ||
| 202 | private void initCaretParagraphListener( final FileEditorTab tab ) { | |
| 203 | tab.addCaretParagraphListener( | |
| 204 | (ObservableValue<? extends Integer> editor, | |
| 205 | final Integer oldValue, final Integer newValue) -> { | |
| 206 | refreshSelectedTab( tab ); | |
| 207 | } | |
| 208 | ); | |
| 209 | } | |
| 210 | ||
| 211 | private void initVariableNameInjector( final FileEditorTab tab ) { | |
| 212 | VariableNameInjector.listen( tab, getDefinitionPane() ); | |
| 213 | } | |
| 214 | ||
| 215 | /** | |
| 216 | * Watch for changes to external files. In particular, this awaits | |
| 217 | * modifications to any XSL files associated with XML files being edited. When | |
| 218 | * an XSL file is modified (external to the application), the watchdog's ears | |
| 219 | * perk up and the file is reloaded. This keeps the XSL transformation up to | |
| 220 | * date with what's on the file system. | |
| 221 | */ | |
| 222 | private void initWatchDog() { | |
| 223 | getSnitch().addObserver( this ); | |
| 224 | } | |
| 225 | ||
| 226 | /** | |
| 227 | * Called whenever the preview pane becomes out of sync with the file editor | |
| 228 | * tab. This can be called when the text changes, the caret paragraph changes, | |
| 229 | * or the file tab changes. | |
| 230 | * | |
| 231 | * @param tab The file editor tab that has been changed in some fashion. | |
| 232 | */ | |
| 233 | private void refreshSelectedTab( final FileEditorTab tab ) { | |
| 234 | getPreviewPane().setPath( tab.getPath() ); | |
| 235 | ||
| 236 | Processor<String> processor = getProcessors().get( tab ); | |
| 237 | ||
| 238 | if( processor == null ) { | |
| 239 | processor = createProcessor( tab ); | |
| 240 | getProcessors().put( tab, processor ); | |
| 241 | } | |
| 242 | ||
| 243 | processor.processChain( tab.getEditorText() ); | |
| 244 | } | |
| 245 | ||
| 246 | /** | |
| 247 | * Returns the variable map of interpolated definitions. | |
| 248 | * | |
| 249 | * @return A map to help dereference variables. | |
| 250 | */ | |
| 251 | private Map<String, String> getResolvedMap() { | |
| 252 | return getDefinitionSource().getResolvedMap(); | |
| 253 | } | |
| 254 | ||
| 255 | /** | |
| 256 | * Returns the root node for the hierarchical definition source. | |
| 257 | * | |
| 258 | * @return Data to display in the definition pane. | |
| 259 | */ | |
| 260 | private TreeView<String> getTreeView() { | |
| 261 | try { | |
| 262 | return getDefinitionSource().asTreeView(); | |
| 263 | } catch( Exception e ) { | |
| 264 | alert( e ); | |
| 265 | } | |
| 266 | ||
| 267 | return new TreeView<>(); | |
| 268 | } | |
| 269 | ||
| 270 | /** | |
| 271 | * Called when a definition file is opened. | |
| 272 | * | |
| 273 | * @param path Path to the file that was opened. | |
| 274 | */ | |
| 275 | private void openDefinition( final Path path ) { | |
| 276 | openDefinition( path.toString() ); | |
| 277 | } | |
| 278 | ||
| 279 | /** | |
| 280 | * Called to load a definition file from its source location. | |
| 281 | * | |
| 282 | * @param path The path to the definition file that was loaded. | |
| 283 | */ | |
| 284 | private void openDefinition( final String path ) { | |
| 285 | try { | |
| 286 | final DefinitionSource ds = createDefinitionSource( path ); | |
| 287 | setDefinitionSource( ds ); | |
| 288 | storeDefinitionSource(); | |
| 289 | updateDefinitionPane(); | |
| 290 | } catch( Exception e ) { | |
| 291 | alert( e ); | |
| 292 | } | |
| 293 | } | |
| 294 | ||
| 295 | private void updateDefinitionPane() { | |
| 296 | getDefinitionPane().setRoot( getDefinitionSource().asTreeView() ); | |
| 297 | } | |
| 298 | ||
| 299 | private void restoreDefinitionSource() { | |
| 300 | final Preferences preferences = getPreferences(); | |
| 301 | final String source = preferences.get( PREFS_DEFINITION_SOURCE, null ); | |
| 302 | setDefinitionSource( createDefinitionSource( source ) ); | |
| 303 | } | |
| 304 | ||
| 305 | private void storeDefinitionSource() { | |
| 306 | final Preferences preferences = getPreferences(); | |
| 307 | final DefinitionSource ds = getDefinitionSource(); | |
| 308 | ||
| 309 | preferences.put( PREFS_DEFINITION_SOURCE, ds.toString() ); | |
| 310 | } | |
| 311 | ||
| 312 | /** | |
| 313 | * Called when the last open tab is closed. This clears out the preview pane | |
| 314 | * and the definition pane. | |
| 315 | */ | |
| 316 | private void closeRemainingTab() { | |
| 317 | getPreviewPane().clear(); | |
| 318 | getDefinitionPane().clear(); | |
| 319 | } | |
| 320 | ||
| 321 | /** | |
| 322 | * Called when an exception occurs that warrants the user's attention. | |
| 323 | * | |
| 324 | * @param e The exception with a message that the user should know about. | |
| 325 | */ | |
| 326 | private void alert( final Exception e ) { | |
| 327 | // TODO: Update the status bar. | |
| 328 | } | |
| 329 | ||
| 330 | //---- File actions ------------------------------------------------------- | |
| 331 | /** | |
| 332 | * Called when a file has been modified. | |
| 333 | * | |
| 334 | * @param snitch The watchdog file monitoring instance. | |
| 335 | * @param file The file that was modified. | |
| 336 | */ | |
| 337 | @Override | |
| 338 | public void update( final Observable snitch, final Object file ) { | |
| 339 | if( file instanceof Path ) { | |
| 340 | update( (Path)file ); | |
| 341 | } | |
| 342 | } | |
| 343 | ||
| 344 | /** | |
| 345 | * Called when a file has been modified. | |
| 346 | * | |
| 347 | * @param file Path to the modified file. | |
| 348 | */ | |
| 349 | private void update( final Path file ) { | |
| 350 | // Avoid throwing IllegalStateException by running from a non-JavaFX thread. | |
| 351 | Platform.runLater( | |
| 352 | () -> { | |
| 353 | // Brute-force XSLT file reload by re-instantiating all processors. | |
| 354 | resetProcessors(); | |
| 355 | refreshSelectedTab( getActiveFileEditor() ); | |
| 356 | } | |
| 357 | ); | |
| 358 | } | |
| 359 | ||
| 360 | /** | |
| 361 | * After resetting the processors, they will refresh anew to be up-to-date | |
| 362 | * with the files (text and definition) currently loaded into the editor. | |
| 363 | */ | |
| 364 | private void resetProcessors() { | |
| 365 | getProcessors().clear(); | |
| 366 | } | |
| 367 | ||
| 368 | //---- File actions ------------------------------------------------------- | |
| 369 | private void fileNew() { | |
| 370 | getFileEditorPane().newEditor(); | |
| 371 | } | |
| 372 | ||
| 373 | private void fileOpen() { | |
| 374 | getFileEditorPane().openFileDialog(); | |
| 375 | } | |
| 376 | ||
| 377 | private void fileClose() { | |
| 378 | getFileEditorPane().closeEditor( getActiveFileEditor(), true ); | |
| 379 | } | |
| 380 | ||
| 381 | private void fileCloseAll() { | |
| 382 | getFileEditorPane().closeAllEditors(); | |
| 383 | } | |
| 384 | ||
| 385 | private void fileSave() { | |
| 386 | getFileEditorPane().saveEditor( getActiveFileEditor() ); | |
| 387 | } | |
| 388 | ||
| 389 | private void fileSaveAll() { | |
| 390 | getFileEditorPane().saveAllEditors(); | |
| 391 | } | |
| 392 | ||
| 393 | private void fileExit() { | |
| 394 | final Window window = getWindow(); | |
| 395 | fireEvent( window, new WindowEvent( window, WINDOW_CLOSE_REQUEST ) ); | |
| 396 | } | |
| 397 | ||
| 398 | //---- Help actions ------------------------------------------------------- | |
| 399 | private void helpAbout() { | |
| 400 | Alert alert = new Alert( AlertType.INFORMATION ); | |
| 401 | alert.setTitle( get( "Dialog.about.title" ) ); | |
| 402 | alert.setHeaderText( get( "Dialog.about.header" ) ); | |
| 403 | alert.setContentText( get( "Dialog.about.content" ) ); | |
| 404 | alert.setGraphic( new ImageView( new Image( FILE_LOGO_32 ) ) ); | |
| 405 | alert.initOwner( getWindow() ); | |
| 406 | ||
| 407 | alert.showAndWait(); | |
| 408 | } | |
| 409 | ||
| 410 | //---- Convenience accessors ---------------------------------------------- | |
| 411 | private float getFloat( final String key, final float defaultValue ) { | |
| 412 | return getPreferences().getFloat( key, defaultValue ); | |
| 413 | } | |
| 414 | ||
| 415 | private Preferences getPreferences() { | |
| 416 | return getOptions().getState(); | |
| 417 | } | |
| 418 | ||
| 419 | private Window getWindow() { | |
| 420 | return getScene().getWindow(); | |
| 421 | } | |
| 422 | ||
| 423 | private MarkdownEditorPane getActiveEditor() { | |
| 424 | final EditorPane pane = getActiveFileEditor().getEditorPane(); | |
| 425 | ||
| 426 | return pane instanceof MarkdownEditorPane ? (MarkdownEditorPane)pane : null; | |
| 427 | } | |
| 428 | ||
| 429 | private FileEditorTab getActiveFileEditor() { | |
| 430 | return getFileEditorPane().getActiveFileEditor(); | |
| 431 | } | |
| 432 | ||
| 433 | //---- Member accessors --------------------------------------------------- | |
| 434 | private void setScene( Scene scene ) { | |
| 435 | this.scene = scene; | |
| 436 | } | |
| 437 | ||
| 438 | public Scene getScene() { | |
| 439 | return this.scene; | |
| 440 | } | |
| 441 | ||
| 442 | private void setProcessors( final Map<FileEditorTab, Processor<String>> map ) { | |
| 443 | this.processors = map; | |
| 444 | } | |
| 445 | ||
| 446 | private Map<FileEditorTab, Processor<String>> getProcessors() { | |
| 447 | if( this.processors == null ) { | |
| 448 | setProcessors( new HashMap<>() ); | |
| 449 | } | |
| 450 | ||
| 451 | return this.processors; | |
| 452 | } | |
| 453 | ||
| 454 | private FileEditorTabPane getFileEditorPane() { | |
| 455 | if( this.fileEditorPane == null ) { | |
| 456 | this.fileEditorPane = createFileEditorPane(); | |
| 457 | } | |
| 458 | ||
| 459 | return this.fileEditorPane; | |
| 460 | } | |
| 461 | ||
| 462 | private HTMLPreviewPane getPreviewPane() { | |
| 463 | if( this.previewPane == null ) { | |
| 464 | this.previewPane = createPreviewPane(); | |
| 465 | } | |
| 466 | ||
| 467 | return this.previewPane; | |
| 468 | } | |
| 469 | ||
| 470 | private void setDefinitionSource( final DefinitionSource definitionSource ) { | |
| 471 | this.definitionSource = definitionSource; | |
| 472 | } | |
| 473 | ||
| 474 | private DefinitionSource getDefinitionSource() { | |
| 475 | if( this.definitionSource == null ) { | |
| 476 | this.definitionSource = new EmptyDefinitionSource(); | |
| 477 | } | |
| 478 | ||
| 479 | return this.definitionSource; | |
| 480 | } | |
| 481 | ||
| 482 | private DefinitionPane getDefinitionPane() { | |
| 483 | if( this.definitionPane == null ) { | |
| 484 | this.definitionPane = createDefinitionPane(); | |
| 485 | } | |
| 486 | ||
| 487 | return this.definitionPane; | |
| 488 | } | |
| 489 | ||
| 490 | private Options getOptions() { | |
| 491 | return this.options; | |
| 492 | } | |
| 493 | ||
| 494 | private Snitch getSnitch() { | |
| 495 | return this.snitch; | |
| 496 | } | |
| 497 | ||
| 498 | public void setMenuBar( MenuBar menuBar ) { | |
| 499 | this.menuBar = menuBar; | |
| 500 | } | |
| 501 | ||
| 502 | public MenuBar getMenuBar() { | |
| 503 | return this.menuBar; | |
| 504 | } | |
| 505 | ||
| 506 | //---- Member creators ---------------------------------------------------- | |
| 507 | /** | |
| 508 | * Factory to create processors that are suited to different file types. | |
| 509 | * | |
| 510 | * @param tab The tab that is subjected to processing. | |
| 511 | * | |
| 512 | * @return A processor suited to the file type specified by the tab's path. | |
| 513 | */ | |
| 514 | private Processor<String> createProcessor( final FileEditorTab tab ) { | |
| 515 | return createProcessorFactory().createProcessor( tab ); | |
| 516 | } | |
| 517 | ||
| 518 | private ProcessorFactory createProcessorFactory() { | |
| 519 | return new ProcessorFactory( getPreviewPane(), getResolvedMap() ); | |
| 520 | } | |
| 521 | ||
| 522 | private DefinitionSource createDefinitionSource( final String path ) { | |
| 523 | return createDefinitionFactory().createDefinitionSource( path ); | |
| 524 | } | |
| 525 | ||
| 526 | /** | |
| 527 | * Create an editor pane to hold file editor tabs. | |
| 528 | * | |
| 529 | * @return A new instance, never null. | |
| 530 | */ | |
| 531 | private FileEditorTabPane createFileEditorPane() { | |
| 532 | return new FileEditorTabPane(); | |
| 533 | } | |
| 534 | ||
| 535 | private HTMLPreviewPane createPreviewPane() { | |
| 536 | return new HTMLPreviewPane(); | |
| 537 | } | |
| 538 | ||
| 539 | private DefinitionPane createDefinitionPane() { | |
| 540 | return new DefinitionPane( getTreeView() ); | |
| 541 | } | |
| 542 | ||
| 543 | private DefinitionFactory createDefinitionFactory() { | |
| 544 | return new DefinitionFactory(); | |
| 545 | } | |
| 546 | ||
| 547 | private Node createMenuBar() { | |
| 548 | final BooleanBinding activeFileEditorIsNull = getFileEditorPane().activeFileEditorProperty().isNull(); | |
| 549 | ||
| 550 | // File actions | |
| 551 | Action fileNewAction = new Action( get( "Main.menu.file.new" ), "Shortcut+N", FILE_ALT, e -> fileNew() ); | |
| 552 | Action fileOpenAction = new Action( get( "Main.menu.file.open" ), "Shortcut+O", FOLDER_OPEN_ALT, e -> fileOpen() ); | |
| 553 | Action fileCloseAction = new Action( get( "Main.menu.file.close" ), "Shortcut+W", null, e -> fileClose(), activeFileEditorIsNull ); | |
| 554 | Action fileCloseAllAction = new Action( get( "Main.menu.file.close_all" ), null, null, e -> fileCloseAll(), activeFileEditorIsNull ); | |
| 555 | Action fileSaveAction = new Action( get( "Main.menu.file.save" ), "Shortcut+S", FLOPPY_ALT, e -> fileSave(), | |
| 556 | createActiveBooleanProperty( FileEditorTab::modifiedProperty ).not() ); | |
| 557 | Action fileSaveAllAction = new Action( get( "Main.menu.file.save_all" ), "Shortcut+Shift+S", null, e -> fileSaveAll(), | |
| 558 | Bindings.not( getFileEditorPane().anyFileEditorModifiedProperty() ) ); | |
| 559 | Action fileExitAction = new Action( get( "Main.menu.file.exit" ), null, null, e -> fileExit() ); | |
| 560 | ||
| 561 | // Edit actions | |
| 562 | Action editUndoAction = new Action( get( "Main.menu.edit.undo" ), "Shortcut+Z", UNDO, | |
| 563 | e -> getActiveEditor().undo(), | |
| 564 | createActiveBooleanProperty( FileEditorTab::canUndoProperty ).not() ); | |
| 565 | Action editRedoAction = new Action( get( "Main.menu.edit.redo" ), "Shortcut+Y", REPEAT, | |
| 566 | e -> getActiveEditor().redo(), | |
| 567 | createActiveBooleanProperty( FileEditorTab::canRedoProperty ).not() ); | |
| 568 | ||
| 569 | // Insert actions | |
| 570 | Action insertBoldAction = new Action( get( "Main.menu.insert.bold" ), "Shortcut+B", BOLD, | |
| 571 | e -> getActiveEditor().surroundSelection( "**", "**" ), | |
| 572 | activeFileEditorIsNull ); | |
| 573 | Action insertItalicAction = new Action( get( "Main.menu.insert.italic" ), "Shortcut+I", ITALIC, | |
| 574 | e -> getActiveEditor().surroundSelection( "*", "*" ), | |
| 575 | activeFileEditorIsNull ); | |
| 576 | Action insertStrikethroughAction = new Action( get( "Main.menu.insert.strikethrough" ), "Shortcut+T", STRIKETHROUGH, | |
| 577 | e -> getActiveEditor().surroundSelection( "~~", "~~" ), | |
| 578 | activeFileEditorIsNull ); | |
| 579 | Action insertBlockquoteAction = new Action( get( "Main.menu.insert.blockquote" ), "Ctrl+Q", QUOTE_LEFT, // not Shortcut+Q because of conflict on Mac | |
| 580 | e -> getActiveEditor().surroundSelection( "\n\n> ", "" ), | |
| 581 | activeFileEditorIsNull ); | |
| 582 | Action insertCodeAction = new Action( get( "Main.menu.insert.code" ), "Shortcut+K", CODE, | |
| 583 | e -> getActiveEditor().surroundSelection( "`", "`" ), | |
| 584 | activeFileEditorIsNull ); | |
| 585 | Action insertFencedCodeBlockAction = new Action( get( "Main.menu.insert.fenced_code_block" ), "Shortcut+Shift+K", FILE_CODE_ALT, | |
| 586 | e -> getActiveEditor().surroundSelection( "\n\n```\n", "\n```\n\n", get( "Main.menu.insert.fenced_code_block.prompt" ) ), | |
| 587 | activeFileEditorIsNull ); | |
| 588 | ||
| 589 | Action insertLinkAction = new Action( get( "Main.menu.insert.link" ), "Shortcut+L", LINK, | |
| 590 | e -> getActiveEditor().insertLink(), | |
| 591 | activeFileEditorIsNull ); | |
| 592 | Action insertImageAction = new Action( get( "Main.menu.insert.image" ), "Shortcut+G", PICTURE_ALT, | |
| 593 | e -> getActiveEditor().insertImage(), | |
| 594 | activeFileEditorIsNull ); | |
| 595 | ||
| 596 | final Action[] headers = new Action[ 6 ]; | |
| 597 | ||
| 598 | // Insert header actions (H1 ... H6) | |
| 599 | for( int i = 1; i <= 6; i++ ) { | |
| 600 | final String hashes = new String( new char[ i ] ).replace( "\0", "#" ); | |
| 601 | final String markup = String.format( "%n%n%s ", hashes ); | |
| 602 | final String text = get( "Main.menu.insert.header_" + i ); | |
| 603 | final String accelerator = "Shortcut+" + i; | |
| 604 | final String prompt = get( "Main.menu.insert.header_" + i + ".prompt" ); | |
| 605 | ||
| 606 | headers[ i - 1 ] = new Action( text, accelerator, HEADER, | |
| 607 | e -> getActiveEditor().surroundSelection( markup, "", prompt ), | |
| 608 | activeFileEditorIsNull ); | |
| 609 | } | |
| 610 | ||
| 611 | Action insertUnorderedListAction = new Action( get( "Main.menu.insert.unordered_list" ), "Shortcut+U", LIST_UL, | |
| 612 | e -> getActiveEditor().surroundSelection( "\n\n* ", "" ), | |
| 613 | activeFileEditorIsNull ); | |
| 614 | Action insertOrderedListAction = new Action( get( "Main.menu.insert.ordered_list" ), "Shortcut+Shift+O", LIST_OL, | |
| 615 | e -> getActiveEditor().surroundSelection( "\n\n1. ", "" ), | |
| 616 | activeFileEditorIsNull ); | |
| 617 | Action insertHorizontalRuleAction = new Action( get( "Main.menu.insert.horizontal_rule" ), "Shortcut+H", null, | |
| 618 | e -> getActiveEditor().surroundSelection( "\n\n---\n\n", "" ), | |
| 619 | activeFileEditorIsNull ); | |
| 620 | ||
| 621 | // Help actions | |
| 622 | Action helpAboutAction = new Action( get( "Main.menu.help.about" ), null, null, e -> helpAbout() ); | |
| 623 | ||
| 624 | //---- MenuBar ---- | |
| 625 | Menu fileMenu = ActionUtils.createMenu( get( "Main.menu.file" ), | |
| 626 | fileNewAction, | |
| 627 | fileOpenAction, | |
| 628 | null, | |
| 629 | fileCloseAction, | |
| 630 | fileCloseAllAction, | |
| 631 | null, | |
| 632 | fileSaveAction, | |
| 633 | fileSaveAllAction, | |
| 634 | null, | |
| 635 | fileExitAction ); | |
| 636 | ||
| 637 | Menu editMenu = ActionUtils.createMenu( get( "Main.menu.edit" ), | |
| 638 | editUndoAction, | |
| 639 | editRedoAction ); | |
| 640 | ||
| 641 | Menu insertMenu = ActionUtils.createMenu( get( "Main.menu.insert" ), | |
| 642 | insertBoldAction, | |
| 643 | insertItalicAction, | |
| 644 | insertStrikethroughAction, | |
| 645 | insertBlockquoteAction, | |
| 646 | insertCodeAction, | |
| 647 | insertFencedCodeBlockAction, | |
| 648 | null, | |
| 649 | insertLinkAction, | |
| 650 | insertImageAction, | |
| 651 | null, | |
| 652 | headers[ 0 ], | |
| 653 | headers[ 1 ], | |
| 654 | headers[ 2 ], | |
| 655 | headers[ 3 ], | |
| 656 | headers[ 4 ], | |
| 657 | headers[ 5 ], | |
| 658 | null, | |
| 659 | insertUnorderedListAction, | |
| 660 | insertOrderedListAction, | |
| 661 | insertHorizontalRuleAction ); | |
| 662 | ||
| 663 | Menu helpMenu = ActionUtils.createMenu( get( "Main.menu.help" ), | |
| 664 | helpAboutAction ); | |
| 665 | ||
| 666 | menuBar = new MenuBar( fileMenu, editMenu, insertMenu, helpMenu ); | |
| 667 | ||
| 668 | //---- ToolBar ---- | |
| 669 | ToolBar toolBar = ActionUtils.createToolBar( | |
| 670 | fileNewAction, | |
| 671 | fileOpenAction, | |
| 672 | fileSaveAction, | |
| 673 | null, | |
| 674 | editUndoAction, | |
| 675 | editRedoAction, | |
| 676 | null, | |
| 677 | insertBoldAction, | |
| 678 | insertItalicAction, | |
| 679 | insertBlockquoteAction, | |
| 680 | insertCodeAction, | |
| 681 | insertFencedCodeBlockAction, | |
| 682 | null, | |
| 683 | insertLinkAction, | |
| 684 | insertImageAction, | |
| 685 | null, | |
| 686 | headers[ 0 ], | |
| 687 | null, | |
| 688 | insertUnorderedListAction, | |
| 689 | insertOrderedListAction ); | |
| 690 | ||
| 691 | return new VBox( menuBar, toolBar ); | |
| 692 | } | |
| 693 | ||
| 694 | /** | |
| 695 | * Creates a boolean property that is bound to another boolean value of the | |
| 696 | * active editor. | |
| 697 | */ | |
| 698 | private BooleanProperty createActiveBooleanProperty( | |
| 699 | final Function<FileEditorTab, ObservableBooleanValue> func ) { | |
| 700 | ||
| 701 | final BooleanProperty b = new SimpleBooleanProperty(); | |
| 702 | final FileEditorTab tab = getActiveFileEditor(); | |
| 703 | ||
| 704 | if( tab != null ) { | |
| 705 | b.bind( func.apply( tab ) ); | |
| 706 | } | |
| 707 | ||
| 708 | getFileEditorPane().activeFileEditorProperty().addListener( | |
| 709 | (observable, oldFileEditor, newFileEditor) -> { | |
| 710 | b.unbind(); | |
| 711 | ||
| 712 | if( newFileEditor != null ) { | |
| 713 | b.bind( func.apply( newFileEditor ) ); | |
| 714 | } else { | |
| 715 | b.set( false ); | |
| 716 | } | |
| 717 | } | |
| 718 | ); | |
| 719 | ||
| 720 | return b; | |
| 721 | } | |
| 722 | ||
| 723 | private void initLayout() { | |
| 724 | final SplitPane splitPane = new SplitPane( | |
| 725 | getDefinitionPane().getNode(), | |
| 726 | getFileEditorPane().getNode(), | |
| 727 | getPreviewPane().getNode() ); | |
| 728 | ||
| 729 | splitPane.setDividerPositions( | |
| 730 | getFloat( K_PANE_SPLIT_DEFINITION, .10f ), | |
| 731 | getFloat( K_PANE_SPLIT_EDITOR, .45f ), | |
| 732 | getFloat( K_PANE_SPLIT_PREVIEW, .45f ) ); | |
| 733 | ||
| 734 | // See: http://broadlyapplicable.blogspot.ca/2015/03/javafx-capture-restorePreferences-splitpane.html | |
| 735 | final BorderPane borderPane = new BorderPane(); | |
| 736 | borderPane.setPrefSize( 1024, 800 ); | |
| 737 | borderPane.setTop( createMenuBar() ); | |
| 738 | borderPane.setCenter( splitPane ); | |
| 739 | ||
| 723 | 740 | final Scene appScene = new Scene( borderPane ); |
| 724 | 741 | setScene( appScene ); |
| 60 | 60 | /** |
| 61 | 61 | * |
| 62 | * @param path | |
| 62 | * @param path Path to a file containing definitions. | |
| 63 | 63 | * @return |
| 64 | 64 | */ |
| 28 | 28 | package com.scrivenvar.definition; |
| 29 | 29 | |
| 30 | import java.io.IOException; | |
| 31 | 30 | import java.util.Map; |
| 32 | 31 | import javafx.scene.control.TreeView; |
| ... | ||
| 45 | 44 | * |
| 46 | 45 | * @return A hierarchical tree suitable for displaying in the definition pane. |
| 47 | * | |
| 48 | * @throws IOException Could not obtain the definition source data. | |
| 49 | 46 | */ |
| 50 | public TreeView<String> asTreeView() throws IOException; | |
| 47 | public TreeView<String> asTreeView(); | |
| 51 | 48 | |
| 52 | 49 | /** |
| ... | ||
| 62 | 59 | * Must return a re-loadable path to the data source. For a file, this is the |
| 63 | 60 | * absolute file path. For a database, this could be the JDBC connection. For |
| 64 | * a web site, this might be the GET url. | |
| 61 | * a web site, this might be the GET URL. | |
| 65 | 62 | * |
| 66 | 63 | * @return A non-null, non-empty string. |
| 28 | 28 | package com.scrivenvar.definition; |
| 29 | 29 | |
| 30 | import java.io.IOException; | |
| 31 | 30 | import java.util.HashMap; |
| 32 | 31 | import java.util.Map; |
| ... | ||
| 44 | 43 | |
| 45 | 44 | @Override |
| 46 | public TreeView<String> asTreeView() throws IOException { | |
| 45 | public TreeView<String> asTreeView() { | |
| 47 | 46 | return new TreeView<>(); |
| 48 | 47 | } |
| 49 | 48 | |
| 50 | 49 | @Override |
| 51 | 50 | public Map<String, String> getResolvedMap() { |
| 52 | 51 | return new HashMap<>(); |
| 53 | 52 | } |
| 54 | ||
| 55 | 53 | } |
| 56 | 54 | |
| 29 | 29 | |
| 30 | 30 | import static com.scrivenvar.Messages.get; |
| 31 | import java.io.IOException; | |
| 32 | 31 | import java.io.InputStream; |
| 33 | 32 | import java.nio.file.Files; |
| ... | ||
| 45 | 44 | private YamlTreeAdapter yamlTreeAdapter; |
| 46 | 45 | private YamlParser yamlParser; |
| 46 | private TreeView<String> treeView; | |
| 47 | 47 | |
| 48 | 48 | /** |
| 49 | 49 | * Constructs a new YAML definition source, populated from the given file. |
| 50 | 50 | * |
| 51 | 51 | * @param path Path to the YAML definition file. |
| 52 | 52 | */ |
| 53 | 53 | public YamlFileDefinitionSource( final Path path ) { |
| 54 | 54 | super( path ); |
| 55 | init(); | |
| 56 | } | |
| 57 | ||
| 58 | private void init() { | |
| 59 | setYamlParser( createYamlParser() ); | |
| 55 | 60 | } |
| 56 | 61 | |
| 57 | 62 | /** |
| 58 | 63 | * TODO: Associate variable file with path to current file. |
| 59 | 64 | * |
| 60 | 65 | * @return The TreeView for this definition source. |
| 61 | * | |
| 62 | * @throws IOException | |
| 63 | 66 | */ |
| 64 | 67 | @Override |
| 65 | public TreeView<String> asTreeView() throws IOException { | |
| 66 | ||
| 67 | try( final InputStream in = Files.newInputStream( getPath() ) ) { | |
| 68 | return getYamlTreeAdapter().adapt( | |
| 69 | in, | |
| 70 | get( "Pane.defintion.node.root.title" ) | |
| 71 | ); | |
| 68 | public TreeView<String> asTreeView() { | |
| 69 | if( this.treeView == null ) { | |
| 70 | this.treeView = createTreeView(); | |
| 72 | 71 | } |
| 72 | ||
| 73 | return this.treeView; | |
| 73 | 74 | } |
| 74 | 75 | |
| ... | ||
| 92 | 93 | private YamlParser getYamlParser() { |
| 93 | 94 | if( this.yamlParser == null ) { |
| 94 | setYamlParser( new YamlParser() ); | |
| 95 | setYamlParser( createYamlParser() ); | |
| 95 | 96 | } |
| 96 | 97 | |
| 97 | 98 | return this.yamlParser; |
| 98 | 99 | } |
| 99 | 100 | |
| 100 | 101 | private void setYamlParser( final YamlParser yamlParser ) { |
| 101 | 102 | this.yamlParser = yamlParser; |
| 103 | } | |
| 104 | ||
| 105 | private YamlParser createYamlParser() { | |
| 106 | try( final InputStream in = Files.newInputStream( getPath() ) ) { | |
| 107 | return new YamlParser( in ); | |
| 108 | } catch( final Exception e ) { | |
| 109 | throw new RuntimeException( e ); | |
| 110 | } | |
| 111 | } | |
| 112 | ||
| 113 | private TreeView<String> createTreeView() { | |
| 114 | return getYamlTreeAdapter().adapt( | |
| 115 | get( "Pane.defintion.node.root.title" ) | |
| 116 | ); | |
| 102 | 117 | } |
| 103 | 118 | } |
| 79 | 79 | * @author White Magic Software, Ltd. |
| 80 | 80 | */ |
| 81 | public class YamlParser { | |
| 82 | ||
| 83 | /** | |
| 84 | * Separates YAML variable nodes (e.g., the dots in | |
| 85 | * <code>$root.node.var$</code>). | |
| 86 | */ | |
| 87 | public static final String SEPARATOR = "."; | |
| 88 | public static final char SEPARATOR_CHAR = SEPARATOR.charAt( 0 ); | |
| 89 | ||
| 90 | private final static int GROUP_DELIMITED = 1; | |
| 91 | private final static int GROUP_REFERENCE = 2; | |
| 92 | ||
| 93 | private final static VariableDecorator VARIABLE_DECORATOR | |
| 94 | = new YamlVariableDecorator(); | |
| 95 | ||
| 96 | /** | |
| 97 | * Compiled version of DEFAULT_REGEX. | |
| 98 | */ | |
| 99 | private final static Pattern REGEX_PATTERN | |
| 100 | = Pattern.compile( YamlVariableDecorator.REGEX ); | |
| 101 | ||
| 102 | /** | |
| 103 | * Should be JsonPointer.SEPARATOR, but Jackson YAML uses magic values. | |
| 104 | */ | |
| 105 | private final static char SEPARATOR_YAML = '/'; | |
| 106 | ||
| 107 | /** | |
| 108 | * Start of the Universe (the YAML document node that contains all others). | |
| 109 | */ | |
| 110 | private ObjectNode documentRoot; | |
| 111 | ||
| 112 | /** | |
| 113 | * Map of references to dereferenced field values. | |
| 114 | */ | |
| 115 | private Map<String, String> references; | |
| 116 | ||
| 117 | public YamlParser() { | |
| 118 | } | |
| 119 | ||
| 120 | /** | |
| 121 | * Returns the given string with all the delimited references swapped with | |
| 122 | * their recursively resolved values. | |
| 123 | * | |
| 124 | * @param text The text to parse with zero or more delimited references to | |
| 125 | * replace. | |
| 126 | * | |
| 127 | * @return The substituted value. | |
| 128 | */ | |
| 129 | public String substitute( String text ) { | |
| 130 | final Matcher matcher = patternMatch( text ); | |
| 131 | final Map<String, String> map = getReferences(); | |
| 132 | ||
| 133 | while( matcher.find() ) { | |
| 134 | final String key = matcher.group( GROUP_DELIMITED ); | |
| 135 | final String value = map.get( key ); | |
| 136 | ||
| 137 | if( value == null ) { | |
| 138 | missing( text ); | |
| 139 | } else { | |
| 140 | text = text.replace( key, value ); | |
| 141 | } | |
| 142 | } | |
| 143 | ||
| 144 | return text; | |
| 145 | } | |
| 146 | ||
| 147 | /** | |
| 148 | * Returns all the strings with their values resolved in a flat hierarchy. | |
| 149 | * This copies all the keys and resolved values into a new map. | |
| 150 | * | |
| 151 | * @return The new map created with all values having been resolved, | |
| 152 | * recursively. | |
| 153 | */ | |
| 154 | public Map<String, String> createResolvedMap() { | |
| 155 | final Map<String, String> map = new HashMap<>( 1024 ); | |
| 156 | ||
| 157 | resolve( getDocumentRoot(), "", map ); | |
| 158 | ||
| 159 | return map; | |
| 160 | } | |
| 161 | ||
| 162 | /** | |
| 163 | * Iterate over a given root node (at any level of the tree) and adapt each | |
| 164 | * leaf node. | |
| 165 | * | |
| 166 | * @param rootNode A JSON node (YAML node) to adapt. | |
| 167 | */ | |
| 168 | private void resolve( | |
| 169 | final JsonNode rootNode, final String path, final Map<String, String> map ) { | |
| 170 | ||
| 171 | if( rootNode != null ) { | |
| 172 | rootNode.fields().forEachRemaining( | |
| 173 | (Entry<String, JsonNode> leaf) -> resolve( leaf, path, map ) | |
| 174 | ); | |
| 175 | } | |
| 176 | } | |
| 177 | ||
| 178 | /** | |
| 179 | * Recursively adapt each rootNode to a corresponding rootItem. | |
| 180 | * | |
| 181 | * @param rootNode The node to adapt. | |
| 182 | */ | |
| 183 | private void resolve( | |
| 184 | final Entry<String, JsonNode> rootNode, | |
| 185 | final String path, | |
| 186 | final Map<String, String> map ) { | |
| 187 | ||
| 188 | final JsonNode leafNode = rootNode.getValue(); | |
| 189 | final String key = rootNode.getKey(); | |
| 190 | ||
| 191 | if( leafNode.isValueNode() ) { | |
| 192 | final String value = rootNode.getValue().asText(); | |
| 193 | ||
| 194 | map.put( VARIABLE_DECORATOR.decorate( path + key ), substitute( value ) ); | |
| 195 | } | |
| 196 | ||
| 197 | if( leafNode.isObject() ) { | |
| 198 | resolve( leafNode, path + key + SEPARATOR, map ); | |
| 199 | } | |
| 200 | } | |
| 201 | ||
| 202 | /** | |
| 203 | * Reads the first document from the given stream of YAML data and returns a | |
| 204 | * corresponding object that represents the YAML hierarchy. The calling class | |
| 205 | * is responsible for closing the stream. Calling classes should use | |
| 206 | * <code>JsonNode.fields()</code> to walk through the YAML tree of fields. | |
| 207 | * | |
| 208 | * @param in The input stream containing YAML content. | |
| 209 | * | |
| 210 | * @return An object hierarchy to represent the content. | |
| 211 | * | |
| 212 | * @throws IOException Could not read the stream. | |
| 213 | */ | |
| 214 | public JsonNode process( final InputStream in ) throws IOException { | |
| 215 | ||
| 216 | final ObjectNode root = (ObjectNode)getObjectMapper().readTree( in ); | |
| 217 | setDocumentRoot( root ); | |
| 218 | process( root ); | |
| 219 | return getDocumentRoot(); | |
| 220 | } | |
| 221 | ||
| 222 | /** | |
| 223 | * Iterate over a given root node (at any level of the tree) and process each | |
| 224 | * leaf node. | |
| 225 | * | |
| 226 | * @param root A node to process. | |
| 227 | */ | |
| 228 | private void process( final JsonNode root ) { | |
| 229 | root.fields().forEachRemaining( this::process ); | |
| 230 | } | |
| 231 | ||
| 232 | /** | |
| 233 | * Process the given field, which is a named node. This is where the | |
| 234 | * application does the up-front work of mapping references to their fully | |
| 235 | * recursively dereferenced values. | |
| 236 | * | |
| 237 | * @param field The named node. | |
| 238 | */ | |
| 239 | private void process( final Entry<String, JsonNode> field ) { | |
| 240 | final JsonNode node = field.getValue(); | |
| 241 | ||
| 242 | if( node.isObject() ) { | |
| 243 | process( node ); | |
| 244 | } else { | |
| 245 | final JsonNode fieldValue = field.getValue(); | |
| 246 | ||
| 247 | // Only basic data types can be parsed into variable values. For | |
| 248 | // node structures, YAML has a built-in mechanism. | |
| 249 | if( fieldValue.isValueNode() ) { | |
| 250 | try { | |
| 251 | resolve( fieldValue.asText() ); | |
| 252 | } catch( StackOverflowError e ) { | |
| 253 | throw new IllegalArgumentException( | |
| 254 | "Unresolvable: " + node.textValue() + " = " + fieldValue ); | |
| 255 | } | |
| 256 | } | |
| 257 | } | |
| 258 | } | |
| 259 | ||
| 260 | /** | |
| 261 | * Inserts the delimited references and field values into the cache. This will | |
| 262 | * overwrite existing references. | |
| 263 | * | |
| 264 | * @param fieldValue YAML field containing zero or more delimited references. | |
| 265 | * If it contains a delimited reference, the parameter is modified with the | |
| 266 | * dereferenced value before it is returned. | |
| 267 | * | |
| 268 | * @return fieldValue without delimited references. | |
| 269 | */ | |
| 270 | private String resolve( String fieldValue ) { | |
| 271 | final Matcher matcher = patternMatch( fieldValue ); | |
| 272 | ||
| 273 | while( matcher.find() ) { | |
| 274 | final String delimited = matcher.group( GROUP_DELIMITED ); | |
| 275 | final String reference = matcher.group( GROUP_REFERENCE ); | |
| 276 | final String dereference = resolve( lookup( reference ) ); | |
| 277 | ||
| 278 | fieldValue = fieldValue.replace( delimited, dereference ); | |
| 279 | ||
| 280 | // This will perform some superfluous calls by overwriting existing | |
| 281 | // items in the delimited reference map. | |
| 282 | put( delimited, dereference ); | |
| 283 | } | |
| 284 | ||
| 285 | return fieldValue; | |
| 286 | } | |
| 287 | ||
| 288 | /** | |
| 289 | * Inserts a key/value pair into the references map. The map retains | |
| 290 | * references and dereferenced values found in the YAML. If the reference | |
| 291 | * already exists, this will overwrite with a new value. | |
| 292 | * | |
| 293 | * @param delimited The variable name. | |
| 294 | * @param dereferenced The resolved value. | |
| 295 | */ | |
| 296 | private void put( String delimited, String dereferenced ) { | |
| 297 | if( dereferenced.isEmpty() ) { | |
| 298 | missing( delimited ); | |
| 299 | } else { | |
| 300 | getReferences().put( delimited, dereferenced ); | |
| 301 | } | |
| 302 | } | |
| 303 | ||
| 304 | /** | |
| 305 | * Writes the modified YAML document to standard output. | |
| 306 | */ | |
| 307 | private void writeDocument() throws IOException { | |
| 308 | getObjectMapper().writeValue( System.out, getDocumentRoot() ); | |
| 309 | } | |
| 310 | ||
| 311 | /** | |
| 312 | * Called when a delimited reference is dereferenced to an empty string. This | |
| 313 | * should produce a warning for the user. | |
| 314 | * | |
| 315 | * @param delimited Delimited reference with no derived value. | |
| 316 | */ | |
| 317 | private void missing( final String delimited ) { | |
| 318 | throw new InvalidParameterException( | |
| 319 | MessageFormat.format( "Missing value for '{0}'.", delimited ) ); | |
| 320 | } | |
| 321 | ||
| 322 | /** | |
| 323 | * Returns a REGEX_PATTERN matcher for the given text. | |
| 324 | * | |
| 325 | * @param text The text that contains zero or more instances of a | |
| 326 | * REGEX_PATTERN that can be found using the regular expression. | |
| 327 | */ | |
| 328 | private Matcher patternMatch( String text ) { | |
| 329 | return getPattern().matcher( text ); | |
| 330 | } | |
| 331 | ||
| 332 | /** | |
| 333 | * Finds the YAML value for a reference. | |
| 334 | * | |
| 335 | * @param reference References a value in the YAML document. | |
| 336 | * | |
| 337 | * @return The dereferenced value. | |
| 338 | */ | |
| 339 | private String lookup( final String reference ) { | |
| 340 | return getDocumentRoot().at( asPath( reference ) ).asText(); | |
| 341 | } | |
| 342 | ||
| 343 | /** | |
| 344 | * Converts a reference (not delimited) to a path that can be used to find a | |
| 345 | * value that should exist inside the YAML document. | |
| 346 | * | |
| 347 | * @param reference The reference to convert to a YAML document path. | |
| 348 | * | |
| 349 | * @return The reference with a leading slash and its separator characters | |
| 350 | * converted to slashes. | |
| 351 | */ | |
| 352 | private String asPath( final String reference ) { | |
| 353 | return SEPARATOR_YAML + reference.replace( getDelimitedSeparator(), SEPARATOR_YAML ); | |
| 354 | } | |
| 355 | ||
| 356 | /** | |
| 357 | * Sets the parent node for the entire YAML document tree. | |
| 358 | * | |
| 359 | * @param documentRoot The parent node. | |
| 360 | */ | |
| 361 | private void setDocumentRoot( ObjectNode documentRoot ) { | |
| 362 | this.documentRoot = documentRoot; | |
| 363 | } | |
| 364 | ||
| 365 | /** | |
| 366 | * Returns the parent node for the entire YAML document tree. | |
| 367 | * | |
| 368 | * @return The parent node. | |
| 369 | */ | |
| 370 | private ObjectNode getDocumentRoot() { | |
| 371 | return this.documentRoot; | |
| 372 | } | |
| 373 | ||
| 374 | /** | |
| 375 | * Returns the compiled regular expression REGEX_PATTERN used to match | |
| 376 | * delimited references. | |
| 377 | * | |
| 378 | * @return A compiled regex for use with the Matcher. | |
| 379 | */ | |
| 380 | private Pattern getPattern() { | |
| 381 | return REGEX_PATTERN; | |
| 382 | } | |
| 383 | ||
| 384 | /** | |
| 385 | * Returns the list of references mapped to dereferenced values. | |
| 386 | * | |
| 387 | * @return | |
| 388 | */ | |
| 389 | private Map<String, String> getReferences() { | |
| 390 | if( this.references == null ) { | |
| 391 | this.references = createReferences(); | |
| 392 | } | |
| 393 | ||
| 394 | return this.references; | |
| 395 | } | |
| 396 | ||
| 397 | /** | |
| 398 | * Subclasses can override this method to insert their own map. | |
| 399 | * | |
| 400 | * @return An empty HashMap, never null. | |
| 401 | */ | |
| 402 | protected Map<String, String> createReferences() { | |
| 403 | return new HashMap<>(); | |
| 404 | } | |
| 405 | ||
| 406 | private final class ResolverYAMLFactory extends YAMLFactory { | |
| 407 | ||
| 408 | private static final long serialVersionUID = 1L; | |
| 409 | ||
| 410 | @Override | |
| 411 | protected YAMLGenerator _createGenerator( | |
| 412 | final Writer out, final IOContext ctxt ) throws IOException { | |
| 413 | ||
| 414 | return new ResolverYAMLGenerator( | |
| 415 | ctxt, _generatorFeatures, _yamlGeneratorFeatures, _objectCodec, | |
| 416 | out, _version ); | |
| 417 | } | |
| 418 | } | |
| 419 | ||
| 420 | private class ResolverYAMLGenerator extends YAMLGenerator { | |
| 421 | ||
| 422 | public ResolverYAMLGenerator( | |
| 423 | final IOContext ctxt, | |
| 424 | final int jsonFeatures, | |
| 425 | final int yamlFeatures, | |
| 426 | final ObjectCodec codec, | |
| 427 | final Writer out, | |
| 428 | final DumperOptions.Version version ) throws IOException { | |
| 429 | ||
| 430 | super( ctxt, jsonFeatures, yamlFeatures, codec, out, version ); | |
| 431 | } | |
| 432 | ||
| 433 | @Override | |
| 434 | public void writeString( final String text ) | |
| 435 | throws IOException, JsonGenerationException { | |
| 436 | super.writeString( substitute( text ) ); | |
| 437 | } | |
| 438 | } | |
| 439 | ||
| 440 | private YAMLFactory getYAMLFactory() { | |
| 441 | return new ResolverYAMLFactory(); | |
| 442 | } | |
| 443 | ||
| 81 | public class YamlParser { | |
| 82 | ||
| 83 | /** | |
| 84 | * Separates YAML variable nodes (e.g., the dots in | |
| 85 | * <code>$root.node.var$</code>). | |
| 86 | */ | |
| 87 | public static final String SEPARATOR = "."; | |
| 88 | public static final char SEPARATOR_CHAR = SEPARATOR.charAt( 0 ); | |
| 89 | ||
| 90 | private final static int GROUP_DELIMITED = 1; | |
| 91 | private final static int GROUP_REFERENCE = 2; | |
| 92 | ||
| 93 | private final static VariableDecorator VARIABLE_DECORATOR | |
| 94 | = new YamlVariableDecorator(); | |
| 95 | ||
| 96 | /** | |
| 97 | * Compiled version of DEFAULT_REGEX. | |
| 98 | */ | |
| 99 | private final static Pattern REGEX_PATTERN | |
| 100 | = Pattern.compile( YamlVariableDecorator.REGEX ); | |
| 101 | ||
| 102 | /** | |
| 103 | * Should be JsonPointer.SEPARATOR, but Jackson YAML uses magic values. | |
| 104 | */ | |
| 105 | private final static char SEPARATOR_YAML = '/'; | |
| 106 | ||
| 107 | /** | |
| 108 | * Start of the Universe (the YAML document node that contains all others). | |
| 109 | */ | |
| 110 | private JsonNode documentRoot; | |
| 111 | ||
| 112 | /** | |
| 113 | * Map of references to dereferenced field values. | |
| 114 | */ | |
| 115 | private Map<String, String> references; | |
| 116 | ||
| 117 | public YamlParser( final InputStream in ) throws IOException { | |
| 118 | process( in ); | |
| 119 | } | |
| 120 | ||
| 121 | /** | |
| 122 | * Returns the given string with all the delimited references swapped with | |
| 123 | * their recursively resolved values. | |
| 124 | * | |
| 125 | * @param text The text to parse with zero or more delimited references to | |
| 126 | * replace. | |
| 127 | * | |
| 128 | * @return The substituted value. | |
| 129 | */ | |
| 130 | public String substitute( String text ) { | |
| 131 | final Matcher matcher = patternMatch( text ); | |
| 132 | final Map<String, String> map = getReferences(); | |
| 133 | ||
| 134 | while( matcher.find() ) { | |
| 135 | final String key = matcher.group( GROUP_DELIMITED ); | |
| 136 | final String value = map.get( key ); | |
| 137 | ||
| 138 | if( value == null ) { | |
| 139 | missing( text ); | |
| 140 | } else { | |
| 141 | text = text.replace( key, value ); | |
| 142 | } | |
| 143 | } | |
| 144 | ||
| 145 | return text; | |
| 146 | } | |
| 147 | ||
| 148 | /** | |
| 149 | * Returns all the strings with their values resolved in a flat hierarchy. | |
| 150 | * This copies all the keys and resolved values into a new map. | |
| 151 | * | |
| 152 | * @return The new map created with all values having been resolved, | |
| 153 | * recursively. | |
| 154 | */ | |
| 155 | public Map<String, String> createResolvedMap() { | |
| 156 | final Map<String, String> map = new HashMap<>( 1024 ); | |
| 157 | ||
| 158 | resolve( getDocumentRoot(), "", map ); | |
| 159 | ||
| 160 | return map; | |
| 161 | } | |
| 162 | ||
| 163 | /** | |
| 164 | * Iterate over a given root node (at any level of the tree) and adapt each | |
| 165 | * leaf node. | |
| 166 | * | |
| 167 | * @param rootNode A JSON node (YAML node) to adapt. | |
| 168 | * @param map Container that associates definitions with values. | |
| 169 | */ | |
| 170 | private void resolve( | |
| 171 | final JsonNode rootNode, final String path, final Map<String, String> map ) { | |
| 172 | ||
| 173 | if( rootNode != null ) { | |
| 174 | rootNode.fields().forEachRemaining( | |
| 175 | (Entry<String, JsonNode> leaf) -> resolve( leaf, path, map ) | |
| 176 | ); | |
| 177 | } | |
| 178 | } | |
| 179 | ||
| 180 | /** | |
| 181 | * Recursively adapt each rootNode to a corresponding rootItem. | |
| 182 | * | |
| 183 | * @param rootNode The node to adapt. | |
| 184 | */ | |
| 185 | private void resolve( | |
| 186 | final Entry<String, JsonNode> rootNode, | |
| 187 | final String path, | |
| 188 | final Map<String, String> map ) { | |
| 189 | ||
| 190 | final JsonNode leafNode = rootNode.getValue(); | |
| 191 | final String key = rootNode.getKey(); | |
| 192 | ||
| 193 | if( leafNode.isValueNode() ) { | |
| 194 | final String value = rootNode.getValue().asText(); | |
| 195 | ||
| 196 | map.put( VARIABLE_DECORATOR.decorate( path + key ), substitute( value ) ); | |
| 197 | } | |
| 198 | ||
| 199 | if( leafNode.isObject() ) { | |
| 200 | resolve( leafNode, path + key + SEPARATOR, map ); | |
| 201 | } | |
| 202 | } | |
| 203 | ||
| 204 | /** | |
| 205 | * Reads the first document from the given stream of YAML data and returns a | |
| 206 | * corresponding object that represents the YAML hierarchy. The calling class | |
| 207 | * is responsible for closing the stream. Calling classes should use | |
| 208 | * <code>JsonNode.fields()</code> to walk through the YAML tree of fields. | |
| 209 | * | |
| 210 | * @param in The input stream containing YAML content. | |
| 211 | * | |
| 212 | * @return An object hierarchy to represent the content. | |
| 213 | * | |
| 214 | * @throws IOException Could not read the stream. | |
| 215 | */ | |
| 216 | private JsonNode process( final InputStream in ) throws IOException { | |
| 217 | ||
| 218 | final ObjectNode root = (ObjectNode)getObjectMapper().readTree( in ); | |
| 219 | setDocumentRoot( root ); | |
| 220 | process( root ); | |
| 221 | return getDocumentRoot(); | |
| 222 | } | |
| 223 | ||
| 224 | /** | |
| 225 | * Iterate over a given root node (at any level of the tree) and process each | |
| 226 | * leaf node. | |
| 227 | * | |
| 228 | * @param root A node to process. | |
| 229 | */ | |
| 230 | private void process( final JsonNode root ) { | |
| 231 | root.fields().forEachRemaining( this::process ); | |
| 232 | } | |
| 233 | ||
| 234 | /** | |
| 235 | * Process the given field, which is a named node. This is where the | |
| 236 | * application does the up-front work of mapping references to their fully | |
| 237 | * recursively dereferenced values. | |
| 238 | * | |
| 239 | * @param field The named node. | |
| 240 | */ | |
| 241 | private void process( final Entry<String, JsonNode> field ) { | |
| 242 | final JsonNode node = field.getValue(); | |
| 243 | ||
| 244 | if( node.isObject() ) { | |
| 245 | process( node ); | |
| 246 | } else { | |
| 247 | final JsonNode fieldValue = field.getValue(); | |
| 248 | ||
| 249 | // Only basic data types can be parsed into variable values. For | |
| 250 | // node structures, YAML has a built-in mechanism. | |
| 251 | if( fieldValue.isValueNode() ) { | |
| 252 | try { | |
| 253 | resolve( fieldValue.asText() ); | |
| 254 | } catch( StackOverflowError e ) { | |
| 255 | throw new IllegalArgumentException( | |
| 256 | "Unresolvable: " + node.textValue() + " = " + fieldValue ); | |
| 257 | } | |
| 258 | } | |
| 259 | } | |
| 260 | } | |
| 261 | ||
| 262 | /** | |
| 263 | * Inserts the delimited references and field values into the cache. This will | |
| 264 | * overwrite existing references. | |
| 265 | * | |
| 266 | * @param fieldValue YAML field containing zero or more delimited references. | |
| 267 | * If it contains a delimited reference, the parameter is modified with the | |
| 268 | * dereferenced value before it is returned. | |
| 269 | * | |
| 270 | * @return fieldValue without delimited references. | |
| 271 | */ | |
| 272 | private String resolve( String fieldValue ) { | |
| 273 | final Matcher matcher = patternMatch( fieldValue ); | |
| 274 | ||
| 275 | while( matcher.find() ) { | |
| 276 | final String delimited = matcher.group( GROUP_DELIMITED ); | |
| 277 | final String reference = matcher.group( GROUP_REFERENCE ); | |
| 278 | final String dereference = resolve( lookup( reference ) ); | |
| 279 | ||
| 280 | fieldValue = fieldValue.replace( delimited, dereference ); | |
| 281 | ||
| 282 | // This will perform some superfluous calls by overwriting existing | |
| 283 | // items in the delimited reference map. | |
| 284 | put( delimited, dereference ); | |
| 285 | } | |
| 286 | ||
| 287 | return fieldValue; | |
| 288 | } | |
| 289 | ||
| 290 | /** | |
| 291 | * Inserts a key/value pair into the references map. The map retains | |
| 292 | * references and dereferenced values found in the YAML. If the reference | |
| 293 | * already exists, this will overwrite with a new value. | |
| 294 | * | |
| 295 | * @param delimited The variable name. | |
| 296 | * @param dereferenced The resolved value. | |
| 297 | */ | |
| 298 | private void put( String delimited, String dereferenced ) { | |
| 299 | if( dereferenced.isEmpty() ) { | |
| 300 | missing( delimited ); | |
| 301 | } else { | |
| 302 | getReferences().put( delimited, dereferenced ); | |
| 303 | } | |
| 304 | } | |
| 305 | ||
| 306 | /** | |
| 307 | * Writes the modified YAML document to standard output. | |
| 308 | */ | |
| 309 | private void writeDocument() throws IOException { | |
| 310 | getObjectMapper().writeValue( System.out, getDocumentRoot() ); | |
| 311 | } | |
| 312 | ||
| 313 | /** | |
| 314 | * Called when a delimited reference is dereferenced to an empty string. This | |
| 315 | * should produce a warning for the user. | |
| 316 | * | |
| 317 | * @param delimited Delimited reference with no derived value. | |
| 318 | */ | |
| 319 | private void missing( final String delimited ) { | |
| 320 | throw new InvalidParameterException( | |
| 321 | MessageFormat.format( "Missing value for '{0}'.", delimited ) ); | |
| 322 | } | |
| 323 | ||
| 324 | /** | |
| 325 | * Returns a REGEX_PATTERN matcher for the given text. | |
| 326 | * | |
| 327 | * @param text The text that contains zero or more instances of a | |
| 328 | * REGEX_PATTERN that can be found using the regular expression. | |
| 329 | */ | |
| 330 | private Matcher patternMatch( String text ) { | |
| 331 | return getPattern().matcher( text ); | |
| 332 | } | |
| 333 | ||
| 334 | /** | |
| 335 | * Finds the YAML value for a reference. | |
| 336 | * | |
| 337 | * @param reference References a value in the YAML document. | |
| 338 | * | |
| 339 | * @return The dereferenced value. | |
| 340 | */ | |
| 341 | private String lookup( final String reference ) { | |
| 342 | return getDocumentRoot().at( asPath( reference ) ).asText(); | |
| 343 | } | |
| 344 | ||
| 345 | /** | |
| 346 | * Converts a reference (not delimited) to a path that can be used to find a | |
| 347 | * value that should exist inside the YAML document. | |
| 348 | * | |
| 349 | * @param reference The reference to convert to a YAML document path. | |
| 350 | * | |
| 351 | * @return The reference with a leading slash and its separator characters | |
| 352 | * converted to slashes. | |
| 353 | */ | |
| 354 | private String asPath( final String reference ) { | |
| 355 | return SEPARATOR_YAML + reference.replace( getDelimitedSeparator(), SEPARATOR_YAML ); | |
| 356 | } | |
| 357 | ||
| 358 | /** | |
| 359 | * Sets the parent node for the entire YAML document tree. | |
| 360 | * | |
| 361 | * @param documentRoot The parent node. | |
| 362 | */ | |
| 363 | private void setDocumentRoot( ObjectNode documentRoot ) { | |
| 364 | this.documentRoot = documentRoot; | |
| 365 | } | |
| 366 | ||
| 367 | /** | |
| 368 | * Returns the parent node for the entire YAML document tree. | |
| 369 | * | |
| 370 | * @return The parent node. | |
| 371 | */ | |
| 372 | protected JsonNode getDocumentRoot() { | |
| 373 | return this.documentRoot; | |
| 374 | } | |
| 375 | ||
| 376 | /** | |
| 377 | * Returns the compiled regular expression REGEX_PATTERN used to match | |
| 378 | * delimited references. | |
| 379 | * | |
| 380 | * @return A compiled regex for use with the Matcher. | |
| 381 | */ | |
| 382 | private Pattern getPattern() { | |
| 383 | return REGEX_PATTERN; | |
| 384 | } | |
| 385 | ||
| 386 | /** | |
| 387 | * Returns the list of references mapped to dereferenced values. | |
| 388 | * | |
| 389 | * @return | |
| 390 | */ | |
| 391 | private Map<String, String> getReferences() { | |
| 392 | if( this.references == null ) { | |
| 393 | this.references = createReferences(); | |
| 394 | } | |
| 395 | ||
| 396 | return this.references; | |
| 397 | } | |
| 398 | ||
| 399 | /** | |
| 400 | * Subclasses can override this method to insert their own map. | |
| 401 | * | |
| 402 | * @return An empty HashMap, never null. | |
| 403 | */ | |
| 404 | protected Map<String, String> createReferences() { | |
| 405 | return new HashMap<>(); | |
| 406 | } | |
| 407 | ||
| 408 | private final class ResolverYAMLFactory extends YAMLFactory { | |
| 409 | ||
| 410 | private static final long serialVersionUID = 1L; | |
| 411 | ||
| 412 | @Override | |
| 413 | protected YAMLGenerator _createGenerator( | |
| 414 | final Writer out, final IOContext ctxt ) throws IOException { | |
| 415 | ||
| 416 | return new ResolverYAMLGenerator( | |
| 417 | ctxt, _generatorFeatures, _yamlGeneratorFeatures, _objectCodec, | |
| 418 | out, _version ); | |
| 419 | } | |
| 420 | } | |
| 421 | ||
| 422 | private class ResolverYAMLGenerator extends YAMLGenerator { | |
| 423 | ||
| 424 | public ResolverYAMLGenerator( | |
| 425 | final IOContext ctxt, | |
| 426 | final int jsonFeatures, | |
| 427 | final int yamlFeatures, | |
| 428 | final ObjectCodec codec, | |
| 429 | final Writer out, | |
| 430 | final DumperOptions.Version version ) throws IOException { | |
| 431 | ||
| 432 | super( ctxt, jsonFeatures, yamlFeatures, codec, out, version ); | |
| 433 | } | |
| 434 | ||
| 435 | @Override | |
| 436 | public void writeString( final String text ) | |
| 437 | throws IOException, JsonGenerationException { | |
| 438 | super.writeString( substitute( text ) ); | |
| 439 | } | |
| 440 | } | |
| 441 | ||
| 442 | private YAMLFactory getYAMLFactory() { | |
| 443 | return new ResolverYAMLFactory(); | |
| 444 | } | |
| 445 | ||
| 444 | 446 | private ObjectMapper getObjectMapper() { |
| 445 | 447 | return new ObjectMapper( getYAMLFactory() ); |
| 30 | 30 | import com.fasterxml.jackson.databind.JsonNode; |
| 31 | 31 | import com.scrivenvar.definition.VariableTreeItem; |
| 32 | import java.io.IOException; | |
| 33 | import java.io.InputStream; | |
| 34 | 32 | import java.util.Map.Entry; |
| 35 | 33 | import javafx.scene.control.TreeItem; |
| ... | ||
| 52 | 50 | /** |
| 53 | 51 | * Converts a YAML document to a TreeView based on the document keys. Only the |
| 54 | * first document in the stream is adapted. This does not close the stream. | |
| 52 | * first document in the stream is adapted. | |
| 55 | 53 | * |
| 56 | * @param in Contains a YAML document. | |
| 57 | 54 | * @param name Root TreeItem node name. |
| 58 | 55 | * |
| 59 | 56 | * @return A TreeView populated with all the keys in the YAML document. |
| 60 | * | |
| 61 | * @throws IOException Could not read from the stream. | |
| 62 | 57 | */ |
| 63 | public TreeView<String> adapt( final InputStream in, final String name ) | |
| 64 | throws IOException { | |
| 65 | ||
| 66 | final JsonNode rootNode = getYamlParser().process( in ); | |
| 58 | public TreeView<String> adapt( final String name ){ | |
| 59 | final JsonNode rootNode = getYamlParser().getDocumentRoot(); | |
| 67 | 60 | final TreeItem<String> rootItem = createTreeItem( name ); |
| 68 | 61 | |
| 1 | /* | |
| 2 | * Copyright 2016 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.test; | |
| 29 | ||
| 30 | import com.scrivenvar.definition.DefinitionPane; | |
| 31 | import static javafx.application.Application.launch; | |
| 32 | import javafx.scene.control.TreeItem; | |
| 33 | import javafx.scene.control.TreeView; | |
| 34 | import javafx.stage.Stage; | |
| 35 | ||
| 36 | /** | |
| 37 | * TestDefinitionPane application for debugging. | |
| 38 | */ | |
| 39 | public final class TestDefinitionPane extends TestHarness { | |
| 40 | /** | |
| 41 | * Application entry point. | |
| 42 | * | |
| 43 | * @param stage The primary application stage. | |
| 44 | * | |
| 45 | * @throws Exception Could not read configuration file. | |
| 46 | */ | |
| 47 | @Override | |
| 48 | public void start( final Stage stage ) throws Exception { | |
| 49 | super.start( stage ); | |
| 50 | ||
| 51 | TreeView<String> root = createTreeView(); | |
| 52 | DefinitionPane pane = createDefinitionPane( root ); | |
| 53 | ||
| 54 | test( pane, "language.ai.", "article" ); | |
| 55 | test( pane, "language.ai", "ai" ); | |
| 56 | test( pane, "l", "location" ); | |
| 57 | test( pane, "la", "language" ); | |
| 58 | test( pane, "c.p.n", "name" ); | |
| 59 | test( pane, "c.p.n.", "First" ); | |
| 60 | test( pane, "...", "c" ); | |
| 61 | test( pane, "foo", "c" ); | |
| 62 | test( pane, "foo.bar", "c" ); | |
| 63 | test( pane, "", "c" ); | |
| 64 | test( pane, "c", "protagonist" ); | |
| 65 | test( pane, "c.", "protagonist" ); | |
| 66 | test( pane, "c.p", "protagonist" ); | |
| 67 | test( pane, "c.protagonist", "protagonist" ); | |
| 68 | ||
| 69 | throw new RuntimeException( "Complete" ); | |
| 70 | } | |
| 71 | ||
| 72 | private void test( DefinitionPane pane, String path, String value ) { | |
| 73 | System.out.println( "---------------------------" ); | |
| 74 | System.out.println( "Find Path: '" + path + "'" ); | |
| 75 | final TreeItem<String> node = pane.findNode( path ); | |
| 76 | System.out.println( "Path Node: " + node ); | |
| 77 | System.out.println( "Node Val : " + node.getValue() ); | |
| 78 | } | |
| 79 | ||
| 80 | public static void main( String[] args ) { | |
| 81 | launch( args ); | |
| 82 | } | |
| 83 | } | |
| 84 | 1 |
| 1 | /* | |
| 2 | * Copyright 2016 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.test; | |
| 29 | ||
| 30 | import static com.scrivenvar.Messages.get; | |
| 31 | import com.scrivenvar.definition.DefinitionPane; | |
| 32 | import com.scrivenvar.definition.yaml.YamlParser; | |
| 33 | import com.scrivenvar.definition.yaml.YamlTreeAdapter; | |
| 34 | import java.io.IOException; | |
| 35 | import java.io.InputStream; | |
| 36 | import javafx.application.Application; | |
| 37 | import javafx.scene.Scene; | |
| 38 | import javafx.scene.control.TreeView; | |
| 39 | import javafx.scene.layout.BorderPane; | |
| 40 | import javafx.stage.Stage; | |
| 41 | import org.fxmisc.flowless.VirtualizedScrollPane; | |
| 42 | import org.fxmisc.richtext.StyleClassedTextArea; | |
| 43 | ||
| 44 | /** | |
| 45 | * TestDefinitionPane application for debugging and head-banging. | |
| 46 | */ | |
| 47 | public abstract class TestHarness extends Application { | |
| 48 | ||
| 49 | private static Application app; | |
| 50 | private Scene scene; | |
| 51 | ||
| 52 | /** | |
| 53 | * Application entry point. | |
| 54 | * | |
| 55 | * @param stage The primary application stage. | |
| 56 | * | |
| 57 | * @throws Exception Could not read configuration file. | |
| 58 | */ | |
| 59 | @Override | |
| 60 | public void start( final Stage stage ) throws Exception { | |
| 61 | initApplication(); | |
| 62 | initScene(); | |
| 63 | initStage( stage ); | |
| 64 | } | |
| 65 | ||
| 66 | protected TreeView<String> createTreeView() throws IOException { | |
| 67 | return new YamlTreeAdapter( new YamlParser() ).adapt( | |
| 68 | asStream( "/com/scrivenvar/variables.yaml" ), | |
| 69 | get( "Pane.defintion.node.root.title" ) | |
| 70 | ); | |
| 71 | } | |
| 72 | ||
| 73 | protected DefinitionPane createDefinitionPane( TreeView<String> root ) { | |
| 74 | return new DefinitionPane( root ); | |
| 75 | } | |
| 76 | ||
| 77 | private void initApplication() { | |
| 78 | app = this; | |
| 79 | } | |
| 80 | ||
| 81 | private void initScene() { | |
| 82 | final StyleClassedTextArea editor = new StyleClassedTextArea( false ); | |
| 83 | final VirtualizedScrollPane<StyleClassedTextArea> scrollPane = new VirtualizedScrollPane<>( editor ); | |
| 84 | ||
| 85 | final BorderPane borderPane = new BorderPane(); | |
| 86 | borderPane.setPrefSize( 1024, 800 ); | |
| 87 | borderPane.setCenter( scrollPane ); | |
| 88 | ||
| 89 | setScene( new Scene( borderPane ) ); | |
| 90 | } | |
| 91 | ||
| 92 | private void initStage( Stage stage ) { | |
| 93 | stage.setScene( getScene() ); | |
| 94 | } | |
| 95 | ||
| 96 | private Scene getScene() { | |
| 97 | return this.scene; | |
| 98 | } | |
| 99 | ||
| 100 | private void setScene( Scene scene ) { | |
| 101 | this.scene = scene; | |
| 102 | } | |
| 103 | ||
| 104 | private static Application getApplication() { | |
| 105 | return app; | |
| 106 | } | |
| 107 | ||
| 108 | public static void showDocument( String uri ) { | |
| 109 | getApplication().getHostServices().showDocument( uri ); | |
| 110 | } | |
| 111 | ||
| 112 | protected InputStream asStream( String resource ) { | |
| 113 | return TestHarness.class.getResourceAsStream( resource ); | |
| 114 | } | |
| 115 | } | |
| 116 | 1 |
| 1 | /* | |
| 2 | * Copyright 2016 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.test; | |
| 29 | ||
| 30 | import com.scrivenvar.definition.VariableTreeItem; | |
| 31 | import java.util.Collection; | |
| 32 | import java.util.HashMap; | |
| 33 | import java.util.Map; | |
| 34 | import static java.util.concurrent.ThreadLocalRandom.current; | |
| 35 | import java.util.concurrent.TimeUnit; | |
| 36 | import static java.util.concurrent.TimeUnit.DAYS; | |
| 37 | import static java.util.concurrent.TimeUnit.HOURS; | |
| 38 | import static java.util.concurrent.TimeUnit.MILLISECONDS; | |
| 39 | import static java.util.concurrent.TimeUnit.MINUTES; | |
| 40 | import static java.util.concurrent.TimeUnit.NANOSECONDS; | |
| 41 | import static java.util.concurrent.TimeUnit.SECONDS; | |
| 42 | import static javafx.application.Application.launch; | |
| 43 | import javafx.scene.control.TreeItem; | |
| 44 | import javafx.scene.control.TreeView; | |
| 45 | import javafx.stage.Stage; | |
| 46 | import org.ahocorasick.trie.*; | |
| 47 | import org.ahocorasick.trie.Trie.TrieBuilder; | |
| 48 | import static org.apache.commons.lang3.RandomStringUtils.randomNumeric; | |
| 49 | import org.apache.commons.lang3.StringUtils; | |
| 50 | ||
| 51 | /** | |
| 52 | * Tests substituting variable definitions with their values in a swath of text. | |
| 53 | * | |
| 54 | * @author White Magic Software, Ltd. | |
| 55 | */ | |
| 56 | public class TestVariableNameProcessor extends TestHarness { | |
| 57 | ||
| 58 | private final static int TEXT_SIZE = 1000000; | |
| 59 | private final static int MATCHES_DIVISOR = 1000; | |
| 60 | ||
| 61 | private final static StringBuilder SOURCE | |
| 62 | = new StringBuilder( randomNumeric( TEXT_SIZE ) ); | |
| 63 | ||
| 64 | private final static boolean DEBUG = false; | |
| 65 | ||
| 66 | public TestVariableNameProcessor() { | |
| 67 | } | |
| 68 | ||
| 69 | @Override | |
| 70 | public void start( final Stage stage ) throws Exception { | |
| 71 | super.start( stage ); | |
| 72 | ||
| 73 | final TreeView<String> treeView = createTreeView(); | |
| 74 | final Map<String, String> definitions = new HashMap<>(); | |
| 75 | ||
| 76 | populate( treeView.getRoot(), definitions ); | |
| 77 | injectVariables( definitions ); | |
| 78 | ||
| 79 | final String text = SOURCE.toString(); | |
| 80 | ||
| 81 | show( text ); | |
| 82 | ||
| 83 | long duration = System.nanoTime(); | |
| 84 | String result = testBorAhoCorasick( text, definitions ); | |
| 85 | duration = System.nanoTime() - duration; | |
| 86 | show( result ); | |
| 87 | System.out.println( elapsed( duration ) ); | |
| 88 | ||
| 89 | duration = System.nanoTime(); | |
| 90 | result = testStringUtils( text, definitions ); | |
| 91 | duration = System.nanoTime() - duration; | |
| 92 | show( result ); | |
| 93 | System.out.println( elapsed( duration ) ); | |
| 94 | ||
| 95 | throw new RuntimeException( "Complete" ); | |
| 96 | } | |
| 97 | ||
| 98 | private void show( final String s ) { | |
| 99 | if( DEBUG ) { | |
| 100 | System.out.printf( "%s%n%n", s ); | |
| 101 | } | |
| 102 | } | |
| 103 | ||
| 104 | private String testBorAhoCorasick( | |
| 105 | final String text, | |
| 106 | final Map<String, String> definitions ) { | |
| 107 | // Create a buffer sufficiently large that re-allocations are minimized. | |
| 108 | final StringBuilder sb = new StringBuilder( text.length() << 1 ); | |
| 109 | ||
| 110 | final TrieBuilder builder = Trie.builder(); | |
| 111 | builder.onlyWholeWords(); | |
| 112 | builder.removeOverlaps(); | |
| 113 | ||
| 114 | final String[] keys = keys( definitions ); | |
| 115 | ||
| 116 | for( final String key : keys ) { | |
| 117 | builder.addKeyword( key ); | |
| 118 | } | |
| 119 | ||
| 120 | final Trie trie = builder.build(); | |
| 121 | final Collection<Emit> emits = trie.parseText( text ); | |
| 122 | ||
| 123 | int prevIndex = 0; | |
| 124 | ||
| 125 | for( final Emit emit : emits ) { | |
| 126 | final int matchIndex = emit.getStart(); | |
| 127 | ||
| 128 | sb.append( text.substring( prevIndex, matchIndex ) ); | |
| 129 | sb.append( definitions.get( emit.getKeyword() ) ); | |
| 130 | prevIndex = emit.getEnd() + 1; | |
| 131 | } | |
| 132 | ||
| 133 | // Add the remainder of the string (contains no more matches). | |
| 134 | sb.append( text.substring( prevIndex ) ); | |
| 135 | ||
| 136 | return sb.toString(); | |
| 137 | } | |
| 138 | ||
| 139 | private String testStringUtils( | |
| 140 | final String text, final Map<String, String> definitions ) { | |
| 141 | final String[] keys = keys( definitions ); | |
| 142 | final String[] values = values( definitions ); | |
| 143 | ||
| 144 | return StringUtils.replaceEach( text, keys, values ); | |
| 145 | } | |
| 146 | ||
| 147 | private String[] keys( final Map<String, String> definitions ) { | |
| 148 | final int size = definitions.size(); | |
| 149 | return definitions.keySet().toArray( new String[ size ] ); | |
| 150 | } | |
| 151 | ||
| 152 | private String[] values( final Map<String, String> definitions ) { | |
| 153 | final int size = definitions.size(); | |
| 154 | return definitions.values().toArray( new String[ size ] ); | |
| 155 | } | |
| 156 | ||
| 157 | /** | |
| 158 | * Decomposes a period of time into days, hours, minutes, seconds, | |
| 159 | * milliseconds, and nanoseconds. | |
| 160 | * | |
| 161 | * @param duration Time in nanoseconds. | |
| 162 | * | |
| 163 | * @return A non-null, comma-separated string (without newline). | |
| 164 | */ | |
| 165 | public String elapsed( long duration ) { | |
| 166 | final TimeUnit scale = NANOSECONDS; | |
| 167 | ||
| 168 | long days = scale.toDays( duration ); | |
| 169 | duration -= DAYS.toMillis( days ); | |
| 170 | long hours = scale.toHours( duration ); | |
| 171 | duration -= HOURS.toMillis( hours ); | |
| 172 | long minutes = scale.toMinutes( duration ); | |
| 173 | duration -= MINUTES.toMillis( minutes ); | |
| 174 | long seconds = scale.toSeconds( duration ); | |
| 175 | duration -= SECONDS.toMillis( seconds ); | |
| 176 | long millis = scale.toMillis( duration ); | |
| 177 | duration -= MILLISECONDS.toMillis( seconds ); | |
| 178 | long nanos = scale.toNanos( duration ); | |
| 179 | ||
| 180 | return String.format( | |
| 181 | "%d days, %d hours, %d minutes, %d seconds, %d millis, %d nanos", | |
| 182 | days, hours, minutes, seconds, millis, nanos | |
| 183 | ); | |
| 184 | } | |
| 185 | ||
| 186 | private void injectVariables( final Map<String, String> definitions ) { | |
| 187 | for( int i = (SOURCE.length() / MATCHES_DIVISOR) + 1; i > 0; i-- ) { | |
| 188 | final int r = current().nextInt( 1, SOURCE.length() ); | |
| 189 | SOURCE.insert( r, randomKey( definitions ) ); | |
| 190 | } | |
| 191 | } | |
| 192 | ||
| 193 | private String randomKey( final Map<String, String> map ) { | |
| 194 | final Object[] keys = map.keySet().toArray(); | |
| 195 | final int r = current().nextInt( keys.length ); | |
| 196 | return keys[ r ].toString(); | |
| 197 | } | |
| 198 | ||
| 199 | private void populate( final TreeItem<String> parent, final Map<String, String> map ) { | |
| 200 | for( final TreeItem<String> child : parent.getChildren() ) { | |
| 201 | if( child.isLeaf() ) { | |
| 202 | final VariableTreeItem<String> item; | |
| 203 | ||
| 204 | if( child instanceof VariableTreeItem ) { | |
| 205 | item = ((VariableTreeItem<String>)child); | |
| 206 | } else { | |
| 207 | throw new IllegalArgumentException( | |
| 208 | "Child must be subclass of VariableTreeItem: " + child ); | |
| 209 | } | |
| 210 | ||
| 211 | final String key = asDefinition( item.toPath() ); | |
| 212 | final String value = child.getValue(); | |
| 213 | ||
| 214 | map.put( key, value ); | |
| 215 | } else { | |
| 216 | populate( child, map ); | |
| 217 | } | |
| 218 | } | |
| 219 | } | |
| 220 | ||
| 221 | private String asDefinition( final String key ) { | |
| 222 | return "$" + key + "$"; | |
| 223 | } | |
| 224 | ||
| 225 | public static void main( String[] args ) { | |
| 226 | launch( args ); | |
| 227 | } | |
| 228 | } | |
| 229 | 1 |