1. Project Clover database mar. avr. 16 2024 08:19:06 CEST
  2. Package org.devacfr.maven.skins.reflow

File SkinConfigTool.java

 

Coverage histogram

../../../../../img/srcFileCovDistChart7.png
50% of files have more coverage

Code metrics

108
211
34
1
796
420
94
0,45
6,21
34
2,76

Classes

Class Line # Actions
SkinConfigTool 91 211 0% 94 124
0.648725264,9%
 

Contributing tests

This file is covered by 7 tests. .

Source view

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  6 toggle @Override
146    protected void configure(final ValueParser values) {
147  6 final String altkey = values.getString("key");
148  6 if (altkey != null) {
149  0 setKey(altkey);
150    }
151   
152    // allow changing skin key in the configuration
153  6 final String altSkinKey = values.getString("skinKey");
154  6 if (altSkinKey != null) {
155  0 this.skinKey = altSkinKey;
156    }
157   
158    // retrieve the site model from Velocity context
159  6 final Object vc = values.get("velocityContext");
160   
161  6 if (!(vc instanceof ToolContext)) {
162  0 return;
163    }
164   
165  6 this.velocityContext = (ToolContext) vc;
166   
167  6 final Object projectObj = velocityContext.get("project");
168  6 if (projectObj instanceof MavenProject) {
169  6 this.project = (MavenProject) projectObj;
170  6 final String artifactId = project.getArtifactId();
171    // use artifactId "sluggified" as the projectId
172  6 projectId = HtmlTool.slug(artifactId);
173    }
174   
175    // calculate the page ID from the current file name
176  6 final String currentFileObj = getCurrentFileName();
177  6 fileId = slugFilename(currentFileObj);
178   
179  6 final Object siteModelObj = velocityContext.get("site");
180   
181  6 if (!(siteModelObj instanceof SiteModel)) {
182  0 return;
183    }
184   
185  6 this.siteModel = (SiteModel) siteModelObj;
186  6 final Object customObj = siteModel.getCustom();
187   
188  6 if (!(customObj instanceof Xpp3Dom)) {
189  0 return;
190    }
191   
192    // Now that we have the custom node, get the global properties
193    // under the skin tag
194  6 final Xpp3Dom customNode = (Xpp3Dom) customObj;
195  6 Xpp3Dom skinNode = customNode.getChild(skinKey);
196  6 final String namespaceKey = ":" + skinKey;
197   
198  6 if (skinNode == null) {
199    // try searching with any namespace
200  0 for (final Xpp3Dom child : customNode.getChildren()) {
201  0 if (child.getName().endsWith(namespaceKey)) {
202  0 skinNode = child;
203  0 break;
204    }
205    }
206    }
207   
208  6 if (skinNode != null) {
209  6 globalProperties = skinNode;
210   
211  6 if (skinNode.getName().endsWith(namespaceKey)) {
212    // extract the namespace (including the colon)
213  0 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  6 final Xpp3Dom pagesNode = Xpp3Utils.getFirstChild(skinNode, "pages", namespace);
220  6 if (pagesNode != null) {
221   
222    // Get the page for the file
223  6 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  6 if (page != null && projectId != null) {
229  6 final String pageProject = page.getAttribute("project");
230  6 if (pageProject != null && !projectId.equals(pageProject)) {
231    // project ID indicated, and is different - do not use the config
232  0 page = null;
233    }
234    }
235   
236  6 if (page != null) {
237  6 pageProperties = page;
238    }
239    }
240   
241    // Config option <localResources>true</localResources> to force CDN-less
242    // Bootstrap & jQuery
243  6 this.velocityContext.put("localResources", is("localResources"));
244    // Use config option
245    // <absoluteResourceURL>http://mysite.com/</absoluteResourceURL>
246  6 this.velocityContext.put("resourcePath", getResourcePath());
247   
248  6 this.context = Context.buildContext(this);
249    }
250  6 if (LOGGER.isDebugEnabled()) {
251  0 LOGGER.debug("Current Filename: {}", currentFileObj);
252  0 LOGGER.debug("Project id: {}", projectId);
253  0 LOGGER.debug("File id: {}", fileId);
254  0 LOGGER.debug("Context: {}", this.context);
255  0 LOGGER.debug("Namespace: {}", this.namespace);
256  0 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  0 toggle protected void setKey(final String key) {
268  0 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  0 toggle public String getKey() {
276  0 return this.key;
277    }
278   
279    /**
280    * {@inheritDoc}
281    */
 
282  0 toggle @Override
283    @Nullable public <T> T getContextValue(@Nonnull final String key, @Nonnull final Class<T> type) {
284  0 requireNonNull(type);
285  0 if (String.class.isAssignableFrom(type)) {
286  0 return this.eval("$" + key, type);
287    } else {
288  0 throw new UnsupportedOperationException();
289    }
290    }
291   
292    /**
293    * {@inheritDoc}
294    */
 
295  0 toggle @Override
296    public void setContextValue(@Nonnull final String key, @Nullable final Object value) {
297  0 requireNonNull(key);
298  0 if (value instanceof String) {
299  0 this.eval("#set( $" + key + "= \"" + value.toString() + "\")", Void.class);
300    } else {
301  0 throw new UnsupportedOperationException();
302    }
303    }
304   
305    /**
306    * {@inheritDoc}
307    */
 
308  0 toggle @Override
309    @Nullable @SuppressWarnings("unchecked")
310    public <T> T getToolbox(@Nonnull final String toolName, @Nonnull final Class<T> toolType) {
311  0 requireNonNull(toolType);
312  0 return (T) this.velocityContext.getToolbox().get(requireNonNull(toolName));
313    }
314   
315    /**
316    * {@inheritDoc}
317    */
 
318  271 toggle @Override
319    @Nullable public Xpp3Dom get(@Nonnull final String property) {
320  271 requireNonNull(property);
321    // first try page properties
322  271 Xpp3Dom propNode = Xpp3Utils.getFirstChild(pageProperties, property, namespace);
323  271 if (propNode == null) {
324    // try global
325  265 propNode = Xpp3Utils.getFirstChild(globalProperties, property, namespace);
326    }
327   
328  271 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  163 toggle @Nullable public String value(@Nonnull final String property) {
341  163 requireNonNull(property);
342  163 final Xpp3Dom propNode = get(property);
343   
344  163 if (propNode == null) {
345    // not found
346  6 return null;
347    }
348   
349  157 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  12 toggle @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  12 requireNonNull(property, "property is required");
372  12 requireNonNull(targetType, "targetType is required");
373  12 final String value = value(property);
374  12 if (value == null) {
375  0 return defaultValue;
376    }
377  12 Object returnedValue = value;
378  12 if (targetType.isAssignableFrom(Boolean.class)) {
379  6 returnedValue = Boolean.valueOf(value);
380  6 } else if (targetType.isAssignableFrom(Integer.class)) {
381  0 returnedValue = Integer.valueOf(value);
382  6 } else if (targetType.isAssignableFrom(Long.class)) {
383  0 returnedValue = Long.valueOf(value);
384    }
385  12 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  0 toggle public List<String> getChildren(final Xpp3Dom parentNode) {
398  0 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  84 toggle @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  84 requireNonNull(property, "property is required");
424  84 requireNonNull(attribute, "attribute is required");
425  84 requireNonNull(targetType, "targetType is required");
426   
427  84 Xpp3Dom element = get(property);
428  84 if (element == null) {
429  30 return defaultValue;
430    }
431  54 String value = element.getAttribute(attribute);
432  54 if (value == null) {
433  18 return defaultValue;
434    }
435   
436  36 if ("inherit".equals(value) || Strings.isNullOrEmpty(value)) {
437  0 element = Xpp3Utils.getFirstChild(globalProperties, property, namespace);
438  0 if (LOGGER.isDebugEnabled()) {
439  0 LOGGER.debug("Inherit value property '{}': {}", property, element);
440    }
441    }
442  36 if (element == null) {
443  0 return defaultValue;
444    }
445  36 value = element.getAttribute(attribute);
446  36 if (value == null) {
447  0 return defaultValue;
448    }
449   
450  36 Object returnedValue = value;
451  36 if (targetType.isAssignableFrom(Boolean.class)) {
452  6 returnedValue = Boolean.valueOf(value);
453  30 } else if (targetType.isAssignableFrom(Integer.class)) {
454  0 returnedValue = Integer.valueOf(value);
455  30 } else if (targetType.isAssignableFrom(Long.class)) {
456  0 returnedValue = Long.valueOf(value);
457    }
458  36 return (T) returnedValue;
459    }
460   
461    /**
462    * {@inheritDoc}
463    */
 
464  18 toggle @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  18 if (element == null) {
471  0 return defaultValue;
472    }
473  18 final String value = element.getAttribute(attribute);
474  18 if (value == null) {
475  12 return defaultValue;
476    }
477  6 Object returnedValue = value;
478  6 if (targetType.isAssignableFrom(Boolean.class)) {
479  0 returnedValue = Boolean.valueOf(value);
480  6 } else if (targetType.isAssignableFrom(Integer.class)) {
481  0 returnedValue = Integer.valueOf(value);
482  6 } else if (targetType.isAssignableFrom(Long.class)) {
483  0 returnedValue = Long.valueOf(value);
484    }
485  6 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  8 toggle public boolean is(final String property) {
499  8 return "true".equals(value(property));
500    }
501   
502    /**
503    * {@inheritDoc}
504    */
 
505  8 toggle @Override
506    public boolean not(final String property) {
507  8 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  2 toggle public boolean isValue(final String property, final String value) {
522  2 return value != null && value.equals(value(property));
523    }
524   
525    /**
526    * {@inheritDoc}
527    */
 
528  0 toggle @Override
529    @Nullable public String getProjectId() {
530  0 return projectId;
531    }
532   
533    /**
534    * {@inheritDoc}
535    */
 
536  6 toggle @Override
537    @Nullable public String getFileId() {
538  6 return fileId;
539    }
540   
541    /**
542    * @return the context
543    */
 
544  0 toggle @Override
545    @Nonnull
546    public Context<?> getContext() {
547  0 return context;
548    }
549   
550    /**
551    * @return the velocity Context
552    */
 
553  12 toggle public ToolContext getVelocityContext() {
554  12 return velocityContext;
555    }
556   
557    /**
558    * {@inheritDoc}
559    */
 
560  108 toggle @Override
561    @Nonnull
562    public MavenProject getProject() {
563  108 return project;
564    }
565   
566    /**
567    * {@inheritDoc}
568    */
 
569  12 toggle @Override
570    @Nonnull
571    public SiteModel getSiteModel() {
572  12 return siteModel;
573    }
574   
575    /**
576    * {@inheritDoc}
577    */
 
578  6 toggle @Override
579    @Nonnull
580    public Xpp3Dom getPageProperties() {
581  6 return pageProperties;
582    }
583   
584    /**
585    * {@inheritDoc}
586    */
 
587  6 toggle @Override
588    @Nonnull
589    public Xpp3Dom getGlobalProperties() {
590  6 return globalProperties;
591    }
592   
593    /**
594    * {@inheritDoc}
595    */
 
596  18 toggle @Override
597    @Nonnull
598    public String getNamespace() {
599  18 return namespace;
600    }
601   
602    /**
603    * @return Returns the location of project.
604    */
 
605  102 toggle @Nonnull
606    public String getProjectLocation() {
607  102 String projectSiteLoc = getProject().getUrl();
608  102 if (!Strings.isNullOrEmpty(projectSiteLoc)) {
609   
610  102 if (!projectSiteLoc.endsWith("/")) {
611  0 projectSiteLoc += "/";
612    }
613    }
614  102 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  108 toggle @Nonnull
626    public String getCurrentFileName() {
627  108 return (String) velocityContext.get("currentFileName");
628    }
629   
630    /**
631    * @return Returns a {@link String} representing the location path of current rendered file.
632    */
 
633  96 toggle @Nonnull
634    public String getCurrentFileLocation() {
635  96 final String projectSiteLoc = getProjectLocation();
636  96 return URITool.toURI(projectSiteLoc).resolve(getCurrentFileName()).toString();
637    }
638   
639    /**
640    * {@inheritDoc}
641    */
 
642  6 toggle @Override
643    @SuppressWarnings("unchecked")
644    @Nullable public <T> T eval(@Nullable final String vtl, @Nonnull final Class<T> requiredClass) {
645  6 if (vtl == null) {
646  0 return null;
647    }
648  6 final RenderTool renderTool = (RenderTool) getVelocityContext().get("render");
649  6 try {
650  6 return (T) renderTool.eval(getVelocityContext(), vtl);
651    } catch (final Exception ex) {
652  0 throw new RuntimeException("error when try evaluate '" + vtl + "'", ex);
653    }
654    }
655   
656    /**
657    * {@inheritDoc}
658    */
 
659  120 toggle @Override
660    public String relativeLink(final String href) {
661  120 if (href == null) {
662  0 return null;
663    }
664  120 if (isExternalLink(href)) {
665  24 return href;
666    }
667  96 final String relativePath = (String) velocityContext.get("relativePath");
668  96 String relativeLink = PathTool.calculateLink(href, relativePath);
669  96 relativeLink = relativeLink.replaceAll("\\\\", "/");
670  96 if (Strings.isNullOrEmpty(relativeLink)) {
671  0 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  96 final String currentFileLoc = getCurrentFileLocation();
681  96 final String absoluteLink = URITool.toURI(currentFileLoc).resolve(relativeLink).normalize().toString();
682  96 if (currentFileLoc.equals(absoluteLink)) {
683    // for matching link, use empty relative link
684  0 relativeLink = StringUtils.EMPTY;
685    } else {
686    // relativize the absolute link based on current directory
687    // (uses Maven project link relativization)
688  96 final String currentFileDir = PathTool.getDirectoryComponent(currentFileLoc);
689  96 relativeLink = URITool.relativizeLink(currentFileDir, absoluteLink);
690    }
691  96 if (LOGGER.isDebugEnabled()) {
692  0 LOGGER.debug("-- Relative Link ----------------------------------");
693  0 LOGGER.debug("link: {}", href);
694  0 LOGGER.debug("currentFileLoc: {}", currentFileLoc);
695  0 LOGGER.debug("absoluteLink: {}", absoluteLink);
696  0 LOGGER.debug("relativeLink: {}", relativeLink);
697  0 LOGGER.debug("---------------------------------------------------");
698    }
699  96 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  128 toggle public boolean isExternalLink(final String url) {
708  128 if (url == null) {
709  1 return false;
710    }
711  127 final String absoluteResourceURL = this.value("absoluteResourceURL");
712  127 if (!Strings.isNullOrEmpty(absoluteResourceURL) && url.startsWith(absoluteResourceURL)) {
713  7 return false;
714    }
715  120 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  101 toggle @Override
724    public boolean isActiveLink(@Nullable final String href) {
725  101 final String alignedFileName = (String) velocityContext.get("alignedFileName");
726  101 if (href == null) {
727  1 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  100 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  30 toggle @Nullable public static String slugFilename(@Nullable final String fileName) {
742  30 if (fileName == null) {
743  0 return null;
744    }
745  30 String currentFile = fileName;
746   
747    // drop the extension
748  30 final int lastDot = currentFile.lastIndexOf(".");
749  30 if (lastDot >= 0) {
750  29 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  30 return HtmlTool.slug(currentFile.replace("/", "-").replace("\\", "-"));
760    }
761   
762    /**
763    * @return Returns a {@link String} representing the relative path to root site.
764    */
 
765  6 toggle @Nonnull
766    public String getResourcePath() {
767  6 final String absoluteResourceURL = this.value("absoluteResourceURL");
768  6 String projectUrl = getProjectLocation();
769  6 final String currentFileName = getCurrentFileName();
770  6 if (!Strings.isNullOrEmpty(projectUrl) && currentFileName != null) {
771  6 if (projectUrl.charAt(projectUrl.length() - 1) != '/') {
772  0 projectUrl += '/';
773    }
774  6 final String currentFileDir = URITool.toURI(projectUrl).resolve(currentFileName).resolve(".").toString();
775  6 return URITool.relativizeLink(currentFileDir, absoluteResourceURL);
776    }
777  0 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  0 toggle @Nullable public Date getBuildOutputTimestamp() throws ParseException {
787  0 if (!this.velocityContext.containsKey(PROJECT_BUILD_OUTPUTTIMESTAMP)) {
788  0 return null;
789    }
790  0 Object outputTimestamp = this.velocityContext.get(PROJECT_BUILD_OUTPUTTIMESTAMP);
791  0 if (outputTimestamp != null) {
792  0 return ISO_8601BASIC_DATE.parse(outputTimestamp.toString());
793    }
794  0 return null;
795    }
796    }