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 }