View Javadoc
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 }