View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.devacfr.maven.skins.reflow;
20  
21  import javax.annotation.Nonnull;
22  import javax.annotation.Nullable;
23  
24  import java.text.ParseException;
25  import java.text.SimpleDateFormat;
26  import java.util.Date;
27  import java.util.List;
28  
29  import com.google.common.base.Strings;
30  import org.apache.commons.lang3.StringUtils;
31  import org.apache.maven.doxia.site.SiteModel;
32  import org.apache.maven.project.MavenProject;
33  import org.apache.velocity.tools.ToolContext;
34  import org.apache.velocity.tools.config.DefaultKey;
35  import org.apache.velocity.tools.generic.RenderTool;
36  import org.apache.velocity.tools.generic.SafeConfig;
37  import org.apache.velocity.tools.generic.ValueParser;
38  import org.codehaus.plexus.util.PathTool;
39  import org.codehaus.plexus.util.xml.Xpp3Dom;
40  import org.devacfr.maven.skins.reflow.context.Context;
41  import org.slf4j.Logger;
42  import org.slf4j.LoggerFactory;
43  
44  import static java.util.Objects.requireNonNull;
45  
46  /**
47   * An Apache Velocity tool that simplifies retrieval of custom configuration values for a Maven Site.
48   * <p>
49   * The tool is configured to access Maven site configuration of a skin inside {@code <custom>} element of site
50   * descriptor. It supports global properties (defined at skin level) and per-page properties (defined in
51   * {@code <page><mypage>} element). The per-page properties override the global ones.
52   * </p>
53   * <p>
54   * A sample configuration would be like that:
55   * </p>
56   *
57   * <pre>
58   * {@code
59   * <custom>
60   *   <reflowSkin>
61   *     <prop1>value1</prop1>
62   *     <prop2>
63   *       <prop21>value2</prop21>
64   *     </prop2>
65   *     <pages>
66   *       <mypage project="myproject">
67   *         <prop1>override value1</prop1>
68   *       </mypage>
69   *     </pages>
70   *   </reflowSkin>
71   * </custom>
72   * }
73   * </pre>
74   * <p>
75   * To get the value of {@code prop1}, one would simply use {@code $config.prop1}. This would return "override value1".
76   * Then {@code $config.prop2} would return "value2" - the global value.
77   * </p>
78   * <p>
79   * The tool allows querying the value easily, falling back from page to global configuration to {@code null}, if none is
80   * available. It also provides convenience accessors for common values.
81   * </p>
82   * <p>
83   * Note
84   * </p>
85   *
86   * @author Andrius Velykis
87   * @author Christophe Friederich
88   * @since 1.0
89   */
90  @DefaultKey("config")
91  public class SkinConfigTool extends SafeConfig implements ISkinConfig {
92  
93      /** */
94      private static final Logger LOGGER = LoggerFactory.getLogger(SkinConfigTool.class);
95  
96      // ISO 8601 BASIC is used by build timestamp
97      private static final SimpleDateFormat ISO_8601BASIC_DATE = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
98  
99      private static final String PROJECT_BUILD_OUTPUTTIMESTAMP = "project.build.outputTimestamp";
100 
101     /** */
102     public static final String DEFAULT_KEY = "config";
103 
104     /** By default use Reflow skin configuration tag. */
105     public static final String SKIN_KEY = "reflowSkin";
106 
107     /** */
108     private String key = DEFAULT_KEY;
109 
110     /** */
111     private String skinKey = SKIN_KEY;
112 
113     /** Create dummy nodes to avoid null checks. */
114     private Xpp3Dom globalProperties = new Xpp3Dom("");
115 
116     /** */
117     private Xpp3Dom pageProperties = new Xpp3Dom("");
118 
119     /** */
120     private String namespace = "";
121 
122     /** */
123     private String projectId = null;
124 
125     /** */
126     private String fileId = null;
127 
128     /** */
129     private Context<?> context = null;
130 
131     /** */
132     private MavenProject project = null;
133 
134     /** */
135     private SiteModel siteModel;
136 
137     /** */
138     private ToolContext velocityContext;
139 
140     /**
141      * {@inheritDoc}
142      *
143      * @see SafeConfig#configure(ValueParser)
144      */
145     @Override
146     protected void configure(final ValueParser values) {
147         final String altkey = values.getString("key");
148         if (altkey != null) {
149             setKey(altkey);
150         }
151 
152         // allow changing skin key in the configuration
153         final String altSkinKey = values.getString("skinKey");
154         if (altSkinKey != null) {
155             this.skinKey = altSkinKey;
156         }
157 
158         // retrieve the site model from Velocity context
159         final Object vc = values.get("velocityContext");
160 
161         if (!(vc instanceof ToolContext)) {
162             return;
163         }
164 
165         this.velocityContext = (ToolContext) vc;
166 
167         final Object projectObj = velocityContext.get("project");
168         if (projectObj instanceof MavenProject) {
169             this.project = (MavenProject) projectObj;
170             final String artifactId = project.getArtifactId();
171             // use artifactId "sluggified" as the projectId
172             projectId = HtmlTool.slug(artifactId);
173         }
174 
175         // calculate the page ID from the current file name
176         final String currentFileObj = getCurrentFileName();
177         fileId = slugFilename(currentFileObj);
178 
179         final Object siteModelObj = velocityContext.get("site");
180 
181         if (!(siteModelObj instanceof SiteModel)) {
182             return;
183         }
184 
185         this.siteModel = (SiteModel) siteModelObj;
186         final Object customObj = siteModel.getCustom();
187 
188         if (!(customObj instanceof Xpp3Dom)) {
189             return;
190         }
191 
192         // Now that we have the custom node, get the global properties
193         // under the skin tag
194         final Xpp3Dom customNode = (Xpp3Dom) customObj;
195         Xpp3Dom skinNode = customNode.getChild(skinKey);
196         final String namespaceKey = ":" + skinKey;
197 
198         if (skinNode == null) {
199             // try searching with any namespace
200             for (final Xpp3Dom child : customNode.getChildren()) {
201                 if (child.getName().endsWith(namespaceKey)) {
202                     skinNode = child;
203                     break;
204                 }
205             }
206         }
207 
208         if (skinNode != null) {
209             globalProperties = skinNode;
210 
211             if (skinNode.getName().endsWith(namespaceKey)) {
212                 // extract the namespace (including the colon)
213                 namespace = Strings.emptyToNull(
214                     skinNode.getName().substring(0, skinNode.getName().length() - namespaceKey.length() + 1));
215             }
216 
217             // for page properties, retrieve the file name and drop the `.html`
218             // extension - this will be used, i.e. `index` instead of `index.html`
219             final Xpp3Dom pagesNode = Xpp3Utils.getFirstChild(skinNode, "pages", namespace);
220             if (pagesNode != null) {
221 
222                 // Get the page for the file
223                 Xpp3Dom page = Xpp3Utils.getFirstChild(pagesNode, fileId, namespace);
224 
225                 // Now check if the project artifact ID is set, and if so, if it matches the
226                 // current project. This allows preventing accidental reuse of parent page
227                 // configs in children modules
228                 if (page != null && projectId != null) {
229                     final String pageProject = page.getAttribute("project");
230                     if (pageProject != null && !projectId.equals(pageProject)) {
231                         // project ID indicated, and is different - do not use the config
232                         page = null;
233                     }
234                 }
235 
236                 if (page != null) {
237                     pageProperties = page;
238                 }
239             }
240 
241             // Config option <localResources>true</localResources> to force CDN-less
242             // Bootstrap & jQuery
243             this.velocityContext.put("localResources", is("localResources"));
244             // Use config option
245             // <absoluteResourceURL>http://mysite.com/</absoluteResourceURL>
246             this.velocityContext.put("resourcePath", getResourcePath());
247 
248             this.context = Context.buildContext(this);
249         }
250         if (LOGGER.isDebugEnabled()) {
251             LOGGER.debug("Current Filename: {}", currentFileObj);
252             LOGGER.debug("Project id: {}", projectId);
253             LOGGER.debug("File id: {}", fileId);
254             LOGGER.debug("Context: {}", this.context);
255             LOGGER.debug("Namespace: {}", this.namespace);
256             LOGGER.debug("---------------------------------------------------");
257         }
258     }
259 
260     /**
261      * Sets the key under which this tool has been configured.
262      *
263      * @param key
264      *            the key of config
265      * @since 1.0
266      */
267     protected void setKey(final String key) {
268         this.key = requireNonNull(key, "SkinConfigTool key cannot be null");
269     }
270 
271     /**
272      * @return Returns the key under which this tool has been configured. The default is `config`.
273      * @since 1.0
274      */
275     public String getKey() {
276         return this.key;
277     }
278 
279     /**
280      * {@inheritDoc}
281      */
282     @Override
283     @Nullable public <T> T getContextValue(@Nonnull final String key, @Nonnull final Class<T> type) {
284         requireNonNull(type);
285         if (String.class.isAssignableFrom(type)) {
286             return this.eval("$" + key, type);
287         } else {
288             throw new UnsupportedOperationException();
289         }
290     }
291 
292     /**
293      * {@inheritDoc}
294      */
295     @Override
296     public void setContextValue(@Nonnull final String key, @Nullable final Object value) {
297         requireNonNull(key);
298         if (value instanceof String) {
299             this.eval("#set( $" + key + "= \"" + value.toString() + "\")", Void.class);
300         } else {
301             throw new UnsupportedOperationException();
302         }
303     }
304 
305     /**
306      * {@inheritDoc}
307      */
308     @Override
309     @Nullable @SuppressWarnings("unchecked")
310     public <T> T getToolbox(@Nonnull final String toolName, @Nonnull final Class<T> toolType) {
311         requireNonNull(toolType);
312         return (T) this.velocityContext.getToolbox().get(requireNonNull(toolName));
313     }
314 
315     /**
316      * {@inheritDoc}
317      */
318     @Override
319     @Nullable public Xpp3Dom get(@Nonnull final String property) {
320         requireNonNull(property);
321         // first try page properties
322         Xpp3Dom propNode = Xpp3Utils.getFirstChild(pageProperties, property, namespace);
323         if (propNode == null) {
324             // try global
325             propNode = Xpp3Utils.getFirstChild(globalProperties, property, namespace);
326         }
327 
328         return propNode;
329     }
330 
331     /**
332      * Retrieves the text value of the given {@code property}, e.g. as in {@code <myprop>value</myprop>}.
333      *
334      * @param property
335      *            the property of interest
336      * @return the configuration value if found in page or globally, {@code null} otherwise.
337      * @see #get(String)
338      * @since 1.0
339      */
340     @Nullable public String value(@Nonnull final String property) {
341         requireNonNull(property);
342         final Xpp3Dom propNode = get(property);
343 
344         if (propNode == null) {
345             // not found
346             return null;
347         }
348 
349         return propNode.getValue();
350     }
351 
352     /**
353      * Gets the text value of the given {@code property}.
354      *
355      * @param property
356      *            the property to use
357      * @param targetType
358      *            the returned target type use to convert value.
359      * @param defaultValue
360      *            the default value used if property doesn't exist.
361      * @return Returns a converted value of the given {@code property}.
362      * @since 2.0
363      * @param <T>
364      *            the type of returned object.
365      */
366     @Override
367     @SuppressWarnings("unchecked")
368     @Nullable public <T> T getPropertyValue(@Nonnull final String property,
369         @Nonnull final Class<T> targetType,
370         @Nullable final T defaultValue) {
371         requireNonNull(property, "property is required");
372         requireNonNull(targetType, "targetType is required");
373         final String value = value(property);
374         if (value == null) {
375             return defaultValue;
376         }
377         Object returnedValue = value;
378         if (targetType.isAssignableFrom(Boolean.class)) {
379             returnedValue = Boolean.valueOf(value);
380         } else if (targetType.isAssignableFrom(Integer.class)) {
381             returnedValue = Integer.valueOf(value);
382         } else if (targetType.isAssignableFrom(Long.class)) {
383             returnedValue = Long.valueOf(value);
384         }
385         return (T) returnedValue;
386     }
387 
388     /**
389      * Gets the list of all children name for the {@code parentNode}.
390      *
391      * @param parentNode
392      *            the parent node to use (can be {@code null}.
393      * @return Returns a list of {@link String} representing the name of all children, which may be empty but never
394      *         {@code null}.
395      * @since 1.3
396      */
397     public List<String> getChildren(final Xpp3Dom parentNode) {
398         return Xpp3Utils.getChildren(parentNode);
399     }
400 
401     /**
402      * Gets the attribute value of the given {@code attribute} of {@code property}.
403      *
404      * @param property
405      *            the property to use
406      * @param attribute
407      *            the attribute to use.
408      * @param targetType
409      *            the returned target type use to convert value.
410      * @param defaultValue
411      *            the default value used if property doesn't exist.
412      * @return Returns a converted value of the given {@code property}.
413      * @since 2.0
414      * @param <T>
415      *            the type of returned object.
416      */
417     @Override
418     @SuppressWarnings("unchecked")
419     @Nullable public <T> T getAttributeValue(@Nonnull final String property,
420         @Nonnull final String attribute,
421         @Nonnull final Class<T> targetType,
422         @Nullable final T defaultValue) {
423         requireNonNull(property, "property is required");
424         requireNonNull(attribute, "attribute is required");
425         requireNonNull(targetType, "targetType is required");
426 
427         Xpp3Dom element = get(property);
428         if (element == null) {
429             return defaultValue;
430         }
431         String value = element.getAttribute(attribute);
432         if (value == null) {
433             return defaultValue;
434         }
435 
436         if ("inherit".equals(value) || Strings.isNullOrEmpty(value)) {
437             element = Xpp3Utils.getFirstChild(globalProperties, property, namespace);
438             if (LOGGER.isDebugEnabled()) {
439                 LOGGER.debug("Inherit value property '{}': {}", property, element);
440             }
441         }
442         if (element == null) {
443             return defaultValue;
444         }
445         value = element.getAttribute(attribute);
446         if (value == null) {
447             return defaultValue;
448         }
449 
450         Object returnedValue = value;
451         if (targetType.isAssignableFrom(Boolean.class)) {
452             returnedValue = Boolean.valueOf(value);
453         } else if (targetType.isAssignableFrom(Integer.class)) {
454             returnedValue = Integer.valueOf(value);
455         } else if (targetType.isAssignableFrom(Long.class)) {
456             returnedValue = Long.valueOf(value);
457         }
458         return (T) returnedValue;
459     }
460 
461     /**
462      * {@inheritDoc}
463      */
464     @Override
465     @SuppressWarnings("unchecked")
466     @Nullable public <T> T getAttributeValue(@Nonnull final Xpp3Dom element,
467         @Nonnull final String attribute,
468         @Nonnull final Class<T> targetType,
469         @Nullable final T defaultValue) {
470         if (element == null) {
471             return defaultValue;
472         }
473         final String value = element.getAttribute(attribute);
474         if (value == null) {
475             return defaultValue;
476         }
477         Object returnedValue = value;
478         if (targetType.isAssignableFrom(Boolean.class)) {
479             returnedValue = Boolean.valueOf(value);
480         } else if (targetType.isAssignableFrom(Integer.class)) {
481             returnedValue = Integer.valueOf(value);
482         } else if (targetType.isAssignableFrom(Long.class)) {
483             returnedValue = Long.valueOf(value);
484         }
485         return (T) returnedValue;
486     }
487 
488     /**
489      * A convenience method to check if the value of the {@code property} is {@code "true"}.
490      *
491      * @param property
492      *            the property of interest
493      * @return {@code true} if the configuration value is set either in page or globally, and is equal to
494      *         {@code "true"}.
495      * @see #get(String)
496      * @since 1.0
497      */
498     public boolean is(final String property) {
499         return "true".equals(value(property));
500     }
501 
502     /**
503      * {@inheritDoc}
504      */
505     @Override
506     public boolean not(final String property) {
507         return "false".equals(value(property));
508     }
509 
510     /**
511      * A convenience method to check if the {@code property} is set to a specific value.
512      *
513      * @param property
514      *            the property of interest
515      * @param value
516      *            the property value to check
517      * @return {@code true} if the configuration value is set either in page or globally, and is equal to {@code value}.
518      * @see #get(String)
519      * @since 1.0
520      */
521     public boolean isValue(final String property, final String value) {
522         return value != null && value.equals(value(property));
523     }
524 
525     /**
526      * {@inheritDoc}
527      */
528     @Override
529     @Nullable public String getProjectId() {
530         return projectId;
531     }
532 
533     /**
534      * {@inheritDoc}
535      */
536     @Override
537     @Nullable public String getFileId() {
538         return fileId;
539     }
540 
541     /**
542      * @return the context
543      */
544     @Override
545     @Nonnull
546     public Context<?> getContext() {
547         return context;
548     }
549 
550     /**
551      * @return the velocity Context
552      */
553     public ToolContext getVelocityContext() {
554         return velocityContext;
555     }
556 
557     /**
558      * {@inheritDoc}
559      */
560     @Override
561     @Nonnull
562     public MavenProject getProject() {
563         return project;
564     }
565 
566     /**
567      * {@inheritDoc}
568      */
569     @Override
570     @Nonnull
571     public SiteModel getSiteModel() {
572         return siteModel;
573     }
574 
575     /**
576      * {@inheritDoc}
577      */
578     @Override
579     @Nonnull
580     public Xpp3Dom getPageProperties() {
581         return pageProperties;
582     }
583 
584     /**
585      * {@inheritDoc}
586      */
587     @Override
588     @Nonnull
589     public Xpp3Dom getGlobalProperties() {
590         return globalProperties;
591     }
592 
593     /**
594      * {@inheritDoc}
595      */
596     @Override
597     @Nonnull
598     public String getNamespace() {
599         return namespace;
600     }
601 
602     /**
603      * @return Returns the location of project.
604      */
605     @Nonnull
606     public String getProjectLocation() {
607         String projectSiteLoc = getProject().getUrl();
608         if (!Strings.isNullOrEmpty(projectSiteLoc)) {
609 
610             if (!projectSiteLoc.endsWith("/")) {
611                 projectSiteLoc += "/";
612             }
613         }
614         return projectSiteLoc;
615     }
616 
617     /**
618      * <p>
619      * See <a href= "https://maven.apache.org/doxia/doxia-sitetools/doxia-site-renderer/">Doxia Sitetools - Site
620      * Renderer</a> for more information.
621      *
622      * @return Returns a {@link String} representing the name of current file of the (HTML) document being rendered,
623      *         relative to the site root.
624      */
625     @Nonnull
626     public String getCurrentFileName() {
627         return (String) velocityContext.get("currentFileName");
628     }
629 
630     /**
631      * @return Returns a {@link String} representing the location path of current rendered file.
632      */
633     @Nonnull
634     public String getCurrentFileLocation() {
635         final String projectSiteLoc = getProjectLocation();
636         return URITool.toURI(projectSiteLoc).resolve(getCurrentFileName()).toString();
637     }
638 
639     /**
640      * {@inheritDoc}
641      */
642     @Override
643     @SuppressWarnings("unchecked")
644     @Nullable public <T> T eval(@Nullable final String vtl, @Nonnull final Class<T> requiredClass) {
645         if (vtl == null) {
646             return null;
647         }
648         final RenderTool renderTool = (RenderTool) getVelocityContext().get("render");
649         try {
650             return (T) renderTool.eval(getVelocityContext(), vtl);
651         } catch (final Exception ex) {
652             throw new RuntimeException("error when try evaluate '" + vtl + "'", ex);
653         }
654     }
655 
656     /**
657      * {@inheritDoc}
658      */
659     @Override
660     public String relativeLink(final String href) {
661         if (href == null) {
662             return null;
663         }
664         if (isExternalLink(href)) {
665             return href;
666         }
667         final String relativePath = (String) velocityContext.get("relativePath");
668         String relativeLink = PathTool.calculateLink(href, relativePath);
669         relativeLink = relativeLink.replaceAll("\\\\", "/");
670         if (Strings.isNullOrEmpty(relativeLink)) {
671             relativeLink = "./";
672         }
673         // Attempt to normalise the relative link - this is useful for active link
674         // calculations and better relative links for subdirectories.
675         //
676         // The issue is particularly visible with pages in subdirectories,
677         // so that if you are in <root>/dev/index.html, the relative menu link to
678         // the _same_ page would likely be ../dev/index.html instead of '' or
679         // 'index.html'.
680         final String currentFileLoc = getCurrentFileLocation();
681         final String absoluteLink = URITool.toURI(currentFileLoc).resolve(relativeLink).normalize().toString();
682         if (currentFileLoc.equals(absoluteLink)) {
683             // for matching link, use empty relative link
684             relativeLink = StringUtils.EMPTY;
685         } else {
686             // relativize the absolute link based on current directory
687             // (uses Maven project link relativization)
688             final String currentFileDir = PathTool.getDirectoryComponent(currentFileLoc);
689             relativeLink = URITool.relativizeLink(currentFileDir, absoluteLink);
690         }
691         if (LOGGER.isDebugEnabled()) {
692             LOGGER.debug("-- Relative Link ----------------------------------");
693             LOGGER.debug("link: {}", href);
694             LOGGER.debug("currentFileLoc: {}", currentFileLoc);
695             LOGGER.debug("absoluteLink: {}", absoluteLink);
696             LOGGER.debug("relativeLink: {}", relativeLink);
697             LOGGER.debug("---------------------------------------------------");
698         }
699         return relativeLink;
700     }
701 
702     /**
703      * @param url
704      *            a url.
705      * @return Returns {@code true} whether the link is a external link to the site.
706      */
707     public boolean isExternalLink(final String url) {
708         if (url == null) {
709             return false;
710         }
711         final String absoluteResourceURL = this.value("absoluteResourceURL");
712         if (!Strings.isNullOrEmpty(absoluteResourceURL) && url.startsWith(absoluteResourceURL)) {
713             return false;
714         }
715         return url.toLowerCase().startsWith("http:/") || url.toLowerCase().startsWith("https:/")
716                 || url.toLowerCase().startsWith("ftp:/") || url.toLowerCase().startsWith("mailto:")
717                 || url.toLowerCase().startsWith("file:/") || url.toLowerCase().indexOf("://") != -1;
718     }
719 
720     /**
721      * {@inheritDoc}
722      */
723     @Override
724     public boolean isActiveLink(@Nullable final String href) {
725         final String alignedFileName = (String) velocityContext.get("alignedFileName");
726         if (href == null) {
727             return false;
728         }
729         // either empty link (pointing to a page), or if the current file is index.html,
730         // the link may point to the whole directory
731         return Strings.isNullOrEmpty(href) || alignedFileName.endsWith("index.html") && ".".equals(href);
732     }
733 
734     /**
735      * Converts a filename to pageId format.
736      *
737      * @param fileName
738      *            the filename to convert
739      * @return Returns a {@link String} representing the pageId of {@code filename}.
740      */
741     @Nullable public static String slugFilename(@Nullable final String fileName) {
742         if (fileName == null) {
743             return null;
744         }
745         String currentFile = fileName;
746 
747         // drop the extension
748         final int lastDot = currentFile.lastIndexOf(".");
749         if (lastDot >= 0) {
750             currentFile = currentFile.substring(0, lastDot);
751         }
752 
753         // get the short ID (in case of nested files)
754         // String fileName = new File(currentFile).getName();
755         // fileShortId = HtmlTool.slug(fileName);
756 
757         // full file ID includes the nested dirs
758         // replace nesting "/" with "-"
759         return HtmlTool.slug(currentFile.replace("/", "-").replace("\\", "-"));
760     }
761 
762     /**
763      * @return Returns a {@link String} representing the relative path to root site.
764      */
765     @Nonnull
766     public String getResourcePath() {
767         final String absoluteResourceURL = this.value("absoluteResourceURL");
768         String projectUrl = getProjectLocation();
769         final String currentFileName = getCurrentFileName();
770         if (!Strings.isNullOrEmpty(projectUrl) && currentFileName != null) {
771             if (projectUrl.charAt(projectUrl.length() - 1) != '/') {
772                 projectUrl += '/';
773             }
774             final String currentFileDir = URITool.toURI(projectUrl).resolve(currentFileName).resolve(".").toString();
775             return URITool.relativizeLink(currentFileDir, absoluteResourceURL);
776         }
777         return (String) velocityContext.get("relativePath");
778     }
779 
780     /**
781      * Gets the reproduce build timestamp whether property 'project.build.outputTimestamp' is fill in.
782      *
783      * @return Returns a instance of {@code Date} representing the reproduce build timestamp whether property
784      *         'project.build.outputTimestamp' is fill in.
785      */
786     @Nullable public Date getBuildOutputTimestamp() throws ParseException {
787         if (!this.velocityContext.containsKey(PROJECT_BUILD_OUTPUTTIMESTAMP)) {
788             return null;
789         }
790         Object outputTimestamp = this.velocityContext.get(PROJECT_BUILD_OUTPUTTIMESTAMP);
791         if (outputTimestamp != null) {
792             return ISO_8601BASIC_DATE.parse(outputTimestamp.toString());
793         }
794         return null;
795     }
796 }