001package org.apache.maven.doxia.macro.snippet;
002
003/*
004 * Licensed to the Apache Software Foundation (ASF) under one
005 * or more contributor license agreements.  See the NOTICE file
006 * distributed with this work for additional information
007 * regarding copyright ownership.  The ASF licenses this file
008 * to you under the Apache License, Version 2.0 (the
009 * "License"); you may not use this file except in compliance
010 * with the License.  You may obtain a copy of the License at
011 *
012 *   http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing,
015 * software distributed under the License is distributed on an
016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
017 * KIND, either express or implied.  See the License for the
018 * specific language governing permissions and limitations
019 * under the License.
020 */
021
022import java.io.BufferedReader;
023import java.io.IOException;
024import java.io.InputStreamReader;
025import java.net.URL;
026import java.util.ArrayList;
027import java.util.List;
028import java.util.regex.Pattern;
029
030import org.codehaus.plexus.util.StringUtils;
031import org.codehaus.plexus.util.IOUtil;
032
033/**
034 * Utility class for reading snippets.
035 */
036public class SnippetReader
037{
038    /** System-dependent EOL. */
039    private static final String EOL = System.getProperty( "line.separator" );
040
041    /** The source. */
042    private URL source;
043
044    /** The encoding of the source. */
045    private String encoding;
046
047    /**
048     * Constructor.
049     *
050     * @param src The source
051     * @param encoding The file encoding
052     */
053    public SnippetReader( URL src, String encoding )
054    {
055        this.source = src;
056        this.encoding = encoding;
057    }
058
059    /**
060     * Constructor.
061     *
062     * @param src The source
063     */
064    public SnippetReader( URL src )
065    {
066        this( src, null ) ;
067    }
068
069    /**
070     * Reads the snippet with given id.
071     *
072     * @param snippetId The id of the snippet.
073     * @return The snippet.
074     * @throws java.io.IOException if something goes wrong.
075     */
076    public StringBuffer readSnippet( String snippetId )
077        throws IOException
078    {
079        List<String> lines = readLines( snippetId );
080        int minIndent = minIndent( lines );
081        StringBuffer result = new StringBuffer();
082        for ( String line : lines )
083        {
084            result.append( line.substring( minIndent ) );
085            result.append( EOL );
086        }
087        return result;
088    }
089
090    /**
091     * Returns the minimal indent of all the lines in the given List.
092     *
093     * @param lines A List of lines.
094     * @return the minimal indent.
095     */
096    int minIndent( List<String> lines )
097    {
098        int minIndent = Integer.MAX_VALUE;
099        for ( String line : lines )
100        {
101            minIndent = Math.min( minIndent, indent( line ) );
102        }
103        return minIndent;
104    }
105
106    /**
107     * Returns the indent of the given line.
108     *
109     * @param line A line.
110     * @return the indent.
111     */
112    int indent( String line )
113    {
114        char[] chars = line.toCharArray();
115        int indent = 0;
116        for ( ; indent < chars.length; indent++ )
117        {
118            if ( chars[indent] != ' ' )
119            {
120                break;
121            }
122        }
123        return indent;
124    }
125
126    /**
127     * Reads the snippet and returns the lines in a List.
128     *
129     * @param snippetId The id of the snippet.
130     * @return A List of lines.
131     * @throws IOException if something goes wrong.
132     */
133    private List<String> readLines( String snippetId )
134        throws IOException
135    {
136        BufferedReader reader;
137        if ( encoding == null || "".equals( encoding ) )
138        {
139            reader = new BufferedReader( new InputStreamReader( source.openStream() ) );
140        }
141        else
142        {
143            reader = new BufferedReader( new InputStreamReader( source.openStream(), encoding ) );
144        }
145
146        List<String> lines = new ArrayList<>();
147        try
148        {
149            boolean capture = false;
150            String line;
151            boolean foundStart = false;
152            boolean foundEnd = false;
153            boolean hasSnippetId = StringUtils.isNotEmpty( snippetId );
154            while ( ( line = reader.readLine() ) != null )
155            {
156                if ( !hasSnippetId )
157                {
158                    lines.add( line );
159                }
160                else
161                {
162                    if ( isStart( snippetId, line ) )
163                    {
164                        capture = true;
165                        foundStart = true;
166                    }
167                    else if ( isEnd( snippetId, line ) )
168                    {
169                        foundEnd = true;
170                        break;
171                    }
172                    else if ( capture )
173                    {
174                        lines.add( line );
175                    }
176                }
177            }
178
179            if ( hasSnippetId && !foundStart )
180            {
181                throw new IOException( "Failed to find START of snippet " + snippetId + " in file at URL: " + source );
182            }
183            if ( hasSnippetId && !foundEnd )
184            {
185                throw new IOException( "Failed to find END of snippet " + snippetId + " in file at URL: " + source );
186            }
187        }
188        finally
189        {
190            IOUtil.close( reader );
191        }
192        return lines;
193    }
194
195    /**
196     * Determines if the given line is a start demarcator.
197     *
198     * @param snippetId the id of the snippet.
199     * @param line the line.
200     * @return True, if the line is a start demarcator.
201     */
202    protected boolean isStart( String snippetId, String line )
203    {
204        return isDemarcator( snippetId, "START", line );
205    }
206
207    /**
208     * Determines if the given line is a demarcator.
209     *
210     * @param snippetId the id of the snippet.
211     * @param what Identifier for the demarcator.
212     * @param line the line.
213     * @return True, if the line is a start demarcator.
214     */
215    protected static boolean isDemarcator( String snippetId, String what, String line )
216    {
217        // SNIPPET and what are case insensitive
218        // SNIPPET and what can switch order
219        String snippetRegExp = "(^|\\W)(?i:SNIPPET)($|\\W)";
220        String snippetIdRegExp = "(^|\\W)" + snippetId + "($|\\W)";
221        String whatRegExp = "(^|\\W)(?i:" + what + ")($|\\W)";
222        
223        return Pattern.compile( snippetRegExp ).matcher( line ).find()
224            && Pattern.compile( whatRegExp ).matcher( line ).find()
225            && Pattern.compile( snippetIdRegExp ).matcher( line ).find();
226    }
227
228    /**
229     * Determines if the given line is an end demarcator.
230     *
231     * @param snippetId the id of the snippet.
232     * @param line the line.
233     * @return True, if the line is an end demarcator.
234     */
235    protected boolean isEnd( String snippetId, String line )
236    {
237        return isDemarcator( snippetId, "END", line );
238    }
239}