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    public void execute(Sink sink, MacroRequest request) throws MacroExecutionException {
075        String id = (String) request.getParameter("id");
076
077        String urlParam = (String) request.getParameter("url");
078
079        String fileParam = (String) request.getParameter("file");
080
081        String debugParam = (String) request.getParameter("debug");
082
083        if (debugParam != null) {
084            this.debug = Boolean.parseBoolean(debugParam);
085        }
086
087        String ignoreDownloadErrorParam = (String) request.getParameter("ignoreDownloadError");
088
089        if (ignoreDownloadErrorParam != null) {
090            this.ignoreDownloadError = Boolean.parseBoolean(ignoreDownloadErrorParam);
091        }
092
093        boolean verbatim = true;
094
095        String verbatimParam = (String) request.getParameter("verbatim");
096
097        if (verbatimParam != null && !"".equals(verbatimParam)) {
098            verbatim = Boolean.valueOf(verbatimParam);
099        }
100
101        boolean source = true;
102
103        String sourceParam = (String) request.getParameter("source");
104
105        if (sourceParam != null && !"".equals(sourceParam)) {
106            source = Boolean.valueOf(sourceParam);
107        }
108
109        String encoding = (String) request.getParameter("encoding");
110
111        URL url;
112
113        if (!(urlParam == null || urlParam.isEmpty())) {
114            try {
115                url = new URL(urlParam);
116            } catch (MalformedURLException e) {
117                throw new IllegalArgumentException(urlParam + " is a malformed URL", e);
118            }
119        } else if (!(fileParam == null || fileParam.isEmpty())) {
120            File f = new File(fileParam);
121
122            if (!f.isAbsolute()) {
123                f = new File(request.getBasedir(), fileParam);
124            }
125
126            try {
127                url = f.toURI().toURL();
128            } catch (MalformedURLException e) {
129                throw new IllegalArgumentException(fileParam + " is a malformed URL", e);
130            }
131        } else {
132            throw new IllegalArgumentException("Either the 'url' or the 'file' param has to be provided");
133        }
134
135        StringBuffer snippet;
136
137        try {
138            snippet = getSnippet(url, encoding, id);
139        } catch (IOException e) {
140            throw new MacroExecutionException("Error reading snippet", e);
141        }
142
143        if (verbatim) {
144            sink.verbatim(source ? SinkEventAttributeSet.SOURCE : null);
145
146            sink.text(snippet.toString());
147
148            sink.verbatim_();
149        } else {
150            sink.rawText(snippet.toString());
151        }
152    }
153
154    /**
155     * Return a snippet of the given url.
156     *
157     * @param url The URL to parse.
158     * @param encoding The encoding of the URL to parse.
159     * @param id  The id of the snippet.
160     * @return The snippet.
161     * @throws IOException if something goes wrong.
162     */
163    private StringBuffer getSnippet(URL url, String encoding, String id) throws IOException {
164        StringBuffer result;
165
166        String cachedSnippet = getCachedSnippet(url, id);
167
168        if (cachedSnippet != null) {
169            result = new StringBuffer(cachedSnippet);
170
171            if (debug) {
172                result.append("(Served from cache)");
173            }
174        } else {
175            try {
176                result = new SnippetReader(url, encoding).readSnippet(id);
177                cacheSnippet(url, id, result.toString());
178                if (debug) {
179                    result.append("(Fetched from url, cache content ")
180                            .append(cache)
181                            .append(")");
182                }
183            } catch (IOException e) {
184                if (ignoreDownloadError) {
185                    LOGGER.debug("Exception while reading '{}'", url, e);
186                    result = new StringBuffer("Error during retrieving content skip as ignoreDownloadError activated.");
187                } else {
188                    throw e;
189                }
190            }
191        }
192        return result;
193    }
194
195    /**
196     * Return a snippet from the cache.
197     *
198     * @param url The URL to parse.
199     * @param id  The id of the snippet.
200     * @return The snippet.
201     */
202    private String getCachedSnippet(URL url, String id) {
203        if (isCacheTimedout(url, id)) {
204            removeFromCache(url, id);
205        }
206        return cache.get(globalSnippetId(url, id));
207    }
208
209    /**
210     * Return true if the snippet has been cached longer than
211     * the current timeout.
212     *
213     * @param url The URL to parse.
214     * @param id  The id of the snippet.
215     * @return True if timeout exceeded.
216     */
217    boolean isCacheTimedout(URL url, String id) {
218        return timeInCache(url, id) >= timeout;
219    }
220
221    /**
222     * Return the time the snippet has been cached.
223     *
224     * @param url The URL to parse.
225     * @param id  The id of the snippet.
226     * @return The cache time.
227     */
228    long timeInCache(URL url, String id) {
229        return System.currentTimeMillis() - getTimeCached(url, id);
230    }
231
232    /**
233     * Return the absolute value of when the snippet has been cached.
234     *
235     * @param url The URL to parse.
236     * @param id  The id of the snippet.
237     * @return The cache time.
238     */
239    long getTimeCached(URL url, String id) {
240        String globalId = globalSnippetId(url, id);
241
242        return timeCached.containsKey(globalId) ? timeCached.get(globalId) : 0;
243    }
244
245    /**
246     * Removes the snippet from the cache.
247     *
248     * @param url The URL to parse.
249     * @param id  The id of the snippet.
250     */
251    private void removeFromCache(URL url, String id) {
252        String globalId = globalSnippetId(url, id);
253
254        timeCached.remove(globalId);
255
256        cache.remove(globalId);
257    }
258
259    /**
260     * Return a global identifier for the snippet.
261     *
262     * @param url The URL to parse.
263     * @param id  The id of the snippet.
264     * @return An identifier, concatenated url and id,
265     *         or just url.toString() if id is empty or null.
266     */
267    private String globalSnippetId(URL url, String id) {
268        if (id == null || id.isEmpty()) {
269            return url.toString();
270        }
271
272        return url + " " + id;
273    }
274
275    /**
276     * Puts the given snippet into the cache.
277     *
278     * @param url     The URL to parse.
279     * @param id      The id of the snippet.
280     * @param content The content of the snippet.
281     */
282    public void cacheSnippet(URL url, String id, String content) {
283        cache.put(globalSnippetId(url, id), content);
284
285        timeCached.put(globalSnippetId(url, id), System.currentTimeMillis());
286    }
287
288    /**
289     * Set the cache timeout.
290     *
291     * @param time The timeout to set.
292     */
293    public void setCacheTimeout(int time) {
294        this.timeout = time;
295    }
296}