1 /*
2 * Copyright (C) 2005 Christian Schulte <cs@schulte.it>
3 * All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions
7 * 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
14 * the documentation and/or other materials provided with the
15 * distribution.
16 *
17 * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
18 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
19 * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
20 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT,
21 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
22 * NOT 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 OF
26 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27 *
28 * $JOMC: SectionEditor.java 5291 2016-08-29 17:31:15Z schulte $
29 *
30 */
31 package org.jomc.util;
32
33 import java.io.IOException;
34 import java.lang.reflect.UndeclaredThrowableException;
35 import java.text.MessageFormat;
36 import java.util.Collection;
37 import java.util.LinkedList;
38 import java.util.List;
39 import java.util.Map;
40 import java.util.ResourceBundle;
41 import java.util.Stack;
42 import java.util.concurrent.Callable;
43 import java.util.concurrent.CancellationException;
44 import java.util.concurrent.ConcurrentHashMap;
45 import java.util.concurrent.ExecutionException;
46 import java.util.concurrent.ExecutorService;
47 import java.util.concurrent.Future;
48
49 /**
50 * Interface to section based editing.
51 * <p>
52 * Section based editing is a two phase process of parsing the editor's input into a corresponding hierarchy of
53 * {@code Section} instances, followed by rendering the parsed sections to produce the output of the editor. Method
54 * {@code editLine} returns {@code null} during parsing and the output of the editor on end of input, rendered by
55 * calling method {@code getOutput}. Parsing is backed by methods {@code getSection} and {@code isSectionFinished}.
56 * </p>
57 *
58 * @author <a href="mailto:cs@schulte.it">Christian Schulte</a>
59 * @version $JOMC: SectionEditor.java 5291 2016-08-29 17:31:15Z schulte $
60 *
61 * @see #edit(java.lang.String)
62 */
63 public class SectionEditor extends LineEditor
64 {
65
66 /**
67 * Marker indicating the start of a section.
68 */
69 private static final String DEFAULT_SECTION_START = "SECTION-START[";
70
71 /**
72 * Marker indicating the end of a section.
73 */
74 private static final String DEFAULT_SECTION_END = "SECTION-END";
75
76 /**
77 * Stack of sections.
78 */
79 private Stack<Section> stack;
80
81 /**
82 * Mapping of section names to flags indicating presence of the section.
83 */
84 private final Map<String, Boolean> presenceFlags = new ConcurrentHashMap<String, Boolean>( 32 );
85
86 /**
87 * The {@code ExecutorService} of the instance.
88 *
89 * @since 1.10
90 */
91 private ExecutorService executorService;
92
93 /**
94 * Creates a new {@code SectionEditor} instance.
95 */
96 public SectionEditor()
97 {
98 this( null, null );
99 }
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 }