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 }