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