001 /*
002 * Copyright (c) 2009 The JOMC Project
003 * Copyright (c) 2005 Christian Schulte <schulte2005@users.sourceforge.net>
004 * All rights reserved.
005 *
006 * Redistribution and use in source and binary forms, with or without
007 * modification, are permitted provided that the following conditions
008 * are met:
009 *
010 * o Redistributions of source code must retain the above copyright
011 * notice, this list of conditions and the following disclaimer.
012 *
013 * o Redistributions in binary form must reproduce the above copyright
014 * notice, this list of conditions and the following disclaimer in
015 * the documentation and/or other materials provided with the
016 * distribution.
017 *
018 * THIS SOFTWARE IS PROVIDED BY THE JOMC PROJECT AND CONTRIBUTORS "AS IS"
019 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
020 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
021 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE JOMC PROJECT OR
022 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
023 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
024 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
025 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
026 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
027 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
028 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
029 *
030 * $Id: SectionEditor.java 1869 2010-05-25 16:20:20Z schulte2005 $
031 *
032 */
033 package org.jomc.util;
034
035 import java.io.IOException;
036 import java.text.MessageFormat;
037 import java.util.HashMap;
038 import java.util.Map;
039 import java.util.ResourceBundle;
040 import java.util.Stack;
041
042 /**
043 * Interface to section based editing.
044 * <p>Section based editing is a two phase process of parsing the editor's input into a corresponding hierarchy of
045 * {@code Section} instances, followed by rendering the parsed sections to produce the output of the editor. Method
046 * {@code editLine} returns {@code null} during parsing and the output of the editor on end of input, rendered by
047 * calling method {@code getOutput}. Parsing is backed by methods {@code getSection} and {@code isSectionFinished}.</p>
048 *
049 * @author <a href="mailto:schulte2005@users.sourceforge.net">Christian Schulte</a>
050 * @version $Id: SectionEditor.java 1869 2010-05-25 16:20:20Z schulte2005 $
051 *
052 * @see #edit(java.lang.String)
053 */
054 public class SectionEditor extends LineEditor
055 {
056
057 /** Marker indicating the start of a section. */
058 private static final String DEFAULT_SECTION_START = "SECTION-START[";
059
060 /** Marker indicating the end of a section. */
061 private static final String DEFAULT_SECTION_END = "SECTION-END";
062
063 /** Stack of sections. */
064 private Stack<Section> stack;
065
066 /** Mapping of section names to flags indicating presence of the section. */
067 private final Map<String, Boolean> presenceFlags = new HashMap<String, Boolean>();
068
069 /** Creates a new {@code SectionEditor} instance. */
070 public SectionEditor()
071 {
072 this( null, null );
073 }
074
075 /**
076 * Creates a new {@code SectionEditor} instance taking a string to use for separating lines.
077 *
078 * @param lineSeparator String to use for separating lines.
079 */
080 public SectionEditor( final String lineSeparator )
081 {
082 this( null, lineSeparator );
083 }
084
085 /**
086 * Creates a new {@code SectionEditor} instance taking an editor to chain.
087 *
088 * @param editor The editor to chain.
089 */
090 public SectionEditor( final LineEditor editor )
091 {
092 this( editor, null );
093 }
094
095 /**
096 * Creates a new {@code SectionEditor} instance taking an editor to chain and a string to use for separating lines.
097 *
098 * @param editor The editor to chain.
099 * @param lineSeparator String to use for separating lines.
100 */
101 public SectionEditor( final LineEditor editor, final String lineSeparator )
102 {
103 super( editor, lineSeparator );
104 }
105
106 @Override
107 protected final String editLine( final String line ) throws IOException
108 {
109 if ( this.stack == null )
110 {
111 final Section root = new Section();
112 root.setMode( Section.MODE_HEAD );
113 this.stack = new Stack<Section>();
114 this.stack.push( root );
115 }
116
117 Section current = this.stack.peek();
118 String replacement = null;
119
120 if ( line != null )
121 {
122 final Section child = this.getSection( line );
123
124 if ( child != null )
125 {
126 child.setStartingLine( line );
127 child.setMode( Section.MODE_HEAD );
128
129 if ( current.getMode() == Section.MODE_TAIL && current.getTailContent().length() > 0 )
130 {
131 final Section s = new Section();
132 s.getHeadContent().append( current.getTailContent() );
133 current.getTailContent().setLength( 0 );
134 current.getSections().add( s );
135 current = s;
136 this.stack.push( current );
137 }
138
139 current.getSections().add( child );
140 current.setMode( Section.MODE_TAIL );
141 this.stack.push( child );
142 }
143 else if ( this.isSectionFinished( line ) )
144 {
145 final Section s = this.stack.pop();
146 s.setEndingLine( line );
147
148 if ( this.stack.isEmpty() )
149 {
150 this.stack = null;
151 throw new IOException( this.getMessage( "unexpectedEndOfSection", new Object[]
152 {
153 s.getName() == null ? "/" : s.getName()
154 } ) );
155
156 }
157
158 if ( this.stack.peek().getName() == null && this.stack.size() > 1 )
159 {
160 this.stack.pop();
161 }
162 }
163 else
164 {
165 switch ( current.getMode() )
166 {
167 case Section.MODE_HEAD:
168 current.getHeadContent().append( line ).append( this.getLineSeparator() );
169 break;
170
171 case Section.MODE_TAIL:
172 current.getTailContent().append( line ).append( this.getLineSeparator() );
173 break;
174
175 default:
176 throw new AssertionError( current.getMode() );
177
178 }
179 }
180 }
181 else
182 {
183 final Section root = this.stack.pop();
184
185 if ( !this.stack.isEmpty() )
186 {
187 this.stack = null;
188 throw new IOException( this.getMessage( "unexpectedEndOfSection", new Object[]
189 {
190 root.getName() == null ? "/" : root.getName()
191 } ) );
192
193 }
194
195 replacement = this.getOutput( root );
196 this.stack = null;
197 }
198
199 return replacement;
200 }
201
202 /**
203 * Parses the given line to mark the start of a new section.
204 *
205 * @param line The line to parse.
206 *
207 * @return The section starting at {@code line} or {@code null} if {@code line} does not mark the start of a
208 * section.
209 */
210 protected Section getSection( final String line )
211 {
212 Section s = null;
213
214 if ( line != null )
215 {
216 final int startIndex = line.indexOf( DEFAULT_SECTION_START );
217 if ( startIndex != -1 )
218 {
219 final String name = line.substring( startIndex + DEFAULT_SECTION_START.length(),
220 line.indexOf( ']', startIndex + DEFAULT_SECTION_START.length() ) );
221
222 s = new Section();
223 s.setName( name );
224 }
225 }
226
227 return s;
228 }
229
230 /**
231 * Parses the given line to mark the end of a section.
232 *
233 * @param line The line to parse.
234 *
235 * @return {@code true} if {@code line} marks the end of a section; {@code false} if {@code line} does not mark the
236 * end of a section.
237 */
238 protected boolean isSectionFinished( final String line )
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 ( Section child : section.getSections() )
283 {
284 this.editSections( child );
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() ).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.
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 ( Section child : section.getSections() )
345 {
346 this.renderSections( child, 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 String getMessage( final String key, final Object arguments )
360 {
361 return new MessageFormat( ResourceBundle.getBundle( SectionEditor.class.getName().
362 replace( '.', '/' ) ).getString( key ) ).format( arguments );
363
364 }
365
366 }