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: SourceFileProcessor.java 5299 2016-08-30 01:50:13Z schulte $
029 *
030 */
031package org.jomc.tools;
032
033import java.io.Closeable;
034import java.io.File;
035import java.io.FileInputStream;
036import java.io.FileOutputStream;
037import java.io.IOException;
038import java.io.InputStreamReader;
039import java.io.OutputStreamWriter;
040import java.io.Reader;
041import java.io.StringWriter;
042import java.io.Writer;
043import java.nio.channels.FileLock;
044import java.text.MessageFormat;
045import java.util.List;
046import java.util.ResourceBundle;
047import java.util.concurrent.CopyOnWriteArrayList;
048import java.util.logging.Level;
049import org.apache.commons.lang.StringUtils;
050import org.apache.velocity.Template;
051import org.apache.velocity.VelocityContext;
052import org.apache.velocity.exception.VelocityException;
053import org.jomc.model.Implementation;
054import org.jomc.model.Implementations;
055import org.jomc.model.Instance;
056import org.jomc.model.Module;
057import org.jomc.model.Specification;
058import org.jomc.tools.model.SourceFileType;
059import org.jomc.tools.model.SourceFilesType;
060import org.jomc.tools.model.SourceSectionType;
061import org.jomc.tools.model.SourceSectionsType;
062import org.jomc.util.LineEditor;
063import org.jomc.util.Section;
064import org.jomc.util.SectionEditor;
065import org.jomc.util.TrailingWhitespaceEditor;
066
067/**
068 * Processes source code files.
069 *
070 * <p>
071 * <b>Use Cases:</b><br/><ul>
072 * <li>{@link #manageSourceFiles(File) }</li>
073 * <li>{@link #manageSourceFiles(Module, File) }</li>
074 * <li>{@link #manageSourceFiles(Specification, File) }</li>
075 * <li>{@link #manageSourceFiles(Implementation, File) }</li>
076 * </ul></p>
077 *
078 * @author <a href="mailto:cs@schulte.it">Christian Schulte</a>
079 * @version $JOMC: SourceFileProcessor.java 5299 2016-08-30 01:50:13Z schulte $
080 */
081public class SourceFileProcessor extends JomcTool
082{
083
084    /**
085     * The source file editor of the instance.
086     */
087    private SourceFileProcessor.SourceFileEditor sourceFileEditor;
088
089    /**
090     * Source files model.
091     */
092    @Deprecated
093    private SourceFilesType sourceFilesType;
094
095    /**
096     * Creates a new {@code SourceFileProcessor} instance.
097     */
098    public SourceFileProcessor()
099    {
100        super();
101    }
102
103    /**
104     * Creates a new {@code SourceFileProcessor} instance taking a {@code SourceFileProcessor} instance to initialize
105     * the instance with.
106     *
107     * @param tool The instance to initialize the new instance with,
108     *
109     * @throws NullPointerException if {@code tool} is {@code null}.
110     * @throws IOException if copying {@code tool} fails.
111     */
112    public SourceFileProcessor( final SourceFileProcessor tool ) throws IOException
113    {
114        super( tool );
115        this.sourceFilesType = tool.sourceFilesType != null ? tool.sourceFilesType.clone() : null;
116        this.sourceFileEditor = tool.sourceFileEditor;
117    }
118
119    /**
120     * Gets the source files model of the instance.
121     * <p>
122     * This accessor method returns a reference to the live object, not a snapshot. Therefore any modification you
123     * make to the returned object will be present inside the object. This is why there is no {@code set} method.
124     * </p>
125     *
126     * @return The source files model of the instance.
127     *
128     * @see #getSourceFileType(org.jomc.model.Specification)
129     * @see #getSourceFileType(org.jomc.model.Implementation)
130     *
131     * @deprecated As of JOMC 1.2, please add source file models to {@code Specification}s and {@code Implementation}s
132     * directly. This method will be removed in version 2.0.
133     */
134    @Deprecated
135    public SourceFilesType getSourceFilesType()
136    {
137        if ( this.sourceFilesType == null )
138        {
139            this.sourceFilesType = new SourceFilesType();
140        }
141
142        return this.sourceFilesType;
143    }
144
145    /**
146     * Gets the model of a specification source file of the modules of the instance.
147     *
148     * @param specification The specification to get a source file model for.
149     *
150     * @return The source file model for {@code specification}. As of JOMC 1.2, this method returns {@code null} if no
151     * source file model is found.
152     *
153     * @throws NullPointerException if {@code specification} is {@code null}.
154     *
155     * @deprecated As of JOMC 1.2, please use method {@link #getSourceFilesType(org.jomc.model.Specification)}. This
156     * method will be removed in version 2.0.
157     */
158    @Deprecated
159    public SourceFileType getSourceFileType( final Specification specification )
160    {
161        if ( specification == null )
162        {
163            throw new NullPointerException( "specification" );
164        }
165
166        SourceFileType sourceFileType = null;
167
168        if ( this.getModules() != null
169                 && this.getModules().getSpecification( specification.getIdentifier() ) != null )
170        {
171            sourceFileType = this.getSourceFilesType().getSourceFile( specification.getIdentifier() );
172
173            if ( sourceFileType == null )
174            {
175                sourceFileType = specification.getAnyObject( SourceFileType.class );
176            }
177        }
178        else if ( this.isLoggable( Level.WARNING ) )
179        {
180            this.log( Level.WARNING, getMessage( "specificationNotFound", specification.getIdentifier() ), null );
181        }
182
183        return sourceFileType;
184    }
185
186    /**
187     * Gets the source files model of a specification of the modules of the instance.
188     *
189     * @param specification The specification to get a source files model for.
190     *
191     * @return The source files model for {@code specification} or {@code null}, if no source files model is found.
192     *
193     * @throws NullPointerException if {@code specification} is {@code null}.
194     *
195     * @since 1.2
196     */
197    public SourceFilesType getSourceFilesType( final Specification specification )
198    {
199        if ( specification == null )
200        {
201            throw new NullPointerException( "specification" );
202        }
203
204        SourceFilesType model = null;
205
206        if ( this.getModules() != null
207                 && this.getModules().getSpecification( specification.getIdentifier() ) != null )
208        {
209            final SourceFileType sourceFileType = this.getSourceFileType( specification );
210
211            if ( sourceFileType != null )
212            {
213                model = new SourceFilesType();
214                model.getSourceFile().add( sourceFileType );
215            }
216            else
217            {
218                model = specification.getAnyObject( SourceFilesType.class );
219            }
220        }
221        else if ( this.isLoggable( Level.WARNING ) )
222        {
223            this.log( Level.WARNING, getMessage( "specificationNotFound", specification.getIdentifier() ), null );
224        }
225
226        return model;
227    }
228
229    /**
230     * Gets the model of an implementation source file of the modules of the instance.
231     *
232     * @param implementation The implementation to get a source file model for.
233     *
234     * @return The source file model for {@code implementation}. As of JOMC 1.2, this method returns {@code null} if no
235     * source file model is found.
236     *
237     * @throws NullPointerException if {@code implementation} is {@code null}.
238     *
239     * @deprecated As of JOMC 1.2, please use method {@link #getSourceFilesType(org.jomc.model.Implementation)}. This
240     * method will be removed in version 2.0.
241     */
242    @Deprecated
243    public SourceFileType getSourceFileType( final Implementation implementation )
244    {
245        if ( implementation == null )
246        {
247            throw new NullPointerException( "implementation" );
248        }
249
250        SourceFileType sourceFileType = null;
251
252        if ( this.getModules() != null
253                 && this.getModules().getImplementation( implementation.getIdentifier() ) != null )
254        {
255            sourceFileType = this.getSourceFilesType().getSourceFile( implementation.getIdentifier() );
256
257            if ( sourceFileType == null )
258            {
259                sourceFileType = implementation.getAnyObject( SourceFileType.class );
260            }
261        }
262        else if ( this.isLoggable( Level.WARNING ) )
263        {
264            this.log( Level.WARNING, getMessage( "implementationNotFound", implementation.getIdentifier() ), null );
265        }
266
267        return sourceFileType;
268    }
269
270    /**
271     * Gets the source files model of an implementation of the modules of the instance.
272     *
273     * @param implementation The implementation to get a source files model for.
274     *
275     * @return The source files model for {@code implementation} or {@code null}, if no source files model is found.
276     *
277     * @throws NullPointerException if {@code implementation} is {@code null}.
278     *
279     * @since 1.2
280     */
281    public SourceFilesType getSourceFilesType( final Implementation implementation )
282    {
283        if ( implementation == null )
284        {
285            throw new NullPointerException( "implementation" );
286        }
287
288        SourceFilesType model = null;
289
290        if ( this.getModules() != null
291                 && this.getModules().getImplementation( implementation.getIdentifier() ) != null )
292        {
293            final SourceFileType sourceFileType = this.getSourceFileType( implementation );
294
295            if ( sourceFileType != null )
296            {
297                model = new SourceFilesType();
298                model.getSourceFile().add( sourceFileType );
299            }
300            else
301            {
302                final Instance instance = this.getModules().getInstance( implementation.getIdentifier() );
303                assert instance != null : "Instance '" + implementation.getIdentifier() + "' not found.";
304                model = instance.getAnyObject( SourceFilesType.class );
305            }
306        }
307        else if ( this.isLoggable( Level.WARNING ) )
308        {
309            this.log( Level.WARNING, getMessage( "implementationNotFound", implementation.getIdentifier() ), null );
310        }
311
312        return model;
313    }
314
315    /**
316     * Gets the source file editor of the instance.
317     *
318     * @return The source file editor of the instance.
319     *
320     * @since 1.2
321     *
322     * @see #setSourceFileEditor(org.jomc.tools.SourceFileProcessor.SourceFileEditor)
323     */
324    public final SourceFileProcessor.SourceFileEditor getSourceFileEditor()
325    {
326        if ( this.sourceFileEditor == null )
327        {
328            this.sourceFileEditor =
329                new SourceFileProcessor.SourceFileEditor( new TrailingWhitespaceEditor( this.getLineSeparator() ),
330                                                          this.getLineSeparator() );
331
332            this.sourceFileEditor.setExecutorService( this.getExecutorService() );
333        }
334
335        return this.sourceFileEditor;
336    }
337
338    /**
339     * Sets the source file editor of the instance.
340     *
341     * @param value The new source file editor of the instance or {@code null}.
342     *
343     * @since 1.2
344     *
345     * @see #getSourceFileEditor()
346     */
347    public final void setSourceFileEditor( final SourceFileProcessor.SourceFileEditor value )
348    {
349        this.sourceFileEditor = value;
350    }
351
352    /**
353     * Gets a new editor for editing the source file of a given specification of the modules of the instance.
354     *
355     * @param specification The specification whose source file to edit.
356     *
357     * @return A new editor for editing the source file of {@code specification}.
358     *
359     * @throws NullPointerException if {@code specification} is {@code null}.
360     *
361     * @deprecated As of JOMC 1.2, please use method {@link #getSourceFileEditor()}. This method will be removed in
362     * version 2.0.
363     *
364     * @see SourceFileEditor#edit(org.jomc.model.Specification, org.jomc.tools.model.SourceFileType, java.io.File)
365     */
366    @Deprecated
367    public SourceFileProcessor.SourceFileEditor getSourceFileEditor( final Specification specification )
368    {
369        if ( specification == null )
370        {
371            throw new NullPointerException( "specification" );
372        }
373
374        return this.getSourceFileEditor();
375    }
376
377    /**
378     * Gets a new editor for editing the source file of a given implementation of the modules of the instance.
379     *
380     * @param implementation The implementation whose source file to edit.
381     *
382     * @return A new editor for editing the source file of {@code implementation}.
383     *
384     * @throws NullPointerException if {@code implementation} is {@code null}.
385     *
386     * @deprecated As of JOMC 1.2, please use method {@link #getSourceFileEditor()}. This method will be removed in
387     * version 2.0.
388     *
389     * @see SourceFileEditor#edit(org.jomc.model.Implementation, org.jomc.tools.model.SourceFileType, java.io.File)
390     */
391    @Deprecated
392    public SourceFileProcessor.SourceFileEditor getSourceFileEditor( final Implementation implementation )
393    {
394        if ( implementation == null )
395        {
396            throw new NullPointerException( "implementation" );
397        }
398
399        return this.getSourceFileEditor();
400    }
401
402    /**
403     * Manages the source files of the modules of the instance.
404     *
405     * @param sourcesDirectory The directory holding the source files to manage.
406     *
407     * @throws NullPointerException if {@code sourcesDirectory} is {@code null}.
408     * @throws IOException if managing source files fails.
409     *
410     * @see #manageSourceFiles(org.jomc.model.Module, java.io.File)
411     */
412    public void manageSourceFiles( final File sourcesDirectory ) throws IOException
413    {
414        if ( sourcesDirectory == null )
415        {
416            throw new NullPointerException( "sourcesDirectory" );
417        }
418
419        if ( this.getModules() != null )
420        {
421            for ( int i = this.getModules().getModule().size() - 1; i >= 0; i-- )
422            {
423                this.manageSourceFiles( this.getModules().getModule().get( i ), sourcesDirectory );
424            }
425        }
426        else if ( this.isLoggable( Level.WARNING ) )
427        {
428            this.log( Level.WARNING, getMessage( "modulesNotFound", this.getModel().getIdentifier() ), null );
429        }
430    }
431
432    /**
433     * Manages the source files of a given module of the modules of the instance.
434     *
435     * @param module The module to process.
436     * @param sourcesDirectory The directory holding the source files to manage.
437     *
438     * @throws NullPointerException if {@code module} or {@code sourcesDirectory} is {@code null}.
439     * @throws IOException if managing source files fails.
440     *
441     * @see #manageSourceFiles(org.jomc.model.Specification, java.io.File)
442     * @see #manageSourceFiles(org.jomc.model.Implementation, java.io.File)
443     */
444    public void manageSourceFiles( final Module module, final File sourcesDirectory ) throws IOException
445    {
446        if ( module == null )
447        {
448            throw new NullPointerException( "module" );
449        }
450        if ( sourcesDirectory == null )
451        {
452            throw new NullPointerException( "sourcesDirectory" );
453        }
454
455        if ( this.getModules() != null && this.getModules().getModule( module.getName() ) != null )
456        {
457            if ( module.getSpecifications() != null )
458            {
459                for ( int i = 0, s0 = module.getSpecifications().getSpecification().size(); i < s0; i++ )
460                {
461                    this.manageSourceFiles( module.getSpecifications().getSpecification().get( i ), sourcesDirectory );
462                }
463            }
464            if ( module.getImplementations() != null )
465            {
466                for ( int i = 0, s0 = module.getImplementations().getImplementation().size(); i < s0; i++ )
467                {
468                    this.manageSourceFiles( module.getImplementations().getImplementation().get( i ), sourcesDirectory );
469                }
470            }
471        }
472        else if ( this.isLoggable( Level.WARNING ) )
473        {
474            this.log( Level.WARNING, getMessage( "moduleNotFound", module.getName() ), null );
475        }
476    }
477
478    /**
479     * Manages the source files of a given specification of the modules of the instance.
480     *
481     * @param specification The specification to process.
482     * @param sourcesDirectory The directory holding the source files to manage.
483     *
484     * @throws NullPointerException if {@code specification} or {@code sourcesDirectory} is {@code null}.
485     * @throws IOException if managing source files fails.
486     *
487     * @see #getSourceFileEditor()
488     * @see #getSourceFilesType(org.jomc.model.Specification)
489     */
490    public void manageSourceFiles( final Specification specification, final File sourcesDirectory ) throws IOException
491    {
492        if ( specification == null )
493        {
494            throw new NullPointerException( "specification" );
495        }
496        if ( sourcesDirectory == null )
497        {
498            throw new NullPointerException( "sourcesDirectory" );
499        }
500
501        if ( this.getModules() != null
502                 && this.getModules().getSpecification( specification.getIdentifier() ) != null )
503        {
504            if ( specification.isClassDeclaration() )
505            {
506                boolean manage = true;
507                final Implementations implementations = this.getModules().getImplementations();
508
509                if ( implementations != null )
510                {
511                    for ( int i = 0, s0 = implementations.getImplementation().size(); i < s0; i++ )
512                    {
513                        final Implementation impl = implementations.getImplementation().get( i );
514
515                        if ( impl.isClassDeclaration() && specification.getClazz().equals( impl.getClazz() ) )
516                        {
517                            this.manageSourceFiles( impl, sourcesDirectory );
518                            manage = false;
519                            break;
520                        }
521                    }
522                }
523
524                if ( manage )
525                {
526                    final SourceFilesType model = this.getSourceFilesType( specification );
527
528                    if ( model != null )
529                    {
530                        for ( int i = 0, s0 = model.getSourceFile().size(); i < s0; i++ )
531                        {
532                            this.getSourceFileEditor().edit(
533                                specification, model.getSourceFile().get( i ), sourcesDirectory );
534
535                        }
536                    }
537                }
538            }
539        }
540        else if ( this.isLoggable( Level.WARNING ) )
541        {
542            this.log( Level.WARNING, getMessage( "specificationNotFound", specification.getIdentifier() ), null );
543        }
544    }
545
546    /**
547     * Manages the source files of a given implementation of the modules of the instance.
548     *
549     * @param implementation The implementation to process.
550     * @param sourcesDirectory The directory holding the source files to manage.
551     *
552     * @throws NullPointerException if {@code implementation} or {@code sourcesDirectory} is {@code null}.
553     * @throws IOException if managing source files fails.
554     *
555     * @see #getSourceFileEditor()
556     * @see #getSourceFilesType(org.jomc.model.Implementation)
557     */
558    public void manageSourceFiles( final Implementation implementation, final File sourcesDirectory )
559        throws IOException
560    {
561        if ( implementation == null )
562        {
563            throw new NullPointerException( "implementation" );
564        }
565        if ( sourcesDirectory == null )
566        {
567            throw new NullPointerException( "sourcesDirectory" );
568        }
569
570        if ( this.getModules() != null
571                 && this.getModules().getImplementation( implementation.getIdentifier() ) != null )
572        {
573            if ( implementation.isClassDeclaration() )
574            {
575                final SourceFilesType model = this.getSourceFilesType( implementation );
576
577                if ( model != null )
578                {
579                    for ( int i = 0, s0 = model.getSourceFile().size(); i < s0; i++ )
580                    {
581                        this.getSourceFileEditor().edit(
582                            implementation, model.getSourceFile().get( i ), sourcesDirectory );
583
584                    }
585                }
586            }
587        }
588        else if ( this.isLoggable( Level.WARNING ) )
589        {
590            this.log( Level.WARNING, getMessage( "implementationNotFound", implementation.getIdentifier() ), null );
591        }
592    }
593
594    private static String getMessage( final String key, final Object... arguments )
595    {
596        if ( key == null )
597        {
598            throw new NullPointerException( "key" );
599        }
600
601        return MessageFormat.format( ResourceBundle.getBundle(
602            SourceFileProcessor.class.getName().replace( '.', '/' ) ).getString( key ), arguments );
603
604    }
605
606    private static String getMessage( final Throwable t )
607    {
608        return t != null
609                   ? t.getMessage() != null && t.getMessage().trim().length() > 0
610                         ? t.getMessage()
611                         : getMessage( t.getCause() )
612                   : null;
613
614    }
615
616    /**
617     * Extension to {@code SectionEditor} adding support for editing source code files.
618     *
619     * @author <a href="mailto:cs@schulte.it">Christian Schulte</a>
620     * @version $JOMC: SourceFileProcessor.java 5299 2016-08-30 01:50:13Z schulte $
621     *
622     * @see #edit(org.jomc.model.Specification, org.jomc.tools.model.SourceFileType, java.io.File)
623     * @see #edit(org.jomc.model.Implementation, org.jomc.tools.model.SourceFileType, java.io.File)
624     */
625    public class SourceFileEditor extends SectionEditor
626    {
627
628        /**
629         * {@code Specification} of the instance or {@code null}.
630         */
631        private Specification specification;
632
633        /**
634         * {@code Implementation} of the instance or {@code null}.
635         */
636        private Implementation implementation;
637
638        /**
639         * The source code file to edit.
640         */
641        private SourceFileType sourceFileType;
642
643        /**
644         * The {@code VelocityContext} of the instance.
645         */
646        private VelocityContext velocityContext;
647
648        /**
649         * List of sections added to the input.
650         */
651        @Deprecated
652        private final List<Section> addedSections = new CopyOnWriteArrayList<Section>();
653
654        /**
655         * List of sections without corresponding model entry.
656         */
657        @Deprecated
658        private final List<Section> unknownSections = new CopyOnWriteArrayList<Section>();
659
660        /**
661         * Creates a new {@code SourceFileEditor} instance.
662         *
663         * @since 1.2
664         */
665        public SourceFileEditor()
666        {
667            this( (LineEditor) null, (String) null );
668        }
669
670        /**
671         * Creates a new {@code SourceFileEditor} instance taking a string to use for separating lines.
672         *
673         * @param lineSeparator String to use for separating lines.
674         *
675         * @since 1.2
676         */
677        public SourceFileEditor( final String lineSeparator )
678        {
679            this( (LineEditor) null, lineSeparator );
680        }
681
682        /**
683         * Creates a new {@code SourceFileEditor} instance taking an editor to chain.
684         *
685         * @param editor The editor to chain.
686         *
687         * @since 1.2
688         */
689        public SourceFileEditor( final LineEditor editor )
690        {
691            this( editor, null );
692        }
693
694        /**
695         * Creates a new {@code SourceFileEditor} instance taking an editor to chain and a string to use for separating
696         * lines.
697         *
698         * @param editor The editor to chain.
699         * @param lineSeparator String to use for separating lines.
700         *
701         * @since 1.2
702         */
703        public SourceFileEditor( final LineEditor editor, final String lineSeparator )
704        {
705            super( editor, lineSeparator );
706        }
707
708        /**
709         * Creates a new {@code SourceFileEditor} taking a {@code Specification} to edit source code of.
710         *
711         * @param specification The specification to edit source code of.
712         *
713         * @deprecated As of JOMC 1.2, please use method {@link #edit(org.jomc.model.Specification, org.jomc.tools.model.SourceFileType, java.io.File)}.
714         * This constructor will be removed in version 2.0.
715         */
716        @Deprecated
717        public SourceFileEditor( final Specification specification )
718        {
719            this( specification, null, null );
720        }
721
722        /**
723         * Creates a new {@code SourceFileEditor} taking a {@code Specification} to edit source code of and a line
724         * separator.
725         *
726         * @param specification The specification to edit source code of.
727         * @param lineSeparator The line separator of the editor.
728         *
729         * @deprecated As of JOMC 1.2, please use method {@link #edit(org.jomc.model.Specification, org.jomc.tools.model.SourceFileType, java.io.File)}.
730         * This constructor will be removed in version 2.0.
731         */
732        @Deprecated
733        public SourceFileEditor( final Specification specification, final String lineSeparator )
734        {
735            this( specification, null, lineSeparator );
736        }
737
738        /**
739         * Creates a new {@code SourceFileEditor} taking a {@code Specification} to edit source code of and an editor to
740         * chain.
741         *
742         * @param specification The specification backing the editor.
743         * @param lineEditor The editor to chain.
744         *
745         * @deprecated As of JOMC 1.2, please use method {@link #edit(org.jomc.model.Specification, org.jomc.tools.model.SourceFileType, java.io.File)}.
746         * This constructor will be removed in version 2.0.
747         */
748        @Deprecated
749        public SourceFileEditor( final Specification specification, final LineEditor lineEditor )
750        {
751            this( specification, lineEditor, null );
752        }
753
754        /**
755         * Creates a new {@code SourceFileEditor} taking a {@code Specification} to edit source code of, an editor to
756         * chain and a line separator.
757         *
758         * @param specification The specification backing the editor.
759         * @param lineEditor The editor to chain.
760         * @param lineSeparator The line separator of the editor.
761         *
762         * @deprecated As of JOMC 1.2, please use method {@link #edit(org.jomc.model.Specification, org.jomc.tools.model.SourceFileType, java.io.File)}.
763         * This constructor will be removed in version 2.0.
764         */
765        @Deprecated
766        public SourceFileEditor( final Specification specification, final LineEditor lineEditor,
767                                 final String lineSeparator )
768        {
769            super( lineEditor, lineSeparator );
770            this.specification = specification;
771            this.implementation = null;
772
773            assert getModules().getSpecification( specification.getIdentifier() ) != null :
774                "Specification '" + specification.getIdentifier() + "' not found.";
775
776        }
777
778        /**
779         * Creates a new {@code SourceFileEditor} taking an {@code Implementation} to edit source code of.
780         *
781         * @param implementation The implementation to edit source code of.
782         *
783         * @deprecated As of JOMC 1.2, please use method {@link #edit(org.jomc.model.Implementation, org.jomc.tools.model.SourceFileType, java.io.File)}.
784         * This constructor will be removed in version 2.0.
785         */
786        @Deprecated
787        public SourceFileEditor( final Implementation implementation )
788        {
789            this( implementation, null, null );
790        }
791
792        /**
793         * Creates a new {@code SourceFileEditor} taking an {@code Implementation} to edit source code of and a line
794         * separator.
795         *
796         * @param implementation The implementation to edit source code of.
797         * @param lineSeparator The line separator of the editor.
798         *
799         * @deprecated As of JOMC 1.2, please use method {@link #edit(org.jomc.model.Implementation, org.jomc.tools.model.SourceFileType, java.io.File)}.
800         * This constructor will be removed in version 2.0.
801         */
802        @Deprecated
803        public SourceFileEditor( final Implementation implementation, final String lineSeparator )
804        {
805            this( implementation, null, lineSeparator );
806        }
807
808        /**
809         * Creates a new {@code SourceFileEditor} taking an {@code Implementation} to edit source code of and an editor
810         * to chain.
811         *
812         * @param implementation The implementation to edit source code of.
813         * @param lineEditor The editor to chain.
814         *
815         * @deprecated As of JOMC 1.2, please use method {@link #edit(org.jomc.model.Implementation, org.jomc.tools.model.SourceFileType, java.io.File)}.
816         * This constructor will be removed in version 2.0.
817         */
818        @Deprecated
819        public SourceFileEditor( final Implementation implementation, final LineEditor lineEditor )
820        {
821            this( implementation, lineEditor, null );
822        }
823
824        /**
825         * Creates a new {@code SourceFileEditor} taking an {@code Implementation} to edit source code of, an editor
826         * to chain and a line separator.
827         *
828         * @param implementation The implementation to edit source code of.
829         * @param lineEditor The editor to chain.
830         * @param lineSeparator The line separator of the editor.
831         *
832         * @deprecated As of JOMC 1.2, please use method {@link #edit(org.jomc.model.Implementation, org.jomc.tools.model.SourceFileType, java.io.File)}.
833         * This constructor will be removed in version 2.0.
834         */
835        @Deprecated
836        public SourceFileEditor( final Implementation implementation, final LineEditor lineEditor,
837                                 final String lineSeparator )
838        {
839            super( lineEditor, lineSeparator );
840            this.implementation = implementation;
841            this.specification = null;
842
843            assert getModules().getImplementation( implementation.getIdentifier() ) != null :
844                "Implementation '" + implementation.getIdentifier() + "' not found.";
845
846        }
847
848        /**
849         * Edits a source file of a given specification.
850         *
851         * @param specification The specification to edit a source file of.
852         * @param sourceFileType The model of the source file to edit.
853         * @param sourcesDirectory The directory holding the source file to edit.
854         *
855         * @throws NullPointerException if {@code specification}, {@code sourceFileType} or {@code sourcesDirectory} is
856         * {@code null}.
857         * @throws IOException if editing fails.
858         *
859         * @since 1.2
860         */
861        public final void edit( final Specification specification, final SourceFileType sourceFileType,
862                                final File sourcesDirectory ) throws IOException
863        {
864            if ( specification == null )
865            {
866                throw new NullPointerException( "specification" );
867            }
868            if ( sourceFileType == null )
869            {
870                throw new NullPointerException( "sourceFileType" );
871            }
872            if ( sourcesDirectory == null )
873            {
874                throw new NullPointerException( "sourcesDirectory" );
875            }
876
877            try
878            {
879                if ( getModules() != null
880                         && getModules().getSpecification( specification.getIdentifier() ) != null )
881                {
882                    this.specification = specification;
883                    this.sourceFileType = sourceFileType;
884                    this.velocityContext = SourceFileProcessor.this.getVelocityContext();
885                    this.velocityContext.put( "specification", specification );
886                    this.velocityContext.put( "smodel", sourceFileType );
887
888                    this.editSourceFile( sourcesDirectory );
889                }
890                else
891                {
892                    throw new IOException( getMessage( "specificationNotFound", specification.getIdentifier() ) );
893                }
894            }
895            finally
896            {
897                this.specification = null;
898                this.implementation = null;
899                this.sourceFileType = null;
900                this.velocityContext = null;
901            }
902        }
903
904        /**
905         * Edits a source file of a given implementation.
906         *
907         * @param implementation The implementation to edit a source file of.
908         * @param sourceFileType The model of the source file to edit.
909         * @param sourcesDirectory The directory holding the source file to edit.
910         *
911         * @throws NullPointerException if {@code implementation}, {@code sourceFileType} or {@code sourcesDirectory} is
912         * {@code null}.
913         * @throws IOException if editing fails.
914         *
915         * @since 1.2
916         */
917        public final void edit( final Implementation implementation, final SourceFileType sourceFileType,
918                                final File sourcesDirectory ) throws IOException
919        {
920            if ( implementation == null )
921            {
922                throw new NullPointerException( "implementation" );
923            }
924            if ( sourceFileType == null )
925            {
926                throw new NullPointerException( "sourceFileType" );
927            }
928            if ( sourcesDirectory == null )
929            {
930                throw new NullPointerException( "sourcesDirectory" );
931            }
932
933            try
934            {
935                if ( getModules() != null
936                         && getModules().getImplementation( implementation.getIdentifier() ) != null )
937                {
938                    this.implementation = implementation;
939                    this.sourceFileType = sourceFileType;
940                    this.velocityContext = SourceFileProcessor.this.getVelocityContext();
941                    this.velocityContext.put( "implementation", implementation );
942                    this.velocityContext.put( "smodel", sourceFileType );
943
944                    this.editSourceFile( sourcesDirectory );
945                }
946                else
947                {
948                    throw new IOException( getMessage( "implementationNotFound", implementation.getIdentifier() ) );
949                }
950            }
951            finally
952            {
953                this.specification = null;
954                this.implementation = null;
955                this.sourceFileType = null;
956                this.velocityContext = null;
957            }
958        }
959
960        /**
961         * Gets a list of sections added to the input.
962         * <p>
963         * This accessor method returns a reference to the live list, not a snapshot. Therefore any modification you
964         * make to the returned list will be present inside the object. This is why there is no {@code set} method
965         * for the added sections property.
966         * </p>
967         *
968         * @return A list of sections added to the input.
969         *
970         * @deprecated As of JOMC 1.2, deprecated without replacement. This method will be removed in version 2.0.
971         */
972        @Deprecated
973        public List<Section> getAddedSections()
974        {
975            return this.addedSections;
976        }
977
978        /**
979         * Gets a list of sections without corresponding model entry.
980         * <p>
981         * This accessor method returns a reference to the live list, not a snapshot. Therefore any modification you
982         * make to the returned list will be present inside the object. This is why there is no {@code set} method
983         * for the unknown sections property.
984         * </p>
985         *
986         * @return A list of sections without corresponding model entry.
987         *
988         * @deprecated As of JOMC 1.2, deprecated without replacement. This method will be removed in version 2.0.
989         */
990        @Deprecated
991        public List<Section> getUnknownSections()
992        {
993            return this.unknownSections;
994        }
995
996        /**
997         * Gets the currently edited source code file.
998         *
999         * @return The currently edited source code file.
1000         *
1001         * @deprecated As of JOMC 1.2, deprecated without replacement. This method will be removed in version 2.0.
1002         */
1003        @Deprecated
1004        protected SourceFileType getSourceFileType()
1005        {
1006            if ( this.sourceFileType == null )
1007            {
1008                if ( this.specification != null )
1009                {
1010                    return SourceFileProcessor.this.getSourceFileType( this.specification );
1011                }
1012
1013                if ( this.implementation != null )
1014                {
1015                    return SourceFileProcessor.this.getSourceFileType( this.implementation );
1016                }
1017            }
1018
1019            return this.sourceFileType;
1020        }
1021
1022        /**
1023         * Gets a new velocity context used for merging templates.
1024         *
1025         * @return A new velocity context used for merging templates.
1026         *
1027         * @throws IOException if creating a new context instance fails.
1028         *
1029         * @deprecated As of JOMC 1.2, deprecated without replacement. This method will be removed in version 2.0.
1030         */
1031        @Deprecated
1032        protected VelocityContext getVelocityContext() throws IOException
1033        {
1034            if ( this.velocityContext == null )
1035            {
1036                final VelocityContext ctx = SourceFileProcessor.this.getVelocityContext();
1037
1038                if ( this.specification != null )
1039                {
1040                    ctx.put( "specification", this.specification );
1041                }
1042
1043                if ( this.implementation != null )
1044                {
1045                    ctx.put( "implementation", this.implementation );
1046                }
1047
1048                return ctx;
1049            }
1050
1051            return this.velocityContext;
1052        }
1053
1054        /**
1055         * {@inheritDoc}
1056         * <p>
1057         * This method creates any sections declared in the model of the source file as returned by method
1058         * {@code getSourceFileType} prior to rendering the output of the editor.
1059         * </p>
1060         *
1061         * @param section The section to start rendering the editor's output with.
1062         *
1063         * @see #createSection(java.lang.String, java.lang.String, org.jomc.tools.model.SourceSectionType)
1064         */
1065        @Override
1066        protected String getOutput( final Section section ) throws IOException
1067        {
1068            this.getAddedSections().clear();
1069            this.getUnknownSections().clear();
1070
1071            final SourceFileType model = this.getSourceFileType();
1072
1073            if ( model != null )
1074            {
1075                this.createSections( model, model.getSourceSections(), section );
1076            }
1077
1078            return super.getOutput( section );
1079        }
1080
1081        /**
1082         * {@inheritDoc}
1083         * <p>
1084         * This method searches the model of the source file for a section matching {@code s} and updates properties
1085         * {@code headContent} and {@code tailContent} of {@code s} according to the templates declared in the model
1086         * as returned by method {@code getSourceFileType}.
1087         * </p>
1088         *
1089         * @param s The section to edit.
1090         */
1091        @Override
1092        protected void editSection( final Section s ) throws IOException
1093        {
1094            try
1095            {
1096                super.editSection( s );
1097
1098                final SourceFileType model = this.getSourceFileType();
1099
1100                if ( s.getName() != null && model != null && model.getSourceSections() != null )
1101                {
1102                    final SourceSectionType sourceSectionType =
1103                        model.getSourceSections().getSourceSection( s.getName() );
1104
1105                    if ( sourceSectionType != null )
1106                    {
1107                        if ( s.getStartingLine() != null )
1108                        {
1109                            s.setStartingLine( getIndentation( sourceSectionType.getIndentationLevel() )
1110                                                   + s.getStartingLine().trim() );
1111
1112                        }
1113                        if ( s.getEndingLine() != null )
1114                        {
1115                            s.setEndingLine( getIndentation( sourceSectionType.getIndentationLevel() )
1116                                                 + s.getEndingLine().trim() );
1117
1118                        }
1119
1120                        VelocityContext ctx = null;
1121
1122                        if ( sourceSectionType.getHeadTemplate() != null
1123                                 && ( !sourceSectionType.isEditable()
1124                                      || s.getHeadContent().toString().trim().length() == 0 ) )
1125                        {
1126                            final StringWriter writer = new StringWriter();
1127                            final Template template = getVelocityTemplate( sourceSectionType.getHeadTemplate() );
1128
1129                            if ( ctx == null )
1130                            {
1131                                ctx = (VelocityContext) this.getVelocityContext().clone();
1132                            }
1133
1134                            ctx.put( "template", template );
1135                            ctx.put( "ssection", sourceSectionType );
1136                            template.merge( ctx, writer );
1137                            writer.close();
1138                            s.getHeadContent().setLength( 0 );
1139                            s.getHeadContent().append( writer.toString() );
1140                            ctx.remove( "template" );
1141                            ctx.remove( "ssection" );
1142                        }
1143
1144                        if ( sourceSectionType.getTailTemplate() != null
1145                                 && ( !sourceSectionType.isEditable()
1146                                      || s.getTailContent().toString().trim().length() == 0 ) )
1147                        {
1148                            final StringWriter writer = new StringWriter();
1149                            final Template template = getVelocityTemplate( sourceSectionType.getTailTemplate() );
1150
1151                            if ( ctx == null )
1152                            {
1153                                ctx = (VelocityContext) this.getVelocityContext().clone();
1154                            }
1155
1156                            ctx.put( "template", template );
1157                            ctx.put( "ssection", sourceSectionType );
1158                            template.merge( ctx, writer );
1159                            writer.close();
1160                            s.getTailContent().setLength( 0 );
1161                            s.getTailContent().append( writer.toString() );
1162                            ctx.remove( "template" );
1163                            ctx.remove( "ssection" );
1164                        }
1165                    }
1166                    else if ( isLoggable( Level.WARNING ) )
1167                    {
1168                        if ( this.implementation != null )
1169                        {
1170                            final Module m =
1171                                getModules().getModuleOfImplementation( this.implementation.getIdentifier() );
1172
1173                            log( Level.WARNING, getMessage(
1174                                 "unknownImplementationSection", m.getName(), this.implementation.getIdentifier(),
1175                                 model.getIdentifier(), s.getName() ), null );
1176
1177                        }
1178                        else if ( this.specification != null )
1179                        {
1180                            final Module m =
1181                                getModules().getModuleOfSpecification( this.specification.getIdentifier() );
1182
1183                            log( Level.WARNING, getMessage(
1184                                 "unknownSpecificationSection", m.getName(), this.specification.getIdentifier(),
1185                                 model.getIdentifier(), s.getName() ), null );
1186
1187                        }
1188
1189                        this.getUnknownSections().add( s );
1190                    }
1191                }
1192            }
1193            catch ( final VelocityException e )
1194            {
1195                // JDK: As of JDK 6, "new IOException( message, cause )".
1196                throw (IOException) new IOException( getMessage( e ) ).initCause( e );
1197            }
1198        }
1199
1200        private void createSections( final SourceFileType sourceFileType, final SourceSectionsType sourceSectionsType,
1201                                     final Section section ) throws IOException
1202        {
1203            if ( sourceSectionsType != null && section != null )
1204            {
1205                for ( int i = 0, s0 = sourceSectionsType.getSourceSection().size(); i < s0; i++ )
1206                {
1207                    final SourceSectionType sourceSectionType = sourceSectionsType.getSourceSection().get( i );
1208                    Section childSection = section.getSection( sourceSectionType.getName() );
1209
1210                    if ( childSection == null && !sourceSectionType.isOptional() )
1211                    {
1212                        childSection = this.createSection( StringUtils.defaultString( sourceFileType.getHeadComment() ),
1213                                                           StringUtils.defaultString( sourceFileType.getTailComment() ),
1214                                                           sourceSectionType );
1215
1216                        section.getSections().add( childSection );
1217
1218                        if ( isLoggable( Level.FINE ) )
1219                        {
1220                            log( Level.FINE, getMessage(
1221                                 "addedSection", sourceFileType.getIdentifier(), childSection.getName() ), null );
1222
1223                        }
1224
1225                        this.getAddedSections().add( childSection );
1226                    }
1227
1228                    this.createSections( sourceFileType, sourceSectionType.getSourceSections(), childSection );
1229                }
1230            }
1231        }
1232
1233        /**
1234         * Creates a new {@code Section} instance for a given {@code SourceSectionType}.
1235         *
1236         * @param headComment Characters to use to start a comment in the source file.
1237         * @param tailComment Characters to use to end a comment in the source file.
1238         * @param sourceSectionType The {@code SourceSectionType} to create a new {@code Section} instance for.
1239         *
1240         * @return A new {@code Section} instance for {@code sourceSectionType}.
1241         *
1242         * @throws NullPointerException if {@code headComment}, {@code tailComment} or {@code sourceSectionType} is
1243         * {@code null}.
1244         * @throws IOException if creating a new {@code Section} instance fails.
1245         *
1246         * @since 1.2
1247         */
1248        private Section createSection( final String headComment, final String tailComment,
1249                                       final SourceSectionType sourceSectionType ) throws IOException
1250        {
1251            if ( headComment == null )
1252            {
1253                throw new NullPointerException( "headComment" );
1254            }
1255            if ( tailComment == null )
1256            {
1257                throw new NullPointerException( "tailComment" );
1258            }
1259            if ( sourceSectionType == null )
1260            {
1261                throw new NullPointerException( "sourceSectionType" );
1262            }
1263
1264            final Section s = new Section();
1265            s.setName( sourceSectionType.getName() );
1266
1267            final StringBuilder head = new StringBuilder( 255 );
1268            head.append( getIndentation( sourceSectionType.getIndentationLevel() ) ).append( headComment );
1269
1270            s.setStartingLine( head + " SECTION-START[" + sourceSectionType.getName() + ']' + tailComment );
1271            s.setEndingLine( head + " SECTION-END" + tailComment );
1272
1273            return s;
1274        }
1275
1276        private void editSourceFile( final File sourcesDirectory ) throws IOException
1277        {
1278            if ( sourcesDirectory == null )
1279            {
1280                throw new NullPointerException( "sourcesDirectory" );
1281            }
1282            if ( !sourcesDirectory.isDirectory() )
1283            {
1284                throw new IOException( getMessage( "directoryNotFound", sourcesDirectory.getAbsolutePath() ) );
1285            }
1286
1287            final SourceFileType model = this.getSourceFileType();
1288
1289            if ( model != null && model.getLocation() != null )
1290            {
1291                final File f = new File( sourcesDirectory, model.getLocation() );
1292
1293                try
1294                {
1295                    String content = "";
1296                    String edited = null;
1297                    boolean creating = false;
1298
1299                    if ( !f.exists() )
1300                    {
1301                        if ( model.getTemplate() != null )
1302                        {
1303                            final StringWriter writer = new StringWriter();
1304                            final Template template = getVelocityTemplate( model.getTemplate() );
1305                            final VelocityContext ctx = this.getVelocityContext();
1306                            ctx.put( "template", template );
1307                            template.merge( ctx, writer );
1308                            writer.close();
1309                            content = writer.toString();
1310                            ctx.remove( "template" );
1311                            creating = true;
1312                        }
1313                    }
1314                    else
1315                    {
1316                        if ( isLoggable( Level.FINER ) )
1317                        {
1318                            log( Level.FINER, getMessage( "reading", f.getAbsolutePath() ), null );
1319                        }
1320
1321                        content = this.readSourceFile( f );
1322                    }
1323
1324                    try
1325                    {
1326                        edited = super.edit( content );
1327                    }
1328                    catch ( final IOException e )
1329                    {
1330                        // JDK: As of JDK 6, "new IOException( message, cause )".
1331                        throw (IOException) new IOException( getMessage(
1332                            "failedEditing", f.getAbsolutePath(), getMessage( e ) ) ).initCause( e );
1333
1334                    }
1335
1336                    if ( !edited.equals( content ) || edited.length() == 0 )
1337                    {
1338                        if ( !f.getParentFile().exists() && !f.getParentFile().mkdirs() )
1339                        {
1340                            throw new IOException( getMessage(
1341                                "failedCreatingDirectory", f.getParentFile().getAbsolutePath() ) );
1342
1343                        }
1344
1345                        if ( isLoggable( Level.INFO ) )
1346                        {
1347                            log( Level.INFO, getMessage(
1348                                 creating ? "creating" : "editing", f.getAbsolutePath() ), null );
1349
1350                        }
1351
1352                        this.writeSourceFile( f, edited );
1353                    }
1354                    else if ( isLoggable( Level.FINER ) )
1355                    {
1356                        log( Level.FINER, getMessage( "unchanged", f.getAbsolutePath() ), null );
1357                    }
1358                }
1359                catch ( final VelocityException e )
1360                {
1361                    // JDK: As of JDK 6, "new IOException( message, cause )".
1362                    throw (IOException) new IOException( getMessage(
1363                        "failedEditing", f.getAbsolutePath(), getMessage( e ) ) ).initCause( e );
1364
1365                }
1366            }
1367        }
1368
1369        private String readSourceFile( final File file ) throws IOException
1370        {
1371            if ( file == null )
1372            {
1373                throw new NullPointerException( "file" );
1374            }
1375
1376            FileInputStream in = null;
1377            Reader reader = null;
1378            FileLock fileLock = null;
1379            try
1380            {
1381                in = new FileInputStream( file );
1382                reader = new InputStreamReader( in, getInputEncoding() );
1383                fileLock = in.getChannel().lock( 0L, file.length(), true );
1384
1385                final StringBuilder appendable =
1386                    new StringBuilder( file.length() > 0L ? Long.valueOf( file.length() ).intValue() : 1 );
1387
1388                final char[] chars = new char[ 524288 ];
1389
1390                for ( int read = reader.read( chars ); read >= 0; read = reader.read( chars ) )
1391                {
1392                    appendable.append( chars, 0, read );
1393                }
1394
1395                fileLock.release();
1396                fileLock = null;
1397
1398                reader.close();
1399                reader = null;
1400
1401                return appendable.toString();
1402            }
1403            finally
1404            {
1405                this.releaseAndClose( fileLock, reader );
1406            }
1407        }
1408
1409        private void writeSourceFile( final File file, final String content ) throws IOException
1410        {
1411            if ( file == null )
1412            {
1413                throw new NullPointerException( "file" );
1414            }
1415            if ( content == null )
1416            {
1417                throw new NullPointerException( "content" );
1418            }
1419
1420            FileOutputStream out = null;
1421            Writer writer = null;
1422            FileLock fileLock = null;
1423            try
1424            {
1425                out = new FileOutputStream( file );
1426                writer = new OutputStreamWriter( out, getOutputEncoding() );
1427                fileLock = out.getChannel().lock();
1428
1429                writer.write( content );
1430                out.getChannel().force( true );
1431
1432                fileLock.release();
1433                fileLock = null;
1434
1435                writer.close();
1436                writer = null;
1437            }
1438            finally
1439            {
1440                this.releaseAndClose( fileLock, writer );
1441            }
1442        }
1443
1444        private void releaseAndClose( final FileLock fileLock, final Closeable closeable )
1445            throws IOException
1446        {
1447            try
1448            {
1449                if ( fileLock != null )
1450                {
1451                    fileLock.release();
1452                }
1453            }
1454            catch ( final IOException e )
1455            {
1456                log( Level.SEVERE, getMessage( e ), e );
1457            }
1458            finally
1459            {
1460                try
1461                {
1462                    if ( closeable != null )
1463                    {
1464                        closeable.close();
1465                    }
1466                }
1467                catch ( final IOException e )
1468                {
1469                    log( Level.SEVERE, getMessage( e ), e );
1470                }
1471            }
1472        }
1473
1474    }
1475
1476}