001 /*
002 * Copyright (c) 2009 The JOMC Project
003 * Copyright (c) 2005 Christian Schulte <cs@jomc.org>
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 891 2009-11-02 03:40:00Z schulte2005 $
031 *
032 */
033 package org.jomc.util;
034
035 import java.io.IOException;
036 import java.text.MessageFormat;
037 import java.util.ResourceBundle;
038 import java.util.Stack;
039
040 /**
041 * Interface to section based editing.
042 * <p>Section based editing is a two phase process of parsing the editor's input into a corresponding hierarchy of
043 * {@code Section} instances, followed by rendering the parsed sections to produce the output of the editor. Method
044 * {@code editLine} returns {@code null} during parsing and the output of the editor on end of input, rendered by
045 * calling method {@code getOutput}. Parsing is backed by methods {@code getSection} and {@code isSectionFinished}.</p>
046 *
047 * @author <a href="mailto:cs@jomc.org">Christian Schulte</a>
048 * @version $Id: SectionEditor.java 891 2009-11-02 03:40:00Z schulte2005 $
049 *
050 * @see #edit(java.lang.String)
051 */
052 public class SectionEditor extends LineEditor
053 {
054
055 /** Marker indicating the start of a section. */
056 private static final String DEFAULT_SECTION_START = "SECTION-START[";
057
058 /** Marker indicating the end of a section. */
059 private static final String DEFAULT_SECTION_END = "SECTION-END";
060
061 /** Stack of sections. */
062 private Stack<Section> stack;
063
064 /** Creates a new {@code SectionEditor} instance. */
065 public SectionEditor()
066 {
067 super();
068 }
069
070 /**
071 * Creates a new {@code SectionEditor} instance taking a string to use for separating lines.
072 *
073 * @param lineSeparator String to use for separating lines.
074 */
075 public SectionEditor( final String lineSeparator )
076 {
077 super( lineSeparator );
078 }
079
080 /**
081 * Creates a new {@code SectionEditor} instance taking an editor to chain.
082 *
083 * @param editor The editor to chain.
084 */
085 public SectionEditor( final LineEditor editor )
086 {
087 super( editor );
088 }
089
090 /**
091 * Creates a new {@code SectionEditor} instance taking an editor to chain and a string to use for separating lines.
092 *
093 * @param editor The editor to chain.
094 * @param lineSeparator String to use for separating lines.
095 */
096 public SectionEditor( final LineEditor editor, final String lineSeparator )
097 {
098 super( editor, lineSeparator );
099 }
100
101 @Override
102 protected final String editLine( final String line ) throws IOException
103 {
104 if ( this.stack == null )
105 {
106 final Section root = new Section();
107 root.setMode( Section.MODE_HEAD );
108 this.stack = new Stack<Section>();
109 this.stack.push( root );
110 }
111
112 Section current = this.stack.peek();
113 String replacement = null;
114
115 if ( line != null )
116 {
117 final Section child = this.getSection( line );
118
119 if ( child != null )
120 {
121 child.setStartingLine( line );
122 child.setMode( Section.MODE_HEAD );
123
124 if ( current.getMode() == Section.MODE_TAIL && current.getTailContent().length() > 0 )
125 {
126 final Section s = new Section();
127 s.getHeadContent().append( current.getTailContent() );
128 current.getTailContent().setLength( 0 );
129 current.getSections().add( s );
130 current = s;
131 this.stack.push( current );
132 }
133
134 current.getSections().add( child );
135 current.setMode( Section.MODE_TAIL );
136 this.stack.push( child );
137 }
138 else if ( this.isSectionFinished( line ) )
139 {
140 final Section s = this.stack.pop();
141 s.setEndingLine( line );
142
143 if ( this.stack.isEmpty() )
144 {
145 this.stack = null;
146 throw new IOException( this.getMessage( "unexpectedEndOfSection", new Object[]
147 {
148 s.getName() == null ? "/" : s.getName()
149 } ) );
150
151 }
152
153 if ( this.stack.peek().getName() == null && this.stack.size() > 1 )
154 {
155 this.stack.pop();
156 }
157 }
158 else
159 {
160 switch ( current.getMode() )
161 {
162 case Section.MODE_HEAD:
163 current.getHeadContent().append( line ).append( this.getLineSeparator() );
164 break;
165
166 case Section.MODE_TAIL:
167 current.getTailContent().append( line ).append( this.getLineSeparator() );
168 break;
169
170 default:
171 throw new AssertionError( current.getMode() );
172
173 }
174 }
175 }
176 else
177 {
178 final Section root = this.stack.pop();
179
180 if ( !this.stack.isEmpty() )
181 {
182 this.stack = null;
183 throw new IOException( this.getMessage( "unexpectedEndOfSection", new Object[]
184 {
185 root.getName() == null ? "/" : root.getName()
186 } ) );
187
188 }
189
190 replacement = this.getOutput( root );
191 this.stack = null;
192 }
193
194 return replacement;
195 }
196
197 /**
198 * Parses the given line to mark the start of a new section.
199 *
200 * @param line The line to parse.
201 *
202 * @return The section starting at {@code line} or {@code null} if {@code line} does not mark the start of a
203 * section.
204 */
205 protected Section getSection( final String line )
206 {
207 Section s = null;
208
209 if ( line != null )
210 {
211 final int startIndex = line.indexOf( DEFAULT_SECTION_START );
212 if ( startIndex != -1 )
213 {
214 final String name = line.substring( startIndex + DEFAULT_SECTION_START.length(),
215 line.indexOf( ']', startIndex + DEFAULT_SECTION_START.length() ) );
216
217 s = new Section();
218 s.setName( name );
219 }
220 }
221
222 return s;
223 }
224
225 /**
226 * Parses the given line to mark the end of a section.
227 *
228 * @param line The line to parse.
229 *
230 * @return {@code true} if {@code line} marks the end of a section; {@code false} if {@code line} does not mark the
231 * end of a section.
232 */
233 protected boolean isSectionFinished( final String line )
234 {
235 return line != null && line.indexOf( DEFAULT_SECTION_END ) != -1;
236 }
237
238 /**
239 * Edits a section.
240 * <p>This method does not change any content by default. Overriding classes may use this method for editing
241 * sections prior to rendering.</p>
242 *
243 * @param section The section to edit.
244 *
245 * @throws NullPointerException if {@code section} is {@code null}.
246 * @throws IOException if editing fails.
247 */
248 protected void editSection( final Section section ) throws IOException
249 {
250 if ( section == null )
251 {
252 throw new NullPointerException( "section" );
253 }
254 }
255
256 /**
257 * Edits a section recursively.
258 *
259 * @param section The section to edit recursively.
260 *
261 * @throws NullPointerException if {@code section} is {@code null}.
262 * @throws IOException if editing fails.
263 */
264 private void editSections( final Section section ) throws IOException
265 {
266 if ( section == null )
267 {
268 throw new NullPointerException( "section" );
269 }
270
271 this.editSection( section );
272 for ( Section child : section.getSections() )
273 {
274 this.editSections( child );
275 }
276 }
277
278 /**
279 * Gets the output of the editor.
280 * <p>This method calls method {@code editSection()} for each section of the editor prior to rendering the sections
281 * to produce the output of the editor.</p>
282 *
283 * @param section The section to start rendering the editor's output with.
284 *
285 * @return The output of the editor.
286 *
287 * @throws NullPointerException if {@code section} is {@code null}.
288 * @throws IOException if editing or rendering fails.
289 */
290 protected String getOutput( final Section section ) throws IOException
291 {
292 if ( section == null )
293 {
294 throw new NullPointerException( "section" );
295 }
296
297 this.editSections( section );
298 return this.renderSections( section, new StringBuilder() ).toString();
299 }
300
301 /**
302 * Appends the content of a given section to a given buffer.
303 *
304 * @param section The section to render.
305 * @param buffer The buffer to append the content of {@code section} to.
306 *
307 * @return {@code buffer} with content of {@code section} appended.
308 */
309 private StringBuilder renderSections( final Section section, final StringBuilder buffer )
310 {
311 if ( section.getStartingLine() != null )
312 {
313 buffer.append( section.getStartingLine() ).append( this.getLineSeparator() );
314 }
315
316 buffer.append( section.getHeadContent() );
317
318 for ( Section child : section.getSections() )
319 {
320 this.renderSections( child, buffer );
321 }
322
323 buffer.append( section.getTailContent() );
324
325 if ( section.getEndingLine() != null )
326 {
327 buffer.append( section.getEndingLine() ).append( this.getLineSeparator() );
328 }
329
330 return buffer;
331 }
332
333 private String getMessage( final String key, final Object arguments )
334 {
335 return new MessageFormat( ResourceBundle.getBundle( SectionEditor.class.getName().
336 replace( '.', '/' ) ).getString( key ) ).format( arguments );
337
338 }
339
340 }