1 /* 2 * Copyright 2012 Marek Romanowski 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 pl.matsuo.maven.skins.msb4; 17 18 import org.apache.maven.doxia.site.decoration.DecorationModel; 19 import org.apache.maven.project.MavenProject; 20 import org.apache.velocity.tools.ToolContext; 21 import org.apache.velocity.tools.config.DefaultKey; 22 import org.apache.velocity.tools.generic.SafeConfig; 23 import org.apache.velocity.tools.generic.ValueParser; 24 import org.codehaus.plexus.util.xml.Xpp3Dom; 25 26 /** 27 * An Apache Velocity tool that simplifies retrieval of custom configuration values for a 28 * Maven Site. 29 * <p> 30 * The tool is configured to access Maven site configuration of a skin inside {@code <custom>} 31 * element of site descriptor. It supports global properties (defined at skin level) and per-page 32 * properties (defined in {@code <page><mypage>} element). The per-page properties override the 33 * global ones. 34 * </p> 35 * <p> 36 * A sample configuration would be like that: 37 * </p> 38 * 39 * <pre> 40 * {@code 41 * <custom> 42 * <msb4Skin> 43 * <prop1>value1</prop1> 44 * <prop2> 45 * <prop21>value2</prop21> 46 * </prop2> 47 * <pages> 48 * <mypage project="myproject"> 49 * <prop1>override value1</prop1> 50 * </mypage> 51 * </pages> 52 * </msb4Skin> 53 * </custom> 54 * } 55 * </pre> 56 * <p> 57 * To get the value of {@code prop1}, one would simply use {@code $config.prop1}. This would return 58 * "override value1". Then {@code $config.prop2} would return "value2" - the global value. 59 * </p> 60 * <p> 61 * The tool allows querying the value easily, falling back from page to global configuration to 62 * {@code null}, if none is available. It also provides convenience accessors for common values. 63 * </p> 64 * <p> 65 * Note 66 * </p> 67 * 68 * @author Andrius Velykis 69 * @since 1.0 70 */ 71 @DefaultKey("config") 72 public class SkinConfigTool extends SafeConfig { 73 74 public static final String DEFAULT_KEY = "config"; 75 76 /** By default use Msb4 skin configuration tag */ 77 public static final String SKIN_KEY = "msb4Skin"; 78 79 private String key = DEFAULT_KEY; 80 private String skinKey = SKIN_KEY; 81 82 /* Create dummy nodes to avoid null checks */ 83 private Xpp3Dom globalProperties = new Xpp3Dom(""); 84 private Xpp3Dom pageProperties = new Xpp3Dom(""); 85 private String namespace = ""; 86 87 private String projectId = null; 88 private String fileId = null; 89 // private String fileShortId = null; 90 91 /** 92 * {@inheritDoc} 93 * 94 * @see SafeConfig#configure(ValueParser) 95 */ 96 @Override 97 protected void configure(ValueParser values) { 98 String altkey = values.getString("key"); 99 if (altkey != null) { 100 setKey(altkey); 101 } 102 103 // allow changing skin key in the configuration 104 String altSkinKey = values.getString("skinKey"); 105 if (altSkinKey != null) { 106 this.skinKey = altSkinKey; 107 } 108 109 // retrieve the decoration model from Velocity context 110 Object velocityContext = values.get("velocityContext"); 111 112 if (!(velocityContext instanceof ToolContext)) { 113 return; 114 } 115 116 ToolContext ctxt = (ToolContext) velocityContext; 117 118 Object projectObj = ctxt.get("project"); 119 if (projectObj instanceof MavenProject) { 120 MavenProject project = (MavenProject) projectObj; 121 String artifactId = project.getArtifactId(); 122 // use artifactId "sluggified" as the projectId 123 projectId = HtmlTool.slug(artifactId); 124 } 125 126 // calculate the page ID from the current file name 127 Object currentFileObj = ctxt.get("currentFileName"); 128 if (currentFileObj instanceof String) { 129 130 String currentFile = (String) currentFileObj; 131 132 // drop the extension 133 int lastDot = currentFile.lastIndexOf("."); 134 if (lastDot >= 0) { 135 currentFile = currentFile.substring(0, lastDot); 136 } 137 138 // get the short ID (in case of nested files) 139 // String fileName = new File(currentFile).getName(); 140 // fileShortId = HtmlTool.slug(fileName); 141 142 // full file ID includes the nested dirs 143 // replace nesting "/" with "-" 144 fileId = HtmlTool.slug(currentFile.replace("/", "-").replace("\\", "-")); 145 } 146 147 Object decorationObj = ctxt.get("decoration"); 148 149 if (!(decorationObj instanceof DecorationModel)) { 150 return; 151 } 152 153 DecorationModel decoration = (DecorationModel) decorationObj; 154 Object customObj = decoration.getCustom(); 155 156 if (!(customObj instanceof Xpp3Dom)) { 157 return; 158 } 159 160 // Now that we have the custom node, get the global properties 161 // under the skin tag 162 Xpp3Dom customNode = (Xpp3Dom) customObj; 163 Xpp3Dom skinNode = customNode.getChild(skinKey); 164 String namespaceKey = ":" + skinKey; 165 166 if (skinNode == null) { 167 // try searching with any namespace 168 for (Xpp3Dom child : customNode.getChildren()) { 169 if (child.getName().endsWith(namespaceKey)) { 170 skinNode = child; 171 break; 172 } 173 } 174 } 175 176 if (skinNode != null) { 177 globalProperties = skinNode; 178 179 if (skinNode.getName().endsWith(namespaceKey)) { 180 // extract the namespace (including the colon) 181 namespace = skinNode.getName().substring(0, skinNode.getName().length() - namespaceKey.length() + 1); 182 } 183 184 // for page properties, retrieve the file name and drop the `.html` 185 // extension - this will be used, i.e. `index` instead of `index.html` 186 Xpp3Dom pagesNode = getChild(skinNode, "pages"); 187 if (pagesNode != null) { 188 189 // Get the page for the file 190 // TODO try fileShortId as well? 191 Xpp3Dom page = getChild(pagesNode, fileId); 192 193 // Now check if the project artifact ID is set, and if so, if it matches the 194 // current project. This allows preventing accidental reuse of parent page 195 // configs in children modules 196 if (page != null && projectId != null) { 197 String pageProject = page.getAttribute("project"); 198 if (pageProject != null && !projectId.equals(pageProject)) { 199 // project ID indicated, and is different - do not use the config 200 page = null; 201 } 202 } 203 204 if (page != null) { 205 pageProperties = page; 206 } 207 } 208 } 209 } 210 211 /** 212 * Retrieves the child node. Tests both default name and with namespace. 213 * 214 * @param parentNode 215 * @param name 216 * @return value 217 */ 218 private Xpp3Dom getChild(Xpp3Dom parentNode, String name) { 219 Xpp3Dom child = parentNode.getChild(name); 220 if (child != null) { 221 return child; 222 } 223 224 return parentNode.getChild(namespace + name); 225 } 226 227 /** 228 * Sets the key under which this tool has been configured. 229 * 230 * @param key sets key 231 * @since 1.0 232 */ 233 protected void setKey(String key) { 234 if (key == null) { 235 throw new NullPointerException("SkinConfigTool key cannot be null"); 236 } 237 this.key = key; 238 } 239 240 /** 241 * Should return the key under which this tool has been configured. The default is `config`. 242 * 243 * @return key 244 * @since 1.0 245 */ 246 public String getKey() { 247 return this.key; 248 } 249 250 /** 251 * Default accessor for config properties. Instead of using {@code $config.get("myproperty")}, 252 * one can utilise Velocity fallback onto the default getter and use {@code $config.myproperty}. 253 * 254 * @param property 255 * the property of interest 256 * @return configuration node if found in the following sequence: 257 * <ol> 258 * <li>In page configuration</li> 259 * <li>In global configuration</li> 260 * <li>{@code null} otherwise</li> 261 * </ol> 262 * @since 1.0 263 */ 264 public Xpp3Dom get(String property) { 265 266 // first try page properties 267 Xpp3Dom propNode = getChild(pageProperties, property); 268 if (propNode == null) { 269 // try global 270 propNode = getChild(globalProperties, property); 271 } 272 273 return propNode; 274 } 275 276 /** 277 * Retrieves the text value of the given {@code property}, e.g. as in 278 * {@code <myprop>value</myprop>}. 279 * 280 * @param property 281 * the property of interest 282 * @return the configuration value if found in page or globally, {@code null} otherwise. 283 * @see #get(String) 284 * @since 1.0 285 */ 286 public String value(String property) { 287 288 Xpp3Dom propNode = get(property); 289 290 if (propNode == null) { 291 // not found 292 return null; 293 } 294 295 return propNode.getValue(); 296 } 297 298 /** 299 * A convenience method to check if the value of the {@code property} is {@code "true"}. 300 * 301 * @param property 302 * the property of interest 303 * @return {@code true} if the configuration value is set either in page or globally, and is 304 * equal to {@code "true"}. 305 * @see #get(String) 306 * @since 1.0 307 */ 308 public boolean is(String property) { 309 return "true".equals(value(property)); 310 } 311 312 /** 313 * A convenience method to check if the value of the {@code property} is {@code "false"}. Useful 314 * for properties that are enabled by default - checks if the property is set to {@code "false"} 315 * explicitly. 316 * 317 * @param property 318 * the property of interest 319 * @return {@code true} if the configuration value is set either in page or globally, and is 320 * equal to {@code "false"}. Note that this will return {@code false} if property is not 321 * set at all. 322 * @see #get(String) 323 * @since 1.0 324 */ 325 public boolean not(String property) { 326 return "false".equals(value(property)); 327 } 328 329 /** 330 * A convenience method to check if the {@code property} is set to a specific value. 331 * 332 * @param property 333 * the property of interest 334 * @param value 335 * the property value to check 336 * @return {@code true} if the configuration value is set either in page or globally, and is 337 * equal to {@code value}. 338 * @see #get(String) 339 * @since 1.0 340 */ 341 public boolean isValue(String property, String value) { 342 return value != null && value.equals(value(property)); 343 } 344 345 public String getProjectId() { 346 return projectId; 347 } 348 349 public String getFileId() { 350 return fileId; 351 } 352 353 }