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    }