1. Project Clover database mar. janv. 20 2026 12:32:22 CET
  2. Package org.devacfr.maven.skins.reflow

File SkinConfigTool.java

 

Coverage histogram

../../../../../img/srcFileCovDistChart6.png
71% of files have more coverage

Code metrics

150
308
49
1
1 059
632
140
0,45
6,29
49
2,86

Classes

Class Line # Actions
SkinConfigTool 95 308 0% 140 216
0.573964557,4%
 

Contributing tests

This file is covered by 13 tests. .

Source view

1    /*
2    * Copyright 2012-2025 Christophe Friederich
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.apache.org/licenses/LICENSE-2.0
9    *
10    * Unless required by applicable law or agreed to in writing, software
11    * distributed under the License is distributed on an "AS IS" BASIS,
12    * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13    * See the License for the specific language governing permissions and
14    * limitations under the License.
15    */
16    package org.devacfr.maven.skins.reflow;
17   
18    import static java.util.Objects.requireNonNull;
19   
20    import com.google.common.base.Strings;
21    import java.text.ParseException;
22    import java.text.SimpleDateFormat;
23    import java.util.Date;
24    import java.util.List;
25    import java.util.Optional;
26    import javax.annotation.Nonnull;
27    import javax.annotation.Nullable;
28    import org.apache.commons.lang3.StringUtils;
29    import org.apache.maven.doxia.site.PublishDate;
30    import org.apache.maven.doxia.site.SiteModel;
31    import org.apache.maven.project.MavenProject;
32    import org.apache.velocity.tools.ToolContext;
33    import org.apache.velocity.tools.config.DefaultKey;
34    import org.apache.velocity.tools.generic.DateTool;
35    import org.apache.velocity.tools.generic.RenderTool;
36    import org.apache.velocity.tools.generic.ResourceTool;
37    import org.apache.velocity.tools.generic.SafeConfig;
38    import org.apache.velocity.tools.generic.ValueParser;
39    import org.codehaus.plexus.util.PathTool;
40    import org.codehaus.plexus.util.xml.Xpp3Dom;
41    import org.devacfr.maven.skins.reflow.context.Context;
42    import org.devacfr.maven.skins.reflow.context.PositionType;
43    import org.devacfr.maven.skins.reflow.snippet.SnippetComponent;
44    import org.devacfr.maven.skins.reflow.snippet.SnippetContext;
45    import org.devacfr.maven.skins.reflow.snippet.SnippetParser;
46    import org.jsoup.nodes.Document;
47    import org.slf4j.Logger;
48    import org.slf4j.LoggerFactory;
49   
50    /**
51    * An Apache Velocity tool that simplifies retrieval of custom configuration values for a Maven Site.
52    * <p>
53    * The tool is configured to access Maven site configuration of a skin inside {@code <custom>} element of site
54    * descriptor. It supports global properties (defined at skin level) and per-page properties (defined in
55    * {@code <page><mypage>} element). The per-page properties override the global ones.
56    * </p>
57    * <p>
58    * A sample configuration would be like that:
59    * </p>
60    *
61    * <pre>
62    * {@code
63    * <custom>
64    * <reflowSkin>
65    * <prop1>value1</prop1>
66    * <prop2>
67    * <prop21>value2</prop21>
68    * </prop2>
69    * <pages>
70    * <mypage project="myproject">
71    * <prop1>override value1</prop1>
72    * </mypage>
73    * </pages>
74    * </reflowSkin>
75    * </custom>
76    * }
77    * </pre>
78    * <p>
79    * To get the value of {@code prop1}, one would simply use {@code $config.prop1}. This would return "override value1".
80    * Then {@code $config.prop2} would return "value2" - the global value.
81    * </p>
82    * <p>
83    * The tool allows querying the value easily, falling back from page to global configuration to {@code null}, if none is
84    * available. It also provides convenience accessors for common values.
85    * </p>
86    * <p>
87    * Note
88    * </p>
89    *
90    * @author Andrius Velykis
91    * @author Christophe Friederich
92    * @since 1.0
93    */
94    @DefaultKey("config")
 
