001    /*
002     *   Copyright (C) Christian Schulte, 2005-206
003     *   All rights reserved.
004     *
005     *   Redistribution and use in source and binary forms, with or without
006     *   modification, are permitted provided that the following conditions
007     *   are met:
008     *
009     *     o Redistributions of source code must retain the above copyright
010     *       notice, this list of conditions and the following disclaimer.
011     *
012     *     o Redistributions in binary form must reproduce the above copyright
013     *       notice, this list of conditions and the following disclaimer in
014     *       the documentation and/or other materials provided with the
015     *       distribution.
016     *
017     *   THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
018     *   INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
019     *   AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
020     *   THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT,
021     *   INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
022     *   NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
023     *   DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
024     *   THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
025     *   (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
026     *   THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
027     *
028     *   $JOMC: SectionEditor.java 3934 2011-11-12 00:42:08Z schulte2005 $
029     *
030     */
031    package org.jomc.util;
032    
033    import java.io.IOException;
034    import java.text.MessageFormat;
035    import java.util.HashMap;
036    import java.util.Map;
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:schulte2005@users.sourceforge.net">Christian Schulte</a>
048     * @version $JOMC: SectionEditor.java 3934 2011-11-12 00:42:08Z 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        /** Mapping of section names to flags indicating presence of the section. */
065        private final Map<String, Boolean> presenceFlags = new HashMap<String, Boolean>();
066    
067        /** Creates a new {@code SectionEditor} instance. */
068        public SectionEditor()
069        {
070            this( null, null );
071        }
072    
073        /**
074         * Creates a new {@code SectionEditor} instance taking a string to use for separating lines.
075         *
076         * @param lineSeparator String to use for separating lines.
077         */
078        public SectionEditor( final String lineSeparator )
079        {
080            this( null, lineSeparator );
081        }
082    
083        /**
084         * Creates a new {@code SectionEditor} instance taking an editor to chain.
085         *
086         * @param editor The editor to chain.
087         */
088        public SectionEditor( final LineEditor editor )
089        {
090            this( editor, null );
091        }
092    
093        /**
094         * Creates a new {@code SectionEditor} instance taking an editor to chain and a string to use for separating lines.
095         *
096         * @param editor The editor to chain.
097         * @param lineSeparator String to use for separating lines.
098         */
099        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    }