001/*
002 *   Copyright (C) 2005 Christian Schulte <cs@schulte.it>
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 5291 2016-08-29 17:31:15Z schulte $
029 *
030 */
031package org.jomc.util;
032
033import java.io.IOException;
034import java.lang.reflect.UndeclaredThrowableException;
035import java.text.MessageFormat;
036import java.util.Collection;
037import java.util.LinkedList;
038import java.util.List;
039import java.util.Map;
040import java.util.ResourceBundle;
041import java.util.Stack;
042import java.util.concurrent.Callable;
043import java.util.concurrent.CancellationException;
044import java.util.concurrent.ConcurrentHashMap;
045import java.util.concurrent.ExecutionException;
046import java.util.concurrent.ExecutorService;
047import java.util.concurrent.Future;
048
049/**
050 * Interface to section based editing.
051 * <p>
052 * Section based editing is a two phase process of parsing the editor's input into a corresponding hierarchy of
053 * {@code Section} instances, followed by rendering the parsed sections to produce the output of the editor. Method
054 * {@code editLine} returns {@code null} during parsing and the output of the editor on end of input, rendered by
055 * calling method {@code getOutput}. Parsing is backed by methods {@code getSection} and {@code isSectionFinished}.
056 * </p>
057 *
058 * @author <a href="mailto:cs@schulte.it">Christian Schulte</a>
059 * @version $JOMC: SectionEditor.java 5291 2016-08-29 17:31:15Z schulte $
060 *
061 * @see #edit(java.lang.String)
062 */
063public class SectionEditor extends LineEditor
064{
065
066    /**
067     * Marker indicating the start of a section.
068     */
069    private static final String DEFAULT_SECTION_START = "SECTION-START[";
070
071    /**
072     * Marker indicating the end of a section.
073     */
074    private static final String DEFAULT_SECTION_END = "SECTION-END";
075
076    /**
077     * Stack of sections.
078     */
079    private Stack<Section> stack;
080
081    /**
082     * Mapping of section names to flags indicating presence of the section.
083     */
084    private final Map<String, Boolean> presenceFlags = new ConcurrentHashMap<String, Boolean>( 32 );
085
086    /**
087     * The {@code ExecutorService} of the instance.
088     *
089     * @since 1.10
090     */
091    private ExecutorService executorService;
092
093    /**
094     * Creates a new {@code SectionEditor} instance.
095     */
096    public SectionEditor()
097    {
098        this( null, null );
099    }
100
101    /**
102     * Creates a new {@code SectionEditor} instance taking a string to use for separating lines.
103     *
104     * @param lineSeparator String to use for separating lines.
105     */
106    public SectionEditor( final String lineSeparator )
107    {
108        this( null, lineSeparator );
109    }
110
111    /**
112     * Creates a new {@code SectionEditor} instance taking an editor to chain.
113     *
114     * @param editor The editor to chain.
115     */
116    public SectionEditor( final LineEditor editor )
117    {
118        this( editor, null );
119    }
120
121    /**
122     * Creates a new {@code SectionEditor} instance taking an editor to chain and a string to use for separating lines.
123     *
124     * @param editor The editor to chain.
125     * @param lineSeparator String to use for separating lines.
126     */
127    public SectionEditor( final LineEditor editor, final String lineSeparator )
128    {
129        super( editor, lineSeparator );
130    }
131
132    /**
133     * Gets an {@code ExecutorService} used to edit sections in parallel.
134     *
135     * @return An {@code ExecutorService} used to edit sections in parallel or {@code null}, if no such service has
136     * been provided by an application.
137     *
138     * @since 1.10
139     *
140     * @see #setExecutorService(java.util.concurrent.ExecutorService)
141     */
142    public final ExecutorService getExecutorService()
143    {
144        return this.executorService;
145    }
146
147    /**
148     * Sets the {@code ExecutorService} to be used to edit sections in parallel.
149     * <p>
150     * The {@code ExecutorService} to be used to edit sections in parallel is an optional entity. If no such service is
151     * provided by an application, no parallelization is performed. Configuration or lifecycle management of the given
152     * {@code ExecutorService} is the responsibility of the application.
153     * </p>
154     *
155     * @param value The {@code ExecutorService} to be used to edit sections in parallel or {@code null}, to disable any
156     * parallelization.
157     *
158     * @since 1.10
159     *
160     * @see #getExecutorService()
161     */
162    public final void setExecutorService( final ExecutorService value )
163    {
164        this.executorService = value;
165    }
166
167    @Override
168    protected final String editLine( final String line ) throws IOException
169    {
170        if ( this.stack == null )
171        {
172            final Section root = new Section();
173            root.setMode( Section.MODE_HEAD );
174            this.stack = new Stack<Section>();
175            this.stack.push( root );
176        }
177
178        Section current = this.stack.peek();
179        String replacement = null;
180
181        if ( line != null )
182        {
183            final Section child = this.getSection( line );
184
185            if ( child != null )
186            {
187                child.setStartingLine( line );
188                child.setMode( Section.MODE_HEAD );
189
190                if ( current.getMode() == Section.MODE_TAIL && current.getTailContent().length() > 0 )
191                {
192                    final Section s = new Section();
193                    s.getHeadContent().append( current.getTailContent() );
194                    current.getTailContent().setLength( 0 );
195                    current.getSections().add( s );
196                    current = s;
197                    this.stack.push( current );
198                }
199
200                current.getSections().add( child );
201                current.setMode( Section.MODE_TAIL );
202                this.stack.push( child );
203            }
204            else if ( this.isSectionFinished( line ) )
205            {
206                final Section s = this.stack.pop();
207                s.setEndingLine( line );
208
209                if ( this.stack.isEmpty() )
210                {
211                    this.stack = null;
212                    throw new IOException( getMessage( "unexpectedEndOfSection", this.getLineNumber() ) );
213                }
214
215                if ( this.stack.peek().getName() == null && this.stack.size() > 1 )
216                {
217                    this.stack.pop();
218                }
219            }
220            else
221            {
222                switch ( current.getMode() )
223                {
224                    case Section.MODE_HEAD:
225                        current.getHeadContent().append( line ).append( this.getLineSeparator() );
226                        break;
227
228                    case Section.MODE_TAIL:
229                        current.getTailContent().append( line ).append( this.getLineSeparator() );
230                        break;
231
232                    default:
233                        throw new AssertionError( current.getMode() );
234
235                }
236            }
237        }
238        else
239        {
240            final Section root = this.stack.pop();
241
242            if ( !this.stack.isEmpty() )
243            {
244                this.stack = null;
245                throw new IOException( getMessage( "unexpectedEndOfFile", this.getLineNumber(), root.getName() ) );
246            }
247
248            replacement = this.getOutput( root );
249            this.stack = null;
250        }
251
252        return replacement;
253    }
254
255    /**
256     * Parses the given line to mark the start of a new section.
257     *
258     * @param line The line to parse or {@code null}.
259     *
260     * @return The section starting at {@code line} or {@code null}, if {@code line} does not mark the start of a
261     * section.
262     *
263     * @throws IOException if parsing fails.
264     */
265    protected Section getSection( final String line ) throws IOException
266    {
267        Section s = null;
268
269        if ( line != null )
270        {
271            final int markerIndex = line.indexOf( DEFAULT_SECTION_START );
272
273            if ( markerIndex != -1 )
274            {
275                final int startIndex = markerIndex + DEFAULT_SECTION_START.length();
276                final int endIndex = line.indexOf( ']', startIndex );
277
278                if ( endIndex == -1 )
279                {
280                    throw new IOException( getMessage( "sectionMarkerParseFailure", line, this.getLineNumber() ) );
281                }
282
283                s = new Section();
284                s.setName( line.substring( startIndex, endIndex ) );
285            }
286        }
287
288        return s;
289    }
290
291    /**
292     * Parses the given line to mark the end of a section.
293     *
294     * @param line The line to parse or {@code null}.
295     *
296     * @return {@code true}, if {@code line} marks the end of a section; {@code false}, if {@code line} does not mark
297     * the end of a section.
298     *
299     * @throws IOException if parsing fails.
300     */
301    protected boolean isSectionFinished( final String line ) throws IOException
302    {
303        return line != null && line.contains( DEFAULT_SECTION_END );
304    }
305
306    /**
307     * Edits a section.
308     * <p>
309     * This method does not change any content by default. Overriding classes may use this method for editing
310     * sections prior to rendering.
311     * </p>
312     *
313     * @param section The section to edit.
314     *
315     * @throws NullPointerException if {@code section} is {@code null}.
316     * @throws IOException if editing fails.
317     */
318    protected void editSection( final Section section ) throws IOException
319    {
320        if ( section == null )
321        {
322            throw new NullPointerException( "section" );
323        }
324
325        if ( section.getName() != null )
326        {
327            this.presenceFlags.put( section.getName(), Boolean.TRUE );
328        }
329    }
330
331    /**
332     * Creates tasks recursively for editing sections in parallel.
333     *
334     * @param section The section to edit recursively.
335     * @param tasks The collection of tasks to run in parallel.
336     *
337     * @throws NullPointerException if {@code section} or {@code tasks} is {@code null}.
338     * @throws IOException if editing fails.
339     */
340    private void editSections( final Section section, final Collection<EditSectionTask> tasks ) throws IOException
341    {
342        if ( section == null )
343        {
344            throw new NullPointerException( "section" );
345        }
346        if ( tasks == null )
347        {
348            throw new NullPointerException( "tasks" );
349        }
350
351        tasks.add( new EditSectionTask( section ) );
352        for ( int i = 0, s0 = section.getSections().size(); i < s0; i++ )
353        {
354            this.editSections( section.getSections().get( i ), tasks );
355        }
356    }
357
358    /**
359     * Gets the output of the editor.
360     * <p>
361     * This method calls method {@code editSection()} for each section of the editor prior to rendering the sections
362     * to produce the output of the editor.
363     * </p>
364     *
365     * @param section The section to start rendering the editor's output with.
366     *
367     * @return The output of the editor.
368     *
369     * @throws NullPointerException if {@code section} is {@code null}.
370     * @throws IOException if editing or rendering fails.
371     */
372    protected String getOutput( final Section section ) throws IOException
373    {
374        if ( section == null )
375        {
376            throw new NullPointerException( "section" );
377        }
378
379        try
380        {
381            this.presenceFlags.clear();
382            final List<EditSectionTask> tasks = new LinkedList<EditSectionTask>();
383            this.editSections( section, tasks );
384
385            if ( this.getExecutorService() != null && tasks.size() > 1 )
386            {
387                for ( final Future<Void> task : this.getExecutorService().invokeAll( tasks ) )
388                {
389                    task.get();
390                }
391            }
392            else
393            {
394                for ( int i = 0, s0 = tasks.size(); i < s0; i++ )
395                {
396                    tasks.get( i ).call();
397                }
398            }
399
400            return this.renderSections( section, new StringBuilder( 512 ) ).toString();
401        }
402        catch ( final CancellationException e )
403        {
404            throw (IOException) new IOException( getMessage( e ) ).initCause( e );
405        }
406        catch ( final InterruptedException e )
407        {
408            throw (IOException) new IOException( getMessage( e ) ).initCause( e );
409        }
410        catch ( final ExecutionException e )
411        {
412            if ( e.getCause() instanceof IOException )
413            {
414                throw (IOException) e.getCause();
415            }
416            else if ( e.getCause() instanceof RuntimeException )
417            {
418                // The fork-join framework breaks the exception handling contract of Callable by re-throwing any
419                // exception caught using a runtime exception.
420                if ( e.getCause().getCause() instanceof IOException )
421                {
422                    throw (IOException) e.getCause().getCause();
423                }
424                else if ( e.getCause().getCause() instanceof RuntimeException )
425                {
426                    throw (RuntimeException) e.getCause().getCause();
427                }
428                else if ( e.getCause().getCause() instanceof Error )
429                {
430                    throw (Error) e.getCause().getCause();
431                }
432                else if ( e.getCause().getCause() instanceof Exception )
433                {
434                    // Checked exception not declared to be thrown by the Callable's 'call' method.
435                    throw new UndeclaredThrowableException( e.getCause().getCause() );
436                }
437                else
438                {
439                    throw (RuntimeException) e.getCause();
440                }
441            }
442            else if ( e.getCause() instanceof Error )
443            {
444                throw (Error) e.getCause();
445            }
446            else
447            {
448                // Checked exception not declared to be thrown by the Callable's 'call' method.
449                throw new UndeclaredThrowableException( e.getCause() );
450            }
451        }
452    }
453
454    /**
455     * Gets a flag indicating that the input of the editor contained a named section.
456     *
457     * @param sectionName The name of the section to test or {@code null}.
458     *
459     * @return {@code true}, if the input of the editor contained a section with name {@code sectionName};
460     * {@code false}, if the input of the editor did not contain a section with name {@code sectionName}.
461     */
462    public boolean isSectionPresent( final String sectionName )
463    {
464        return sectionName != null && this.presenceFlags.get( sectionName ) != null
465                   && this.presenceFlags.get( sectionName );
466
467    }
468
469    /**
470     * Appends the content of a given section to a given buffer.
471     *
472     * @param section The section to render.
473     * @param buffer The buffer to append the content of {@code section} to.
474     *
475     * @return {@code buffer} with content of {@code section} appended.
476     */
477    private StringBuilder renderSections( final Section section, final StringBuilder buffer )
478    {
479        if ( section.getStartingLine() != null )
480        {
481            buffer.append( section.getStartingLine() ).append( this.getLineSeparator() );
482        }
483
484        buffer.append( section.getHeadContent() );
485
486        for ( int i = 0, s0 = section.getSections().size(); i < s0; i++ )
487        {
488            this.renderSections( section.getSections().get( i ), buffer );
489        }
490
491        buffer.append( section.getTailContent() );
492
493        if ( section.getEndingLine() != null )
494        {
495            buffer.append( section.getEndingLine() ).append( this.getLineSeparator() );
496        }
497
498        return buffer;
499    }
500
501    private final class EditSectionTask implements Callable<Void>
502    {
503
504        private final Section section;
505
506        EditSectionTask( final Section section )
507        {
508            super();
509            this.section = section;
510        }
511
512        public Void call() throws IOException
513        {
514            editSection( this.section );
515            return null;
516        }
517
518    }
519
520    private static String getMessage( final String key, final Object... arguments )
521    {
522        return MessageFormat.format( ResourceBundle.getBundle( SectionEditor.class.getName() ).getString( key ),
523                                     arguments );
524
525    }
526
527    private static String getMessage( final Throwable t )
528    {
529        return t != null
530                   ? t.getMessage() != null && t.getMessage().trim().length() > 0
531                         ? t.getMessage()
532                         : getMessage( t.getCause() )
533                   : null;
534
535    }
536
537}