001/* 002 * Copyright (C) 2005 Christian Schulte <cs@schulte.it> 003 * All rights reserved. 004 * 005 * Redistribution and use in source and binary forms, with or without 006 * modification, are permitted provided that the following conditions 007 * are met: 008 * 009 * o Redistributions of source code must retain the above copyright 010 * notice, this list of conditions and the following disclaimer. 011 * 012 * o Redistributions in binary form must reproduce the above copyright 013 * notice, this list of conditions and the following disclaimer in 014 * the documentation and/or other materials provided with the 015 * distribution. 016 * 017 * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 018 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY 019 * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL 020 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT, 021 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 022 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 023 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 024 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 025 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 026 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 027 * 028 * $JOMC: SectionEditor.java 5291 2016-08-29 17:31:15Z schulte $ 029 * 030 */ 031package org.jomc.util; 032 033import java.io.IOException; 034import java.lang.reflect.UndeclaredThrowableException; 035import java.text.MessageFormat; 036import java.util.Collection; 037import java.util.LinkedList; 038import java.util.List; 039import java.util.Map; 040import java.util.ResourceBundle; 041import java.util.Stack; 042import java.util.concurrent.Callable; 043import java.util.concurrent.CancellationException; 044import java.util.concurrent.ConcurrentHashMap; 045import java.util.concurrent.ExecutionException; 046import java.util.concurrent.ExecutorService; 047import java.util.concurrent.Future; 048 049/** 050 * Interface to section based editing. 051 * <p> 052 * Section based editing is a two phase process of parsing the editor's input into a corresponding hierarchy of 053 * {@code Section} instances, followed by rendering the parsed sections to produce the output of the editor. Method 054 * {@code editLine} returns {@code null} during parsing and the output of the editor on end of input, rendered by 055 * calling method {@code getOutput}. Parsing is backed by methods {@code getSection} and {@code isSectionFinished}. 056 * </p> 057 * 058 * @author <a href="mailto:cs@schulte.it">Christian Schulte</a> 059 * @version $JOMC: SectionEditor.java 5291 2016-08-29 17:31:15Z schulte $ 060 * 061 * @see #edit(java.lang.String) 062 */ 063public class SectionEditor extends LineEditor 064{ 065 066 /** 067 * Marker indicating the start of a section. 068 */ 069 private static final String DEFAULT_SECTION_START = "SECTION-START["; 070 071 /** 072 * Marker indicating the end of a section. 073 */ 074 private static final String DEFAULT_SECTION_END = "SECTION-END"; 075 076 /** 077 * Stack of sections. 078 */ 079 private Stack<Section> stack; 080 081 /** 082 * Mapping of section names to flags indicating presence of the section. 083 */ 084 private final Map<String, Boolean> presenceFlags = new ConcurrentHashMap<String, Boolean>( 32 ); 085 086 /** 087 * The {@code ExecutorService} of the instance. 088 * 089 * @since 1.10 090 */ 091 private ExecutorService executorService; 092 093 /** 094 * Creates a new {@code SectionEditor} instance. 095 */ 096 public SectionEditor() 097 { 098 this( null, null ); 099 } 100 101 /** 102 * Creates a new {@code SectionEditor} instance taking a string to use for separating lines. 103 * 104 * @param lineSeparator String to use for separating lines. 105 */ 106 public SectionEditor( final String lineSeparator ) 107 { 108 this( null, lineSeparator ); 109 } 110 111 /** 112 * Creates a new {@code SectionEditor} instance taking an editor to chain. 113 * 114 * @param editor The editor to chain. 115 */ 116 public SectionEditor( final LineEditor editor ) 117 { 118 this( editor, null ); 119 } 120 121 /** 122 * Creates a new {@code SectionEditor} instance taking an editor to chain and a string to use for separating lines. 123 * 124 * @param editor The editor to chain. 125 * @param lineSeparator String to use for separating lines. 126 */ 127 public SectionEditor( final LineEditor editor, final String lineSeparator ) 128 { 129 super( editor, lineSeparator ); 130 } 131 132 /** 133 * Gets an {@code ExecutorService} used to edit sections in parallel. 134 * 135 * @return An {@code ExecutorService} used to edit sections in parallel or {@code null}, if no such service has 136 * been provided by an application. 137 * 138 * @since 1.10 139 * 140 * @see #setExecutorService(java.util.concurrent.ExecutorService) 141 */ 142 public final ExecutorService getExecutorService() 143 { 144 return this.executorService; 145 } 146 147 /** 148 * Sets the {@code ExecutorService} to be used to edit sections in parallel. 149 * <p> 150 * The {@code ExecutorService} to be used to edit sections in parallel is an optional entity. If no such service is 151 * provided by an application, no parallelization is performed. Configuration or lifecycle management of the given 152 * {@code ExecutorService} is the responsibility of the application. 153 * </p> 154 * 155 * @param value The {@code ExecutorService} to be used to edit sections in parallel or {@code null}, to disable any 156 * parallelization. 157 * 158 * @since 1.10 159 * 160 * @see #getExecutorService() 161 */ 162 public final void setExecutorService( final ExecutorService value ) 163 { 164 this.executorService = value; 165 } 166 167 @Override 168 protected final String editLine( final String line ) throws IOException 169 { 170 if ( this.stack == null ) 171 { 172 final Section root = new Section(); 173 root.setMode( Section.MODE_HEAD ); 174 this.stack = new Stack<Section>(); 175 this.stack.push( root ); 176 } 177 178 Section current = this.stack.peek(); 179 String replacement = null; 180 181 if ( line != null ) 182 { 183 final Section child = this.getSection( line ); 184 185 if ( child != null ) 186 { 187 child.setStartingLine( line ); 188 child.setMode( Section.MODE_HEAD ); 189 190 if ( current.getMode() == Section.MODE_TAIL && current.getTailContent().length() > 0 ) 191 { 192 final Section s = new Section(); 193 s.getHeadContent().append( current.getTailContent() ); 194 current.getTailContent().setLength( 0 ); 195 current.getSections().add( s ); 196 current = s; 197 this.stack.push( current ); 198 } 199 200 current.getSections().add( child ); 201 current.setMode( Section.MODE_TAIL ); 202 this.stack.push( child ); 203 } 204 else if ( this.isSectionFinished( line ) ) 205 { 206 final Section s = this.stack.pop(); 207 s.setEndingLine( line ); 208 209 if ( this.stack.isEmpty() ) 210 { 211 this.stack = null; 212 throw new IOException( getMessage( "unexpectedEndOfSection", this.getLineNumber() ) ); 213 } 214 215 if ( this.stack.peek().getName() == null && this.stack.size() > 1 ) 216 { 217 this.stack.pop(); 218 } 219 } 220 else 221 { 222 switch ( current.getMode() ) 223 { 224 case Section.MODE_HEAD: 225 current.getHeadContent().append( line ).append( this.getLineSeparator() ); 226 break; 227 228 case Section.MODE_TAIL: 229 current.getTailContent().append( line ).append( this.getLineSeparator() ); 230 break; 231 232 default: 233 throw new AssertionError( current.getMode() ); 234 235 } 236 } 237 } 238 else 239 { 240 final Section root = this.stack.pop(); 241 242 if ( !this.stack.isEmpty() ) 243 { 244 this.stack = null; 245 throw new IOException( getMessage( "unexpectedEndOfFile", this.getLineNumber(), root.getName() ) ); 246 } 247 248 replacement = this.getOutput( root ); 249 this.stack = null; 250 } 251 252 return replacement; 253 } 254 255 /** 256 * Parses the given line to mark the start of a new section. 257 * 258 * @param line The line to parse or {@code null}. 259 * 260 * @return The section starting at {@code line} or {@code null}, if {@code line} does not mark the start of a 261 * section. 262 * 263 * @throws IOException if parsing fails. 264 */ 265 protected Section getSection( final String line ) throws IOException 266 { 267 Section s = null; 268 269 if ( line != null ) 270 { 271 final int markerIndex = line.indexOf( DEFAULT_SECTION_START ); 272 273 if ( markerIndex != -1 ) 274 { 275 final int startIndex = markerIndex + DEFAULT_SECTION_START.length(); 276 final int endIndex = line.indexOf( ']', startIndex ); 277 278 if ( endIndex == -1 ) 279 { 280 throw new IOException( getMessage( "sectionMarkerParseFailure", line, this.getLineNumber() ) ); 281 } 282 283 s = new Section(); 284 s.setName( line.substring( startIndex, endIndex ) ); 285 } 286 } 287 288 return s; 289 } 290 291 /** 292 * Parses the given line to mark the end of a section. 293 * 294 * @param line The line to parse or {@code null}. 295 * 296 * @return {@code true}, if {@code line} marks the end of a section; {@code false}, if {@code line} does not mark 297 * the end of a section. 298 * 299 * @throws IOException if parsing fails. 300 */ 301 protected boolean isSectionFinished( final String line ) throws IOException 302 { 303 return line != null && line.contains( DEFAULT_SECTION_END ); 304 } 305 306 /** 307 * Edits a section. 308 * <p> 309 * This method does not change any content by default. Overriding classes may use this method for editing 310 * sections prior to rendering. 311 * </p> 312 * 313 * @param section The section to edit. 314 * 315 * @throws NullPointerException if {@code section} is {@code null}. 316 * @throws IOException if editing fails. 317 */ 318 protected void editSection( final Section section ) throws IOException 319 { 320 if ( section == null ) 321 { 322 throw new NullPointerException( "section" ); 323 } 324 325 if ( section.getName() != null ) 326 { 327 this.presenceFlags.put( section.getName(), Boolean.TRUE ); 328 } 329 } 330 331 /** 332 * Creates tasks recursively for editing sections in parallel. 333 * 334 * @param section The section to edit recursively. 335 * @param tasks The collection of tasks to run in parallel. 336 * 337 * @throws NullPointerException if {@code section} or {@code tasks} is {@code null}. 338 * @throws IOException if editing fails. 339 */ 340 private void editSections( final Section section, final Collection<EditSectionTask> tasks ) throws IOException 341 { 342 if ( section == null ) 343 { 344 throw new NullPointerException( "section" ); 345 } 346 if ( tasks == null ) 347 { 348 throw new NullPointerException( "tasks" ); 349 } 350 351 tasks.add( new EditSectionTask( section ) ); 352 for ( int i = 0, s0 = section.getSections().size(); i < s0; i++ ) 353 { 354 this.editSections( section.getSections().get( i ), tasks ); 355 } 356 } 357 358 /** 359 * Gets the output of the editor. 360 * <p> 361 * This method calls method {@code editSection()} for each section of the editor prior to rendering the sections 362 * to produce the output of the editor. 363 * </p> 364 * 365 * @param section The section to start rendering the editor's output with. 366 * 367 * @return The output of the editor. 368 * 369 * @throws NullPointerException if {@code section} is {@code null}. 370 * @throws IOException if editing or rendering fails. 371 */ 372 protected String getOutput( final Section section ) throws IOException 373 { 374 if ( section == null ) 375 { 376 throw new NullPointerException( "section" ); 377 } 378 379 try 380 { 381 this.presenceFlags.clear(); 382 final List<EditSectionTask> tasks = new LinkedList<EditSectionTask>(); 383 this.editSections( section, tasks ); 384 385 if ( this.getExecutorService() != null && tasks.size() > 1 ) 386 { 387 for ( final Future<Void> task : this.getExecutorService().invokeAll( tasks ) ) 388 { 389 task.get(); 390 } 391 } 392 else 393 { 394 for ( int i = 0, s0 = tasks.size(); i < s0; i++ ) 395 { 396 tasks.get( i ).call(); 397 } 398 } 399 400 return this.renderSections( section, new StringBuilder( 512 ) ).toString(); 401 } 402 catch ( final CancellationException e ) 403 { 404 throw (IOException) new IOException( getMessage( e ) ).initCause( e ); 405 } 406 catch ( final InterruptedException e ) 407 { 408 throw (IOException) new IOException( getMessage( e ) ).initCause( e ); 409 } 410 catch ( final ExecutionException e ) 411 { 412 if ( e.getCause() instanceof IOException ) 413 { 414 throw (IOException) e.getCause(); 415 } 416 else if ( e.getCause() instanceof RuntimeException ) 417 { 418 // The fork-join framework breaks the exception handling contract of Callable by re-throwing any 419 // exception caught using a runtime exception. 420 if ( e.getCause().getCause() instanceof IOException ) 421 { 422 throw (IOException) e.getCause().getCause(); 423 } 424 else if ( e.getCause().getCause() instanceof RuntimeException ) 425 { 426 throw (RuntimeException) e.getCause().getCause(); 427 } 428 else if ( e.getCause().getCause() instanceof Error ) 429 { 430 throw (Error) e.getCause().getCause(); 431 } 432 else if ( e.getCause().getCause() instanceof Exception ) 433 { 434 // Checked exception not declared to be thrown by the Callable's 'call' method. 435 throw new UndeclaredThrowableException( e.getCause().getCause() ); 436 } 437 else 438 { 439 throw (RuntimeException) e.getCause(); 440 } 441 } 442 else if ( e.getCause() instanceof Error ) 443 { 444 throw (Error) e.getCause(); 445 } 446 else 447 { 448 // Checked exception not declared to be thrown by the Callable's 'call' method. 449 throw new UndeclaredThrowableException( e.getCause() ); 450 } 451 } 452 } 453 454 /** 455 * Gets a flag indicating that the input of the editor contained a named section. 456 * 457 * @param sectionName The name of the section to test or {@code null}. 458 * 459 * @return {@code true}, if the input of the editor contained a section with name {@code sectionName}; 460 * {@code false}, if the input of the editor did not contain a section with name {@code sectionName}. 461 */ 462 public boolean isSectionPresent( final String sectionName ) 463 { 464 return sectionName != null && this.presenceFlags.get( sectionName ) != null 465 && this.presenceFlags.get( sectionName ); 466 467 } 468 469 /** 470 * Appends the content of a given section to a given buffer. 471 * 472 * @param section The section to render. 473 * @param buffer The buffer to append the content of {@code section} to. 474 * 475 * @return {@code buffer} with content of {@code section} appended. 476 */ 477 private StringBuilder renderSections( final Section section, final StringBuilder buffer ) 478 { 479 if ( section.getStartingLine() != null ) 480 { 481 buffer.append( section.getStartingLine() ).append( this.getLineSeparator() ); 482 } 483 484 buffer.append( section.getHeadContent() ); 485 486 for ( int i = 0, s0 = section.getSections().size(); i < s0; i++ ) 487 { 488 this.renderSections( section.getSections().get( i ), buffer ); 489 } 490 491 buffer.append( section.getTailContent() ); 492 493 if ( section.getEndingLine() != null ) 494 { 495 buffer.append( section.getEndingLine() ).append( this.getLineSeparator() ); 496 } 497 498 return buffer; 499 } 500 501 private final class EditSectionTask implements Callable<Void> 502 { 503 504 private final Section section; 505 506 EditSectionTask( final Section section ) 507 { 508 super(); 509 this.section = section; 510 } 511 512 public Void call() throws IOException 513 { 514 editSection( this.section ); 515 return null; 516 } 517 518 } 519 520 private static String getMessage( final String key, final Object... arguments ) 521 { 522 return MessageFormat.format( ResourceBundle.getBundle( SectionEditor.class.getName() ).getString( key ), 523 arguments ); 524 525 } 526 527 private static String getMessage( final Throwable t ) 528 { 529 return t != null 530 ? t.getMessage() != null && t.getMessage().trim().length() > 0 531 ? t.getMessage() 532 : getMessage( t.getCause() ) 533 : null; 534 535 } 536 537}