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    }