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 org.apache.maven.doxia.macro.AbstractMacro;
023import org.apache.maven.doxia.macro.Macro;
024import org.apache.maven.doxia.macro.MacroExecutionException;
025import org.apache.maven.doxia.macro.MacroRequest;
026import org.apache.maven.doxia.sink.Sink;
027import org.apache.maven.doxia.sink.impl.SinkEventAttributeSet;
028import org.codehaus.plexus.component.annotations.Component;
029import org.codehaus.plexus.util.StringUtils;
030
031import java.io.File;
032import java.io.IOException;
033import java.net.MalformedURLException;
034import java.net.URL;
035import java.util.HashMap;
036import java.util.Map;
037
038/**
039 * A macro that prints out the content of a file or a URL.
040 *
041 * @version $Id$
042 */
043@Component( role = Macro.class, hint = "snippet" )
044public class SnippetMacro
045    extends AbstractMacro
046{
047    /**
048     * Holds the cache.
049     */
050    private static Map<String, String> cache = new HashMap<String, String>();
051
052    private static final int HOUR = 60;
053
054    /**
055     * One hour default cache.
056     */
057    private long timeout = HOUR * HOUR * 1000;
058
059    /**
060     * Holds the time cache.
061     */
062    private static Map<String, Long> timeCached = new HashMap<String, Long>();
063
064    /**
065     * Debug.
066     */
067    private boolean debug = false;
068
069    /**
070     * in case of Exception during snippet download error will ignored and empty content returned.
071     */
072    private boolean ignoreDownloadError = true;
073
074    /**
075     * {@inheritDoc}
076     */
077    public void execute( Sink sink, MacroRequest request )
078        throws MacroExecutionException
079    {
080        String id = (String) request.getParameter( "id" );
081
082        String urlParam = (String) request.getParameter( "url" );
083
084        String fileParam = (String) request.getParameter( "file" );
085
086        String debugParam = (String) request.getParameter( "debug" );
087
088        if ( debugParam != null )
089        {
090            this.debug = Boolean.parseBoolean( debugParam );
091        }
092
093        String ignoreDownloadErrorParam = (String) request.getParameter( "ignoreDownloadError" );
094
095        if ( ignoreDownloadErrorParam != null )
096        {
097            this.ignoreDownloadError = Boolean.parseBoolean( ignoreDownloadErrorParam );
098        }
099
100        boolean verbatim = true;
101
102        String verbatimParam = (String) request.getParameter( "verbatim" );
103
104        if ( verbatimParam != null && !"".equals( verbatimParam ) )
105        {
106            verbatim = Boolean.valueOf( verbatimParam ).booleanValue();
107        }
108
109        String encoding = (String) request.getParameter( "encoding" );
110
111        URL url;
112
113        if ( !StringUtils.isEmpty( urlParam ) )
114        {
115            try
116            {
117                url = new URL( urlParam );
118            }
119            catch ( MalformedURLException e )
120            {
121                throw new IllegalArgumentException( urlParam + " is a malformed URL" );
122            }
123        }
124        else if ( !StringUtils.isEmpty( fileParam ) )
125        {
126            File f = new File( fileParam );
127
128            if ( !f.isAbsolute() )
129            {
130                f = new File( request.getBasedir(), fileParam );
131            }
132
133            try
134            {
135                url = f.toURI().toURL();
136            }
137            catch ( MalformedURLException e )
138            {
139                throw new IllegalArgumentException( fileParam + " is a malformed URL" );
140            }
141        }
142        else
143        {
144            throw new IllegalArgumentException( "Either the 'url' or the 'file' param has to be given." );
145        }
146
147        StringBuffer snippet;
148
149        try
150        {
151            snippet = getSnippet( url, encoding, id );
152        }
153        catch ( IOException e )
154        {
155            throw new MacroExecutionException( "Error reading snippet", e );
156        }
157
158        if ( verbatim )
159        {
160            sink.verbatim( SinkEventAttributeSet.BOXED );
161
162            sink.text( snippet.toString() );
163
164            sink.verbatim_();
165        }
166        else
167        {
168            sink.rawText( snippet.toString() );
169        }
170    }
171
172    /**
173     * Return a snippet of the given url.
174     *
175     * @param url The URL to parse.
176     * @param encoding The encoding of the URL to parse.
177     * @param id  The id of the snippet.
178     * @return The snippet.
179     * @throws IOException if something goes wrong.
180     */
181    private StringBuffer getSnippet( URL url, String encoding, String id )
182        throws IOException
183    {
184        StringBuffer result;
185
186        String cachedSnippet = getCachedSnippet( url, id );
187
188        if ( cachedSnippet != null )
189        {
190            result = new StringBuffer( cachedSnippet );
191
192            if ( debug )
193            {
194                result.append( "(Served from cache)" );
195            }
196        }
197        else
198        {
199            try
200            {
201                result = new SnippetReader( url, encoding ).readSnippet( id );
202                cacheSnippet( url, id, result.toString() );
203                if ( debug )
204                {
205                    result.append( "(Fetched from url, cache content " ).append( cache ).append( ")" );
206                }
207            }
208            catch ( IOException e )
209            {
210                if ( ignoreDownloadError )
211                {
212                    getLog().debug( "IOException which reading " + url + ": " + e );
213                    result =
214                        new StringBuffer( "Error during retrieving content skip as ignoreDownloadError activated." );
215                }
216                else
217                {
218                    throw e;
219                }
220            }
221        }
222        return result;
223    }
224
225    /**
226     * Return a snippet from the cache.
227     *
228     * @param url The URL to parse.
229     * @param id  The id of the snippet.
230     * @return The snippet.
231     */
232    private String getCachedSnippet( URL url, String id )
233    {
234        if ( isCacheTimedout( url, id ) )
235        {
236            removeFromCache( url, id );
237        }
238        return cache.get( globalSnippetId( url, id ) );
239    }
240
241    /**
242     * Return true if the snippet has been cached longer than
243     * the current timeout.
244     *
245     * @param url The URL to parse.
246     * @param id  The id of the snippet.
247     * @return True if timeout exceeded.
248     */
249    boolean isCacheTimedout( URL url, String id )
250    {
251        return timeInCache( url, id ) >= timeout;
252    }
253
254    /**
255     * Return the time the snippet has been cached.
256     *
257     * @param url The URL to parse.
258     * @param id  The id of the snippet.
259     * @return The cache time.
260     */
261    long timeInCache( URL url, String id )
262    {
263        return System.currentTimeMillis() - getTimeCached( url, id );
264    }
265
266    /**
267     * Return the absolute value of when the snippet has been cached.
268     *
269     * @param url The URL to parse.
270     * @param id  The id of the snippet.
271     * @return The cache time.
272     */
273    long getTimeCached( URL url, String id )
274    {
275        String globalId = globalSnippetId( url, id );
276
277        return timeCached.containsKey( globalId ) ? timeCached.get( globalId ).longValue() : 0;
278    }
279
280    /**
281     * Removes the snippet from the cache.
282     *
283     * @param url The URL to parse.
284     * @param id  The id of the snippet.
285     */
286    private void removeFromCache( URL url, String id )
287    {
288        String globalId = globalSnippetId( url, id );
289
290        timeCached.remove( globalId );
291
292        cache.remove( globalId );
293    }
294
295    /**
296     * Return a global identifier for the snippet.
297     *
298     * @param url The URL to parse.
299     * @param id  The id of the snippet.
300     * @return An identifier, concatenated url and id,
301     *         or just url.toString() if id is empty or null.
302     */
303    private String globalSnippetId( URL url, String id )
304    {
305        if ( StringUtils.isEmpty( id ) )
306        {
307            return url.toString();
308        }
309
310        return url + " " + id;
311    }
312
313    /**
314     * Puts the given snippet into the cache.
315     *
316     * @param url     The URL to parse.
317     * @param id      The id of the snippet.
318     * @param content The content of the snippet.
319     */
320    public void cacheSnippet( URL url, String id, String content )
321    {
322        cache.put( globalSnippetId( url, id ), content );
323
324        timeCached.put( globalSnippetId( url, id ), Long.valueOf( System.currentTimeMillis() ) );
325    }
326
327    /**
328     * Set the cache timeout.
329     *
330     * @param time The timeout to set.
331     */
332    public void setCacheTimeout( int time )
333    {
334        this.timeout = time;
335    }
336}