1. Project Clover database mer. févr. 4 2026 12:48:44 CET
  2. Package org.devacfr.maven.skins.reflow

File SkinConfigTool.java

 

Coverage histogram

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

Code metrics

152
309
49
1
1 062
635
141
0,46
6,31
49
2,88

Classes

Class Line # Actions
SkinConfigTool 96 309 0% 141 217
0.574509857,5%
 

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