95    public class SkinConfigTool extends SafeConfig implements ISkinConfig {
96   
97    /** */
98    private static final Logger LOGGER = LoggerFactory.getLogger(SkinConfigTool.class);
99   
100    // ISO 8601 BASIC is used by build timestamp
101    private static final SimpleDateFormat ISO_8601BASIC_DATE = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
102   
103    private static final String PROJECT_BUILD_OUTPUTTIMESTAMP = "project.build.outputTimestamp";
104   
105    /** */
106    public static final String DEFAULT_KEY = "config";
107   
108    /** By default use Reflow skin configuration tag. */
109    public static final String SKIN_KEY = "reflowSkin";
110   
111    /** */
112    private String key = DEFAULT_KEY;
113   
114    /** */
115    private String skinKey = SKIN_KEY;
116   
117    /** Create dummy nodes to avoid null checks. */
118    private Xpp3Dom globalProperties = new Xpp3Dom("");
119   
120    /** */
121    private Xpp3Dom pageProperties = new Xpp3Dom("");
122   
123    /** */
124    private String namespace = "";
125   
126    /** */
127    private String projectId = null;
128   
129    /** */
130    private String fileId = null;
131   
132    /** */
133    private Context<?> context = null;
134   
135    /** */
136    private MavenProject project = null;
137   
138    /** */
139    private SiteModel siteModel;
140   
141    /** */
142    private ToolContext velocityContext;
143   
144    /**
145    * {@inheritDoc}
146    *
147    * @see SafeConfig#configure(ValueParser)
148    */
 
149  11 toggle @Override
150    protected void configure(final ValueParser values) {
151  11 final String altkey = values.getString("key");
152  11 if (altkey != null) {
153  0 setKey(altkey);
154    }
155   
156    // allow changing skin key in the configuration
157  11 final String altSkinKey = values.getString("skinKey");
158  11 if (altSkinKey != null) {
159  0 this.skinKey = altSkinKey;
160    }
161   
162    // retrieve the site model from Velocity context
163  11 final Object vc = values.get("velocityContext");
164   
165  11 if (!(vc instanceof ToolContext)) {
166  0 return;
167    }
168   
169  11 this.velocityContext = (ToolContext) vc;
170   
171  11 final Object projectObj = velocityContext.get("project");
172  11 if (projectObj instanceof MavenProject) {
173  11 this.project = (MavenProject) projectObj;
174  11 final String artifactId = project.getArtifactId();
175    // use artifactId "sluggified" as the projectId
176  11 projectId = HtmlTool.slug(artifactId);
177    }
178   
179    // calculate the page ID from the current file name
180  11 final String currentFileObj = getCurrentFileName();
181  11 fileId = slugFilename(currentFileObj);
182   
183  11 final Object siteModelObj = velocityContext.get("site");
184   
185  11 if (!(siteModelObj instanceof SiteModel)) {
186  0 return;
187    }
188   
189  11 this.siteModel = (SiteModel) siteModelObj;
190  11 final Object customObj = siteModel.getCustom();
191   
192  11 if (!(customObj instanceof Xpp3Dom)) {
193  0 return;
194    }
195   
196    // Now that we have the custom node, get the global properties
197    // under the skin tag
198  11 final Xpp3Dom customNode = (Xpp3Dom) customObj;
199  11 Xpp3Dom skinNode = customNode.getChild(skinKey);
200  11 final String namespaceKey = ":" + skinKey;
201   
202  11 if (skinNode == null) {
203    // try searching with any namespace
204  0 for (final Xpp3Dom child : customNode.getChildren()) {
205  0 if (child.getName().endsWith(namespaceKey)) {
206  0 skinNode = child;
207  0 break;
208    }
209    }
210    }
211   
212  11 if (skinNode != null) {
213  11 globalProperties = skinNode;
214   
215  11 if (skinNode.getName().endsWith(namespaceKey)) {
216    // extract the namespace (including the colon)
217  0 namespace = Strings
218    .emptyToNull(skinNode.getName().substring(0, skinNode.getName().length() - namespaceKey.length() + 1));
219    }
220   
221    // for page properties, retrieve the file name and drop the `.html`
222    // extension - this will be used, i.e. `index` instead of `index.html`
223  11 final Xpp3Dom pagesNode = Xpp3Utils.getFirstChild(skinNode, "pages", namespace);
224  11 if (pagesNode != null) {
225   
226    // Get the page for the file
227  11 Xpp3Dom page = Xpp3Utils.getFirstChild(pagesNode, fileId, namespace);
228   
229    // Now check if the project artifact ID is set, and if so, if it matches the
230    // current project. This allows preventing accidental reuse of parent page
231    // configs in children modules
232  11 if (page != null && projectId != null) {
233  11 final String pageProject = page.getAttribute("project");
234  11 if (pageProject != null && !projectId.equals(pageProject)) {
235    // project ID indicated, and is different - do not use the config
236  0 page = null;
237    }
238    }
239   
240  11 if (page != null) {
241  11 pageProperties = page;
242    }
243    }
244   
245    // Config option <localResources>true</localResources> to force CDN-less
246    // Bootstrap & jQuery
247  11 this.velocityContext.put("localResources", is("localResources"));
248    // Use config option
249    // <absoluteResourceURL>http://mysite.com/</absoluteResourceURL>
250  11 this.velocityContext.put("resourcePath", getResourcePath());
251   
252  11 this.context = Context.buildContext(this);
253    }
254  11 if (LOGGER.isDebugEnabled()) {
255  11 LOGGER.debug("Current Filename: {}", currentFileObj);
256  11 LOGGER.debug("title: {}", velocityContext.get("title"));
257  11 LOGGER.debug("shortTitle: {}", velocityContext.get("shortTitle"));
258  11 LOGGER.debug("Project id: {}", projectId);
259  11 LOGGER.debug("File id: {}", fileId);
260  11 LOGGER.debug("Context: {}", this.context);
261  11 LOGGER.debug("Namespace: {}", this.namespace);
262  11 LOGGER.debug("---------------------------------------------------");
263    }
264    }
265   
266    /**
267    * {@inheritDoc}
268    */
 
269  0 toggle @Nonnull
270    @Override
271    public String renderSnippets(final String bodyContent) throws Exception {
272  0 final SnippetContext snippetContext = new SnippetParser().parse(this, bodyContent);
273  0 return snippetContext.html();
274    }
275   
276    /**
277    * {@inheritDoc}
278    */
 
279  1 toggle @Nonnull
280    @Override
281    public String renderSnippet(final String snippet) throws Exception {
282  1 final SnippetParser parser = new SnippetParser();
283  1 final Document doc = getHtmlTool().parse(snippet);
284  1 final SnippetContext context = parser.getSnippetContext();
285  1 final SnippetComponent<?> component = context.create(doc.body().firstElementChild(), null);
286  1 context.setConfig(this);
287  1 component.render(context);
288  1 return doc.body().html();
289    }
290   
291    /**
292    * Sets the key under which this tool has been configured.
293    *
294    * @param key
295    * the key of config
296    * @since 1.0
297    */
 
298  0 toggle protected void setKey(final String key) {
299  0 this.key = requireNonNull(key, "SkinConfigTool key cannot be null");
300    }
301   
302    /**
303    * @return Returns the key under which this tool has been configured. The default is `config`.
304    * @since 1.0
305    */
 
306  0 toggle public String getKey() {
307  0 return this.key;
308    }
309   
310    /**
311    * {@inheritDoc}
312    */
 
313  0 toggle @Override
314    @Nullable public <T> T getContextValue(@Nonnull final String key, @Nonnull final Class<T> type) {
315  0 requireNonNull(type);
316  0 if (String.class.isAssignableFrom(type)) {
317  0 return this.eval("$" + key, type);
318    } else {
319  0 throw new UnsupportedOperationException();
320    }
321    }
322   
323    /**
324    * {@inheritDoc}
325    */
 
326  0 toggle @Override
327    public void setContextValue(@Nonnull final String key, @Nullable final Object value) {
328  0 requireNonNull(key);
329  0 if (value instanceof String) {
330  0 this.eval("#set( $" + key + "= \"" + value.toString() + "\")", Void.class);
331    } else {
332  0 throw new UnsupportedOperationException();
333    }
334    }
335   
336    /**
337    * {@inheritDoc}
338    */
 
339  1 toggle @Override
340    @Nullable @SuppressWarnings("unchecked")
341    public <T> T getToolbox(@Nonnull final String toolName, @Nonnull final Class<T> toolType) {
342  1 requireNonNull(toolType);
343  1 return (T) this.velocityContext.getToolbox().get(requireNonNull(toolName));
344    }
345   
346    /**
347    * Gets the tool {@link HtmlTool}.
348    *
349    * @return Returns the tool {@link HtmlTool}.
350    * @since 2.1
351    */
 
352  1 toggle @Override
353    @Nonnull
354    public HtmlTool getHtmlTool() {
355  1 return requireNonNull(getToolbox("htmlTool", HtmlTool.class), "htmlTool is required");
356    }
357   
358    /**
359    * {@inheritDoc}
360    */
 
361  499 toggle @Override
362    @Nullable public Xpp3Dom get(@Nonnull final String property) {
363  499 requireNonNull(property);
364    // first try page properties
365  499 Xpp3Dom propNode = Xpp3Utils.getFirstChild(pageProperties, property, namespace);
366  499 if (propNode == null) {
367    // try global
368  488 propNode = Xpp3Utils.getFirstChild(globalProperties, property, namespace);
369    }
370   
371  499 return propNode;
372    }
373   
374    /**
375    * Retrieves the text value of the given {@code property}, e.g. as in {@code <myprop>value</myprop>}.
376    *
377    * @param property
378    * the property of interest
379    * @return the configuration value if found in page or globally, {@code null} otherwise.
380    * @see #get(String)
381    * @since 1.0
382    */
 
383  300 toggle @Nullable public String value(@Nonnull final String property) {
384  300 requireNonNull(property);
385  300 final Xpp3Dom propNode = get(property);
386   
387  300 if (propNode == null) {
388    // not found
389  20 return null;
390    }
391   
392  280 return propNode.getValue();
393    }
394   
395    /**
396    * {@inheritDoc}
397    */
 
398  22 toggle @Override
399    @Nullable public <T> T getPropertyValue(@Nonnull final String property,
400    @Nonnull final Class<T> targetType,
401    @Nullable final T defaultValue) {
402  22 return getPropertyValue(property, targetType).orElse(defaultValue);
403    }
404   
405    /**
406    * {@inheritDoc}
407    */
 
408  34 toggle @Override
409    @SuppressWarnings("unchecked")
410    @Nonnull
411    public <T> Optional<T> getPropertyValue(@Nonnull final String property, @Nonnull final Class<T> targetType) {
412  34 requireNonNull(property, "property is required");
413  34 requireNonNull(targetType, "targetType is required");
414  34 final String value = value(property);
415  34 if (value == null) {
416  9 return Optional.empty();
417    }
418  25 Object returnedValue = value;
419  25 if (targetType.isAssignableFrom(Boolean.class)) {
420  11 returnedValue = Boolean.valueOf(value);
421  14 } else if (targetType.isAssignableFrom(Integer.class)) {
422  0 returnedValue = Integer.valueOf(value);
423  14 } else if (targetType.isAssignableFrom(Long.class)) {
424  0 returnedValue = Long.valueOf(value);
425    }
426  25 return Optional.ofNullable((T) returnedValue);
427    }
428   
429    /**
430    * Gets the list of all children name for the {@code parentNode}.
431    *
432    * @param parentNode
433    * the parent node to use (can be {@code null}.
434    * @return Returns a list of {@link String} representing the name of all children, which may be empty but never
435    * {@code null}.
436    * @since 1.3
437    */
 
438  0 toggle public List<String> getChildren(final Xpp3Dom parentNode) {
439  0 return Xpp3Utils.getChildren(parentNode);
440    }
441   
442    /**
443    * {@inheritDoc}
444    */
 
445  154 toggle @Override
446    @Nullable public <T> T getAttributeValue(@Nonnull final String property,
447    @Nonnull final String attribute,
448    @Nonnull final Class<T> targetType,
449    @Nullable final T defaultValue) {
450  154 return getAttributeValue(property, attribute, targetType).orElse(defaultValue);
451    }
452   
453    /**
454    * {@inheritDoc}
455    */
 
456  154 toggle @Override
457    @SuppressWarnings("unchecked")
458    @Nonnull
459    public <T> Optional<T> getAttributeValue(@Nonnull final String property,
460    @Nonnull final String attribute,
461    @Nonnull final Class<T> targetType) {
462  154 requireNonNull(property, "property is required");
463  154 requireNonNull(attribute, "attribute is required");
464  154 requireNonNull(targetType, "targetType is required");
465   
466  154 Xpp3Dom element = get(property);
467  154 if (element == null) {
468  55 return Optional.empty();
469    }
470  99 String value = element.getAttribute(attribute);
471  99 if (value == null) {
472  33 return Optional.empty();
473    }
474   
475  66 if ("inherit".equals(value) || Strings.isNullOrEmpty(value)) {
476  0 element = Xpp3Utils.getFirstChild(globalProperties, property, namespace);
477  0 if (LOGGER.isDebugEnabled()) {
478  0 LOGGER.debug("Inherit value property '{}': {}", property, element);
479    }
480    }
481  66 if (element == null) {
482  0 return Optional.empty();
483    }
484  66 value = element.getAttribute(attribute);
485  66 if (value == null) {
486  0 return Optional.empty();
487    }
488   
489  66 Object returnedValue = value;
490  66 if (targetType.isAssignableFrom(Boolean.class)) {
491  11 returnedValue = Boolean.valueOf(value);
492  55 } else if (targetType.isAssignableFrom(Integer.class)) {
493  0 returnedValue = Integer.valueOf(value);
494  55 } else if (targetType.isAssignableFrom(Long.class)) {
495  0 returnedValue = Long.valueOf(value);
496    }
497  66 return Optional.ofNullable((T) returnedValue);
498    }
499   
500    /**
501    * {@inheritDoc}
502    */
 
503  33 toggle @Override
504    @Nullable public <T> T getAttributeValue(@Nonnull final Xpp3Dom element,
505    @Nonnull final String attribute,
506    @Nonnull final Class<T> targetType,
507    @Nullable final T defaultValue) {
508  33 return getAttributeValue(element, attribute, targetType).orElse(defaultValue);
509    }
510   
511    /**
512    * {@inheritDoc}
513    */
 
514  33 toggle @Override
515    @SuppressWarnings("unchecked")
516    @Nonnull
517    public <T> Optional<T> getAttributeValue(@Nonnull final Xpp3Dom element,
518    @Nonnull final String attribute,
519    @Nonnull final Class<T> targetType) {
520  33 final String value = element.getAttribute(attribute);
521  33 if (value == null) {
522  22 return Optional.empty();
523    }
524  11 Object returnedValue = value;
525  11 if (targetType.isAssignableFrom(Boolean.class)) {
526  0 returnedValue = Boolean.valueOf(value);
527  11 } else if (targetType.isAssignableFrom(Integer.class)) {
528  0 returnedValue = Integer.valueOf(value);
529  11 } else if (targetType.isAssignableFrom(Long.class)) {
530  0 returnedValue = Long.valueOf(value);
531    }
532  11 return Optional.ofNullable((T) returnedValue);
533    }
534   
535    /**
536    * A convenience method to check if the value of the {@code property} is {@code "true"}.
537    *
538    * @param property
539    * the property of interest
540    * @return {@code true} if the configuration value is set either in page or globally, and is equal to {@code "true"}.
541    * @see #get(String)
542    * @since 1.0
543    */
 
544  13 toggle public boolean is(final String property) {
545  13 return "true".equals(value(property));
546    }
547   
548    /**
549    * {@inheritDoc}
550    */
 
551  13 toggle @Override
552    public boolean not(final String property) {
553  13 return "false".equals(value(property));
554    }
555   
556    /**
557    * A convenience method to check if the {@code property} is set to a specific value.
558    *
559    * @param property
560    * the property of interest
561    * @param value
562    * the property value to check
563    * @return {@code true} if the configuration value is set either in page or globally, and is equal to {@code value}.
564    * @see #get(String)
565    * @since 1.0
566    */
 
567  2 toggle public boolean isValue(final String property, final String value) {
568  2 return value != null && value.equals(value(property));
569    }
570   
571    /**
572    * {@inheritDoc}
573    */
 
574  0 toggle @Override
575    @Nullable public String getProjectId() {
576  0 return projectId;
577    }
578   
579    /**
580    * {@inheritDoc}
581    */
 
582  11 toggle @Override
583    @Nullable public String getFileId() {
584  11 return fileId;
585    }
586   
587    /**
588    * {@inheritDoc}
589    */
 
590  3 toggle @SuppressWarnings("unchecked")
591    @Override
592    @Nonnull
593    public <T extends Context<Context<?>>> T getContext() {
594  3 return (T) context;
595    }
596   
597    /**
598    * {@inheritDoc}
599    */
 
600  23 toggle @Override
601    public org.apache.velocity.context.Context getVelocityContext() {
602  23 return velocityContext;
603    }
604   
605    /**
606    * {@inheritDoc}
607    */
 
608  202 toggle @Override
609    @Nonnull
610    public MavenProject getProject() {
611  202 return project;
612    }
613   
614    /**
615    * {@inheritDoc}
616    */
 
617  27 toggle @Override
618    @Nonnull
619    public SiteModel getSiteModel() {
620  27 return siteModel;
621    }
622   
623    /**
624    * {@inheritDoc}
625    */
 
626  11 toggle @Override
627    @Nonnull
628    public Xpp3Dom getPageProperties() {
629  11 return pageProperties;
630    }
631   
632    /**
633    * {@inheritDoc}
634    */
 
635  12 toggle @Override
636    @Nonnull
637    public Xpp3Dom getGlobalProperties() {
638  12 return globalProperties;
639    }
640   
641    /**
642    * {@inheritDoc}
643    */
 
644  4 toggle @SuppressWarnings("null")
645    @Override
646    @Nonnull
647    public String getShortTitle() {
648  4 String shortTitle = getPropertyValue("shortTitle", String.class).orElse((String) velocityContext.get("shortTitle"));
649    // Page title generation is enabled by default, so check for false and negate
650  4 if (shortTitle == null || shortTitle.isEmpty() || "generate".equalsIgnoreCase(shortTitle)
651    || "true".equalsIgnoreCase(shortTitle)) {
652    // Generate the title.
653    // To generate the title, we take the contents of the first h1 tag in the
654    // document.
655    // If it does not exist, we take the contents of the first h2 tag.
656  0 final HtmlTool htmlTool = getHtmlTool();
657  0 final String body = getBodyContent();
658  0 List<String> hTexts = htmlTool.text(body, "h1");
659  0 if (hTexts.isEmpty()) {
660  0 hTexts = htmlTool.text(body, "h2");
661    }
662  0 if (!hTexts.isEmpty()) {
663  0 shortTitle = hTexts.get(0);
664    }
665    }
666  4 return shortTitle;
667    }
668   
669    /**
670    * {@inheritDoc}
671    */
 
672  4 toggle @Override
673    @Nonnull
674    public String getTitle() {
675  4 final String shortTitle = this.getShortTitle();
676  4 String title = getPropertyValue("title", String.class).orElse((String) velocityContext.get("title"));;
677    // If title template is provided, use it to generate title.
678    // Also regenerate title if a new short title was generated.
679  4 String titleTemplate = getPropertyValue("titleTemplate", String.class).orElse("%1$s - %2$s");
680  4 if (shortTitle != null) {
681  4 final SiteModel model = this.getSiteModel();
682    // the company/project name is retrieved from site model ( <project name="Foo">)
683    // or from the project itself
684  4 String titleProjectName = title;
685  4 if (titleProjectName == null || titleProjectName.isEmpty()) {
686  2 titleProjectName = null;
687  2 if (model.getName() != null) {
688  1 titleProjectName = model.getName();
689  1 } else if (getProject() != null && getProject().getName() != null) {
690  1 titleProjectName = getProject().getName();
691    }
692    }
693   
694  4 if (titleProjectName == null) {
695  0 return shortTitle;
696    }
697    // use String.format() to create new title
698  4 try {
699    // try to format the title according to the template
700  4 title = String.format(titleTemplate, titleProjectName, shortTitle);
701    } catch (final Exception e) {
702  0 LOGGER.warn(
703    "Cannot format title with template '{}' and tiltle '{}' and short title '{}', falling back to default format.",
704    titleTemplate,
705    titleProjectName,
706    title,
707    e);
708    }
709    }
710  4 return title;
711    }
712   
713    /**
714    * {@inheritDoc}
715    */
 
716  0 toggle @SuppressWarnings("null")
717    @Override
718    @Nonnull
719    public String getPublishDate() {
720  0 final SiteModel model = this.getSiteModel();
721  0 final PublishDate publishDate = model.getPublishDate();
722  0 String publishDateValue = null;
723  0 String format = "yyyy-MM-dd";
724  0 if (!"none".equals(publishDate.getPosition())) {
725  0 if (publishDate.getFormat() != null && !publishDate.getFormat().isEmpty()) {
726  0 format = publishDate.getFormat();
727    }
728  0 Date outputTimestamp = null;
729  0 try {
730  0 outputTimestamp = getBuildOutputTimestamp();
731    } catch (final ParseException e) {
732  0 LOGGER.warn("Cannot parse the build output timestamp!", e);
733  0 return null;
734    }
735   
736  0 final DateTool dateTool = getToolbox("date", DateTool.class);
737  0 if (dateTool != null) {
738  0 if (outputTimestamp != null) {
739  0 publishDateValue = dateTool.format(format, outputTimestamp);
740    } else {
741  0 publishDateValue = dateTool.get(format);
742    }
743    }
744    }
745  0 return publishDateValue;
746    }
747   
748    /**
749    * {@inheritDoc}
750    */
 
751  0 toggle @SuppressWarnings("null")
752    @Override
753    @Nonnull
754    public String getPublishDateMessage() {
755  0 final String publishDate = getPublishDate();
756  0 if (Strings.isNullOrEmpty(publishDate)) {
757  0 return null;
758    }
759  0 final ResourceTool text = this.getToolbox("text", ResourceTool.class);
760  0 if (text == null) {
761  0 return null;
762    }
763  0 return text.get("template.lastpublished") + ": " + publishDate;
764    }
765   
766    /**
767    * {@inheritDoc}
768    */
 
769  0 toggle @Override
770    @Nonnull
771    public String getDatePosition() {
772    // resolve date position, or set the default otherwise (bottom)
773    // Note, that currently we do not support "navigation-top" position for
774    // either publishDate or version.
775  0 PositionType datePosition = PositionType.none;
776  0 final SiteModel model = this.getSiteModel();
777  0 if (model != null) {
778  0 final PublishDate publishDate = model.getPublishDate();
779  0 if (publishDate != null) {
780  0 final String position = publishDate.getPosition();
781  0 if (position != null && !position.isEmpty()) {
782  0 datePosition = PositionType.valueOfHyphen(position);
783    }
784    }
785    }
786  0 if (datePosition == null) {
787  0 datePosition = PositionType.none;
788    }
789  0 return datePosition.toString();
790    }
791   
792    /**
793    * {@inheritDoc}
794    */
 
795  0 toggle @Override
796    @Nonnull
797    public String getVersion() {
798  0 final MavenProject project = this.getProject();
799  0 return project.getVersion();
800    }
801   
802    /**
803    * {@inheritDoc}
804    */
 
805  0 toggle @Override
806    @Nonnull
807    public String getVersionPosition() {
808  0 final SiteModel model = this.getSiteModel();
809  0 if (model == null || model.getVersion() == null) {
810  0 return PositionType.none.toString();
811    }
812  0 PositionType position = PositionType.valueOfHyphen(model.getVersion().getPosition());
813  0 if (position == null) {
814  0 position = PositionType.none;
815    }
816  0 return position.toString();
817    }
818   
819    /**
820    * {@inheritDoc}
821    */
 
822  0 toggle @SuppressWarnings("null")
823    @Override
824    @Nonnull
825    public String getVersionMessage() {
826  0 final String version = getVersion();
827  0 if (Strings.isNullOrEmpty(version)) {
828  0 return null;
829    }
830  0 final ResourceTool text = this.getToolbox("text", ResourceTool.class);
831  0 if (text == null) {
832  0 return null;
833    }
834  0 return text.get("template.version") + ": " + version;
835    }
836   
837    /**
838    * {@inheritDoc}
839    */
 
840  33 toggle @Override
841    @Nonnull
842    public String getNamespace() {
843  33 return namespace;
844    }
845   
846    /**
847    * @return Returns the html body content stored in velocity context.
848    * @since 2.1
849    */
 
850  0 toggle @Override
851    @Nonnull
852    public String getBodyContent() {
853  0 return requireNonNull(getContextValue("bodyContent", String.class));
854    }
855   
856    /**
857    * @return Returns the location of project.
858    */
 
859  188 toggle @Nonnull
860    public String getProjectLocation() {
861  188 String projectSiteLoc = getProject().getUrl();
862  188 if (!Strings.isNullOrEmpty(projectSiteLoc)) {
863   
864  187 if (!projectSiteLoc.endsWith("/")) {
865  0 projectSiteLoc += "/";
866    }
867    } else {
868  1 LOGGER.warn("the property project.url is required");
869  1 projectSiteLoc = "https://localhost/";
870    }
871  188 return projectSiteLoc;
872    }
873   
874    /**
875    * <p>
876    * See <a href= "https://maven.apache.org/doxia/doxia-sitetools/doxia-site-renderer/">Doxia Sitetools - Site
877    * Renderer</a> for more information.
878    *
879    * @return Returns a {@link String} representing the name of current file of the (HTML) document being rendered,
880    * relative to the site root.
881    */
 
882  198 toggle @Nonnull
883    public String getCurrentFileName() {
884  198 return (String) velocityContext.get("currentFileName");
885    }
886   
887    /**
888    * @return Returns a {@link String} representing the location path of current rendered file.
889    */
 
890  176 toggle @Nonnull
891    public String getCurrentFileLocation() {
892  176 final String projectSiteLoc = getProjectLocation();
893  176 return URITool.toURI(projectSiteLoc).resolve(getCurrentFileName()).toString();
894    }
895   
896    /**
897    * {@inheritDoc}
898    */
 
899  11 toggle @Override
900    @SuppressWarnings("unchecked")
901    @Nullable public <T> T eval(@Nullable final String vtl, @Nonnull final Class<T> requiredClass) {
902  11 if (vtl == null) {
903  0 return null;
904    }
905  11 final RenderTool renderTool = (RenderTool) getVelocityContext().get("render");
906  11 try {
907  11 return (T) renderTool.eval(getVelocityContext(), vtl);
908    } catch (final Exception ex) {
909  0 throw new RuntimeException("error when try evaluate '" + vtl + "'", ex);
910    }
911    }
912   
913    /**
914    * {@inheritDoc}
915    */
 
916  220 toggle @Override
917    public String relativeLink(final String href) {
918  220 if (href == null) {
919  0 return null;
920    }
921  220 if (isExternalLink(href)) {
922  44 return href;
923    }
924  176 final String relativePath = (String) velocityContext.get("relativePath");
925  176 String relativeLink = PathTool.calculateLink(href, relativePath);
926  176 relativeLink = relativeLink.replaceAll("\\\\", "/");
927  176 if (Strings.isNullOrEmpty(relativeLink)) {
928  0 relativeLink = "./";
929    }
930    // Attempt to normalise the relative link - this is useful for active link
931    // calculations and better relative links for subdirectories.
932    //
933    // The issue is particularly visible with pages in subdirectories,
934    // so that if you are in <root>/dev/index.html, the relative menu link to
935    // the _same_ page would likely be ../dev/index.html instead of '' or
936    // 'index.html'.
937  176 final String currentFileLoc = getCurrentFileLocation();
938  176 final String absoluteLink = URITool.toURI(currentFileLoc).resolve(relativeLink).normalize().toString();
939  176 if (currentFileLoc.equals(absoluteLink)) {
940    // for matching link, use empty relative link
941  0 relativeLink = StringUtils.EMPTY;
942    } else {
943    // relativize the absolute link based on current directory
944    // (uses Maven project link relativization)
945  176 final String currentFileDir = PathTool.getDirectoryComponent(currentFileLoc);
946  176 relativeLink = URITool.relativizeLink(currentFileDir, absoluteLink);
947    }
948  176 if (LOGGER.isTraceEnabled()) {
949  0 LOGGER.trace("-- Relative Link ----------------------------------");
950  0 LOGGER.trace("link: {}", href);
951  0 LOGGER.trace("currentFileLoc: {}", currentFileLoc);
952  0 LOGGER.trace("absoluteLink: {}", absoluteLink);
953  0 LOGGER.trace("relativeLink: {}", relativeLink);
954  0 LOGGER.trace("---------------------------------------------------");
955    }
956  176 return relativeLink;
957    }
958   
959    /**
960    * @param url
961    * a url.
962    * @return Returns {@code true} whether the link is a external link to the site.
963    */
 
964  228 toggle @SuppressWarnings("null")
965    @Override
966    public boolean isExternalLink(final String url) {
967  228 if (url == null) {
968  1 return false;
969    }
970  227 final String absoluteResourceURL = this.value("absoluteResourceURL");
971  227 if (!Strings.isNullOrEmpty(absoluteResourceURL) && url.startsWith(absoluteResourceURL)) {
972  12 return false;
973    }
974  215 return url.toLowerCase().startsWith("http:/") || url.toLowerCase().startsWith("https:/")
975    || url.toLowerCase().startsWith("ftp:/") || url.toLowerCase().startsWith("mailto:")
976    || url.toLowerCase().startsWith("file:/") || url.toLowerCase().indexOf("://") != -1;
977    }
978   
979    /**
980    * {@inheritDoc}
981    */
 
982  181 toggle @Override
983    public boolean isActiveLink(@Nullable final String href) {
984  181 final String alignedFileName = (String) velocityContext.get("alignedFileName");
985  181 if (href == null) {
986  1 return false;
987    }
988    // either empty link (pointing to a page), or if the current file is index.html,
989    // the link may point to the whole directory
990  180 return Strings.isNullOrEmpty(href) || alignedFileName.endsWith("index.html") && ".".equals(href);
991    }
992   
993    /**
994    * Converts a filename to pageId format.
995    *
996    * @param fileName
997    * the filename to convert
998    * @return Returns a {@link String} representing the pageId of {@code filename}.
999    */
 
1000  50 toggle @Nullable public static String slugFilename(@Nullable final String fileName) {
1001  50 if (fileName == null) {
1002  0 return null;
1003    }
1004  50 String currentFile = fileName;
1005   
1006    // drop the extension
1007  50 final int lastDot = currentFile.lastIndexOf(".");
1008  50 if (lastDot >= 0) {
1009  49 currentFile = currentFile.substring(0, lastDot);
1010    }
1011   
1012    // get the short ID (in case of nested files)
1013    // String fileName = new File(currentFile).getName();
1014    // fileShortId = HtmlTool.slug(fileName);
1015   
1016    // full file ID includes the nested dirs
1017    // replace nesting "/" with "-"
1018  50 return HtmlTool.slug(currentFile.replace("/", "-").replace("\\", "-"));
1019    }
1020   
1021    /**
1022    * @return Returns a {@link String} representing the relative path to root site.
1023    */
 
1024  11 toggle @SuppressWarnings("null")
1025    @Override
1026    @Nonnull
1027    public String getResourcePath() {
1028  11 final String absoluteResourceURL = this.value("absoluteResourceURL");
1029  11 String projectUrl = getProjectLocation();
1030  11 final String currentFileName = getCurrentFileName();
1031  11 if (!Strings.isNullOrEmpty(projectUrl) && currentFileName != null) {
1032  11 if (projectUrl.charAt(projectUrl.length() - 1) != '/') {
1033  0 projectUrl += '/';
1034    }
1035  11 final String currentFileDir = URITool.toURI(projectUrl).resolve(currentFileName).resolve(".").toString();
1036  11 return URITool.relativizeLink(currentFileDir, absoluteResourceURL);
1037    }
1038  0 return (String) velocityContext.get("relativePath");
1039    }
1040   
1041    /**
1042    * Gets the reproduce build timestamp whether property 'project.build.outputTimestamp' is fill in.
1043    *
1044    * @return Returns a instance of {@code Date} representing the reproduce build timestamp whether property
1045    * 'project.build.outputTimestamp' is fill in.
1046    */
 
1047  0 toggle @Override
1048    @Nullable public Date getBuildOutputTimestamp() throws ParseException {
1049  0 if (!this.velocityContext.containsKey(PROJECT_BUILD_OUTPUTTIMESTAMP)) {
1050  0 return null;
1051    }
1052  0 final Object outputTimestamp = this.velocityContext.get(PROJECT_BUILD_OUTPUTTIMESTAMP);
1053  0 if (outputTimestamp != null) {
1054  0 return ISO_8601BASIC_DATE.parse(outputTimestamp.toString());
1055    }
1056  0 return null;
1057    }
1058   
1059    }