001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *   http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.apache.maven.doxia.macro.snippet;
020
021import javax.inject.Named;
022import javax.inject.Singleton;
023
024import java.io.File;
025import java.io.IOException;
026import java.net.MalformedURLException;
027import java.net.URL;
028import java.util.HashMap;
029import java.util.Map;
030
031import org.apache.maven.doxia.macro.AbstractMacro;
032import org.apache.maven.doxia.macro.MacroExecutionException;
033import org.apache.maven.doxia.macro.MacroRequest;
034import org.apache.maven.doxia.sink.Sink;
035import org.apache.maven.doxia.sink.impl.SinkEventAttributeSet;
036import org.slf4j.Logger;
037import org.slf4j.LoggerFactory;
038
039/**
040 * A macro that prints out the (source code) content of a file or a URL.
041 */
042@Singleton
043@Named("snippet")
044public class SnippetMacro extends AbstractMacro {
045    private static final Logger LOGGER = LoggerFactory.getLogger(SnippetMacro.class);
046
047    /**
048     * Holds the cache.
049     */
050    private static Map<String, String> cache = new HashMap<>();
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<>();
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    /** {@inheritDoc} */
075    public void execute(Sink sink, MacroRequest request) throws MacroExecutionException {
076        String id = (String) request.getParameter("id");
077
078        String urlParam = (String) request.getParameter("url");
079
080        String fileParam = (String) request.getParameter("file");
081
082        String debugParam = (String) request.getParameter("debug");
083
084        if (debugParam != null) {
085            this.debug = Boolean.parseBoolean(debugParam);
086        }
087
088        String ignoreDownloadErrorParam = (String) request.getParameter("ignoreDownloadError");
089
090        if (ignoreDownloadErrorParam != null) {
091            this.ignoreDownloadError = Boolean.parseBoolean(ignoreDownloadErrorParam);
092        }
093
094        boolean verbatim = true;
095
096        String verbatimParam = (String) request.getParameter("verbatim");
097
098        if (verbatimParam != null && !"".equals(verbatimParam)) {
099            verbatim = Boolean.valueOf(verbatimParam);
100        }
101
102        boolean source = true;
103
104        String sourceParam = (String) request.getParameter("source");
105
106        if (sourceParam != null && !"".equals(sourceParam)) {
107            source = Boolean.valueOf(sourceParam);
108        }
109
110        String encoding = (String) request.getParameter("encoding");
111
112        URL url;
113
114        if (!(urlParam == null || urlParam.isEmpty())) {
115            try {
116                url = new URL(urlParam);
117            } catch (MalformedURLException e) {
118                throw new IllegalArgumentException(urlParam + " is a malformed URL", e);
119            }
120        } else if (!(fileParam == null || fileParam.isEmpty())) {
121            File f = new File(fileParam);
122
123            if (!f.isAbsolute()) {
124                f = new File(request.getBasedir(), fileParam);
125            }
126
127            try {
128                url = f.toURI().toURL();
129            } catch (MalformedURLException e) {
130                throw new IllegalArgumentException(fileParam + " is a malformed URL", e);
131            }
132        } else {
133            throw new IllegalArgumentException("Either the 'url' or the 'file' param has to be provided");
134        }
135
136        StringBuffer snippet;
137
138        try {
139            snippet = getSnippet(url, encoding, id);
140        } catch (IOException e) {
141            throw new MacroExecutionException("Error reading snippet", e);
142        }
143
144        if (verbatim) {
145            sink.verbatim(source ? SinkEventAttributeSet.SOURCE : null);
146
147            sink.text(snippet.toString());
148
149            sink.verbatim_();
150        } else {
151            sink.rawText(snippet.toString());
152        }
153    }
154
155    /**
156     * Return a snippet of the given url.
157     *
158     * @param url The URL to parse.
159     * @param encoding The encoding of the URL to parse.
160     * @param id  The id of the snippet.
161     * @return The snippet.
162     * @throws IOException if something goes wrong.
163     */
164    private StringBuffer getSnippet(URL url, String encoding, String id) throws IOException {
165        StringBuffer result;
166
167        String cachedSnippet = getCachedSnippet(url, id);
168
169        if (cachedSnippet != null) {
170            result = new StringBuffer(cachedSnippet);
171
172            if (debug) {
173                result.append("(Served from cache)");
174            }
175        } else {
176            try {
177                result = new SnippetReader(url, encoding).readSnippet(id);
178                cacheSnippet(url, id, result.toString());
179                if (debug) {
180                    result.append("(Fetched from url, cache content ")
181                            .append(cache)
182                            .append(")");
183                }
184            } catch (IOException e) {
185                if (ignoreDownloadError) {
186                    LOGGER.debug("Exception while reading '{}'", url, e);
187                    result = new StringBuffer("Error during retrieving content skip as ignoreDownloadError activated.");
188                } else {
189                    throw e;
190                }
191            }
192        }
193        return result;
194    }
195
196    /**
197     * Return a snippet from the cache.
198     *
199     * @param url The URL to parse.
200     * @param id  The id of the snippet.
201     * @return The snippet.
202     */
203    private String getCachedSnippet(URL url, String id) {
204        if (isCacheTimedout(url, id)) {
205            removeFromCache(url, id);
206        }
207        return cache.get(globalSnippetId(url, id));
208    }
209
210    /**
211     * Return true if the snippet has been cached longer than
212     * the current timeout.
213     *
214     * @param url The URL to parse.
215     * @param id  The id of the snippet.
216     * @return True if timeout exceeded.
217     */
218    boolean isCacheTimedout(URL url, String id) {
219        return timeInCache(url, id) >= timeout;
220    }
221
222    /**
223     * Return the time the snippet has been cached.
224     *
225     * @param url The URL to parse.
226     * @param id  The id of the snippet.
227     * @return The cache time.
228     */
229    long timeInCache(URL url, String id) {
230        return System.currentTimeMillis() - getTimeCached(url, id);
231    }
232
233    /**
234     * Return the absolute value of when the snippet has been cached.
235     *
236     * @param url The URL to parse.
237     * @param id  The id of the snippet.
238     * @return The cache time.
239     */
240    long getTimeCached(URL url, String id) {
241        String globalId = globalSnippetId(url, id);
242
243        return timeCached.containsKey(globalId) ? timeCached.get(globalId) : 0;
244    }
245
246    /**
247     * Removes the snippet from the cache.
248     *
249     * @param url The URL to parse.
250     * @param id  The id of the snippet.
251     */
252    private void removeFromCache(URL url, String id) {
253        String globalId = globalSnippetId(url, id);
254
255        timeCached.remove(globalId);
256
257        cache.remove(globalId);
258    }
259
260    /**
261     * Return a global identifier for the snippet.
262     *
263     * @param url The URL to parse.
264     * @param id  The id of the snippet.
265     * @return An identifier, concatenated url and id,
266     *         or just url.toString() if id is empty or null.
267     */
268    private String globalSnippetId(URL url, String id) {
269        if (id == null || id.isEmpty()) {
270            return url.toString();
271        }
272
273        return url + " " + id;
274    }
275
276    /**
277     * Puts the given snippet into the cache.
278     *
279     * @param url     The URL to parse.
280     * @param id      The id of the snippet.
281     * @param content The content of the snippet.
282     */
283    public void cacheSnippet(URL url, String id, String content) {
284        cache.put(globalSnippetId(url, id), content);
285
286        timeCached.put(globalSnippetId(url, id), System.currentTimeMillis());
287    }
288
289    /**
290     * Set the cache timeout.
291     *
292     * @param time The timeout to set.
293     */
294    public void setCacheTimeout(int time) {
295        this.timeout = time;
296    }
297}