View Javadoc

1   /*
2    *   Copyright (C) Christian Schulte, 2005-206
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 4613 2012-09-22 10:07:08Z schulte $
29   *
30   */
31  package org.jomc.util;
32  
33  import java.io.IOException;
34  import java.text.MessageFormat;
35  import java.util.HashMap;
36  import java.util.Map;
37  import java.util.ResourceBundle;
38  import java.util.Stack;
39  
40  /**
41   * Interface to section based editing.
42   * <p>Section based editing is a two phase process of parsing the editor's input into a corresponding hierarchy of
43   * {@code Section} instances, followed by rendering the parsed sections to produce the output of the editor. Method
44   * {@code editLine} returns {@code null} during parsing and the output of the editor on end of input, rendered by
45   * calling method {@code getOutput}. Parsing is backed by methods {@code getSection} and {@code isSectionFinished}.</p>
46   *
47   * @author <a href="mailto:cs@schulte.it">Christian Schulte</a>
48   * @version $JOMC: SectionEditor.java 4613 2012-09-22 10:07:08Z schulte $
49   *
50   * @see #edit(java.lang.String)
51   */
52  public class SectionEditor extends LineEditor
53  {
54  
55      /** Marker indicating the start of a section. */
56      private static final String DEFAULT_SECTION_START = "SECTION-START[";
57  
58      /** Marker indicating the end of a section. */
59      private static final String DEFAULT_SECTION_END = "SECTION-END";
60  
61      /** Stack of sections. */
62      private Stack<Section> stack;
63  
64      /** Mapping of section names to flags indicating presence of the section. */
65      private final Map<String, Boolean> presenceFlags = new HashMap<String, Boolean>();
66  
67      /** Creates a new {@code SectionEditor} instance. */
68      public SectionEditor()
69      {
70          this( null, null );
71      }
72  
73      /**
74       * Creates a new {@code SectionEditor} instance taking a string to use for separating lines.
75       *
76       * @param lineSeparator String to use for separating lines.
77       */
78      public SectionEditor( final String lineSeparator )
79      {
80          this( null, lineSeparator );
81      }
82  
83      /**
84       * Creates a new {@code SectionEditor} instance taking an editor to chain.
85       *
86       * @param editor The editor to chain.
87       */
88      public SectionEditor( final LineEditor editor )
89      {
90          this( editor, null );
91      }
92  
93      /**
94       * Creates a new {@code SectionEditor} instance taking an editor to chain and a string to use for separating lines.
95       *
96       * @param editor The editor to chain.
97       * @param lineSeparator String to use for separating lines.
98       */
99      public SectionEditor( final LineEditor editor, final String lineSeparator )
100     {
101         super( editor, lineSeparator );
102     }
103 
104     @Override
105     protected final String editLine( final String line ) throws IOException
106     {
107         if ( this.stack == null )
108         {
109             final Section root = new Section();
110             root.setMode( Section.MODE_HEAD );
111             this.stack = new Stack<Section>();
112             this.stack.push( root );
113         }
114 
115         Section current = this.stack.peek();
116         String replacement = null;
117 
118         if ( line != null )
119         {
120             final Section child = this.getSection( line );
121 
122             if ( child != null )
123             {
124                 child.setStartingLine( line );
125                 child.setMode( Section.MODE_HEAD );
126 
127                 if ( current.getMode() == Section.MODE_TAIL && current.getTailContent().length() > 0 )
128                 {
129                     final Section s = new Section();
130                     s.getHeadContent().append( current.getTailContent() );
131                     current.getTailContent().setLength( 0 );
132                     current.getSections().add( s );
133                     current = s;
134                     this.stack.push( current );
135                 }
136 
137                 current.getSections().add( child );
138                 current.setMode( Section.MODE_TAIL );
139                 this.stack.push( child );
140             }
141             else if ( this.isSectionFinished( line ) )
142             {
143                 final Section s = this.stack.pop();
144                 s.setEndingLine( line );
145 
146                 if ( this.stack.isEmpty() )
147                 {
148                     this.stack = null;
149                     throw new IOException( getMessage( "unexpectedEndOfSection", this.getLineNumber() ) );
150                 }
151 
152                 if ( this.stack.peek().getName() == null && this.stack.size() > 1 )
153                 {
154                     this.stack.pop();
155                 }
156             }
157             else
158             {
159                 switch ( current.getMode() )
160                 {
161                     case Section.MODE_HEAD:
162                         current.getHeadContent().append( line ).append( this.getLineSeparator() );
163                         break;
164 
165                     case Section.MODE_TAIL:
166                         current.getTailContent().append( line ).append( this.getLineSeparator() );
167                         break;
168 
169                     default:
170                         throw new AssertionError( current.getMode() );
171 
172                 }
173             }
174         }
175         else
176         {
177             final Section root = this.stack.pop();
178 
179             if ( !this.stack.isEmpty() )
180             {
181                 this.stack = null;
182                 throw new IOException( getMessage( "unexpectedEndOfFile", this.getLineNumber(), root.getName() ) );
183             }
184 
185             replacement = this.getOutput( root );
186             this.stack = null;
187         }
188 
189         return replacement;
190     }
191 
192     /**
193      * Parses the given line to mark the start of a new section.
194      *
195      * @param line The line to parse or {@code null}.
196      *
197      * @return The section starting at {@code line} or {@code null}, if {@code line} does not mark the start of a
198      * section.
199      *
200      * @throws IOException if parsing fails.
201      */
202     protected Section getSection( final String line ) throws IOException
203     {
204         Section s = null;
205 
206         if ( line != null )
207         {
208             final int markerIndex = line.indexOf( DEFAULT_SECTION_START );
209 
210             if ( markerIndex != -1 )
211             {
212                 final int startIndex = markerIndex + DEFAULT_SECTION_START.length();
213                 final int endIndex = line.indexOf( ']', startIndex );
214 
215                 if ( endIndex == -1 )
216                 {
217                     throw new IOException( getMessage( "sectionMarkerParseFailure", line, this.getLineNumber() ) );
218                 }
219 
220                 s = new Section();
221                 s.setName( line.substring( startIndex, endIndex ) );
222             }
223         }
224 
225         return s;
226     }
227 
228     /**
229      * Parses the given line to mark the end of a section.
230      *
231      * @param line The line to parse or {@code null}.
232      *
233      * @return {@code true}, if {@code line} marks the end of a section; {@code false}, if {@code line} does not mark
234      * the end of a section.
235      *
236      * @throws IOException if parsing fails.
237      */
238     protected boolean isSectionFinished( final String line ) throws IOException
239     {
240         return line != null && line.indexOf( DEFAULT_SECTION_END ) != -1;
241     }
242 
243     /**
244      * Edits a section.
245      * <p>This method does not change any content by default. Overriding classes may use this method for editing
246      * sections prior to rendering.</p>
247      *
248      * @param section The section to edit.
249      *
250      * @throws NullPointerException if {@code section} is {@code null}.
251      * @throws IOException if editing fails.
252      */
253     protected void editSection( final Section section ) throws IOException
254     {
255         if ( section == null )
256         {
257             throw new NullPointerException( "section" );
258         }
259 
260         if ( section.getName() != null )
261         {
262             this.presenceFlags.put( section.getName(), Boolean.TRUE );
263         }
264     }
265 
266     /**
267      * Edits a section recursively.
268      *
269      * @param section The section to edit recursively.
270      *
271      * @throws NullPointerException if {@code section} is {@code null}.
272      * @throws IOException if editing fails.
273      */
274     private void editSections( final Section section ) throws IOException
275     {
276         if ( section == null )
277         {
278             throw new NullPointerException( "section" );
279         }
280 
281         this.editSection( section );
282         for ( int i = 0, s0 = section.getSections().size(); i < s0; i++ )
283         {
284             this.editSections( section.getSections().get( i ) );
285         }
286     }
287 
288     /**
289      * Gets the output of the editor.
290      * <p>This method calls method {@code editSection()} for each section of the editor prior to rendering the sections
291      * to produce the output of the editor.</p>
292      *
293      * @param section The section to start rendering the editor's output with.
294      *
295      * @return The output of the editor.
296      *
297      * @throws NullPointerException if {@code section} is {@code null}.
298      * @throws IOException if editing or rendering fails.
299      */
300     protected String getOutput( final Section section ) throws IOException
301     {
302         if ( section == null )
303         {
304             throw new NullPointerException( "section" );
305         }
306 
307         this.presenceFlags.clear();
308         this.editSections( section );
309         return this.renderSections( section, new StringBuilder( 512 ) ).toString();
310     }
311 
312     /**
313      * Gets a flag indicating that the input of the editor contained a named section.
314      *
315      * @param sectionName The name of the section to test or {@code null}.
316      *
317      * @return {@code true}, if the input of the editor contained a section with name {@code sectionName};
318      * {@code false}, if the input of the editor did not contain a section with name {@code sectionName}.
319      */
320     public boolean isSectionPresent( final String sectionName )
321     {
322         return sectionName != null && this.presenceFlags.get( sectionName ) != null
323                && this.presenceFlags.get( sectionName ).booleanValue();
324 
325     }
326 
327     /**
328      * Appends the content of a given section to a given buffer.
329      *
330      * @param section The section to render.
331      * @param buffer The buffer to append the content of {@code section} to.
332      *
333      * @return {@code buffer} with content of {@code section} appended.
334      */
335     private StringBuilder renderSections( final Section section, final StringBuilder buffer )
336     {
337         if ( section.getStartingLine() != null )
338         {
339             buffer.append( section.getStartingLine() ).append( this.getLineSeparator() );
340         }
341 
342         buffer.append( section.getHeadContent() );
343 
344         for ( int i = 0, s0 = section.getSections().size(); i < s0; i++ )
345         {
346             this.renderSections( section.getSections().get( i ), buffer );
347         }
348 
349         buffer.append( section.getTailContent() );
350 
351         if ( section.getEndingLine() != null )
352         {
353             buffer.append( section.getEndingLine() ).append( this.getLineSeparator() );
354         }
355 
356         return buffer;
357     }
358 
359     private static String getMessage( final String key, final Object... arguments )
360     {
361         return MessageFormat.format( ResourceBundle.getBundle( SectionEditor.class.getName().
362             replace( '.', '/' ) ).getString( key ), arguments );
363 
364     }
365 
366 }