001/* 002 * This library is part of OpenCms - 003 * the Open Source Content Management System 004 * 005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com) 006 * 007 * This library is free software; you can redistribute it and/or 008 * modify it under the terms of the GNU Lesser General Public 009 * License as published by the Free Software Foundation; either 010 * version 2.1 of the License, or (at your option) any later version. 011 * 012 * This library is distributed in the hope that it will be useful, 013 * but WITHOUT ANY WARRANTY; without even the implied warranty of 014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 015 * Lesser General Public License for more details. 016 * 017 * For further information about Alkacon Software GmbH & Co. KG, please see the 018 * company website: http://www.alkacon.com 019 * 020 * For further information about OpenCms, please see the 021 * project website: http://www.opencms.org 022 * 023 * You should have received a copy of the GNU Lesser General Public 024 * License along with this library; if not, write to the Free Software 025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 026 */ 027 028package org.opencms.file.wrapper; 029 030import org.opencms.file.CmsFile; 031import org.opencms.file.CmsObject; 032import org.opencms.file.CmsProperty; 033import org.opencms.file.CmsPropertyDefinition; 034import org.opencms.file.CmsResource; 035import org.opencms.file.types.CmsResourceTypePlain; 036import org.opencms.file.types.I_CmsResourceType; 037import org.opencms.i18n.CmsEncoder; 038import org.opencms.main.CmsException; 039import org.opencms.main.OpenCms; 040import org.opencms.util.CmsStringUtil; 041 042import java.io.ByteArrayInputStream; 043import java.io.IOException; 044import java.io.UnsupportedEncodingException; 045import java.util.ArrayList; 046import java.util.HashMap; 047import java.util.Iterator; 048import java.util.List; 049import java.util.Map; 050import java.util.Properties; 051import java.util.regex.Matcher; 052import java.util.regex.Pattern; 053 054/** 055 * Helper class with several methods used by different implementations of the 056 * interface {@link I_CmsResourceWrapper}.<p> 057 * 058 * It provides methods to add or remove file extensions to resources, to handle 059 * creating and writing property files and to add the byte order mask to UTF-8 060 * byte contents.<p> 061 * 062 * @since 6.2.4 063 */ 064public final class CmsResourceWrapperUtils { 065 066 /** The extension to use for the property file. */ 067 public static final String EXTENSION_PROPERTIES = "properties"; 068 069 /** Property name used for reading / changing the resource type. */ 070 public static final String PROPERTY_RESOURCE_TYPE = "resourceType"; 071 072 /** The prefix used for a shared property entry. */ 073 public static final String SUFFIX_PROP_INDIVIDUAL = ".i"; 074 075 /** The prefix used for a shared property entry. */ 076 public static final String SUFFIX_PROP_SHARED = ".s"; 077 078 /** The UTF-8 bytes to add to the beginning of text contents. */ 079 public static final byte[] UTF8_MARKER = new byte[] {(byte)0xEF, (byte)0xBB, (byte)0xBF}; 080 081 /** Pattern to use for incoming strings before storing in OpenCms. */ 082 private static final Pattern PATTERN_UNESCAPE = Pattern.compile("\\\\([^ntru\n\r])"); 083 084 /** 085 * Hide utility class constructor.<p> 086 */ 087 private CmsResourceWrapperUtils() { 088 089 // noop 090 } 091 092 /** 093 * Adds a file extension to the resource name.<p> 094 * 095 * If the file with the new extension already exists, an index count will be 096 * added before the final extension.<p> 097 * 098 * For example: <code>index.html.1.jsp</code>.<p> 099 * 100 * @see #removeFileExtension(CmsObject, String, String) 101 * 102 * @param cms the actual CmsObject 103 * @param resourcename the name of the resource where to add the file extension 104 * @param extension the extension to add 105 * 106 * @return the resource name with the added file extension 107 */ 108 public static String addFileExtension(CmsObject cms, String resourcename, String extension) { 109 110 if (!extension.startsWith(".")) { 111 extension = "." + extension; 112 } 113 114 if (!resourcename.endsWith(extension)) { 115 String name = resourcename + extension; 116 int count = 0; 117 while (cms.existsResource(name)) { 118 count++; 119 name = resourcename + "." + count + extension; 120 } 121 122 return name; 123 } 124 125 return resourcename; 126 } 127 128 /** 129 * Adds the UTF-8 marker add the beginning of the byte array.<p> 130 * 131 * @param content the byte array where to add the UTF-8 marker 132 * 133 * @return the byte with the added UTF-8 marker at the beginning 134 */ 135 public static byte[] addUtf8Marker(byte[] content) { 136 137 if ((content != null) 138 && (content.length >= 3) 139 && (content[0] == UTF8_MARKER[0]) 140 && (content[1] == UTF8_MARKER[1]) 141 && (content[2] == UTF8_MARKER[2])) { 142 return content; 143 } 144 145 if (content == null) { 146 content = new byte[0]; 147 } 148 149 byte[] ret = new byte[UTF8_MARKER.length + content.length]; 150 151 System.arraycopy(UTF8_MARKER, 0, ret, 0, UTF8_MARKER.length); 152 System.arraycopy(content, 0, ret, UTF8_MARKER.length, content.length); 153 154 return ret; 155 } 156 157 /** 158 * Creates a virtual CmsFile with the individual and shared properties as content.<p> 159 * 160 * For example looks like this:<br/> 161 * Title.i=The title of the resource set as individual property<br/> 162 * Title.s=The title of the resource set as shared property<br/> 163 * 164 * @see #writePropertyFile(CmsObject, String, byte[]) 165 * 166 * @param cms the initialized CmsObject 167 * @param res the resource where to read the properties from 168 * @param path the full path to set for the created property file 169 * 170 * @return the created CmsFile with the individual and shared properties as the content 171 * 172 * @throws CmsException if something goes wrong 173 */ 174 public static CmsFile createPropertyFile(CmsObject cms, CmsResource res, String path) throws CmsException { 175 176 StringBuffer content = new StringBuffer(); 177 178 // header 179 content.append("# Properties for resource "); 180 content.append(res.getRootPath()); 181 content.append("\n"); 182 183 content.append("#\n"); 184 content.append("# ${property_name}.i : individual property\n"); 185 content.append("# ${property_name}.s : shared property\n\n"); 186 187 List<CmsPropertyDefinition> propertyDef = cms.readAllPropertyDefinitions(); 188 Map<String, CmsProperty> activeProperties = CmsProperty.getPropertyMap(cms.readPropertyObjects(res, false)); 189 190 String resourceType = OpenCms.getResourceManager().getResourceType(res).getTypeName(); 191 content.append("resourceType="); 192 content.append(resourceType); 193 content.append("\n\n"); 194 195 // iterate over all possible properties for the resource 196 Iterator<CmsPropertyDefinition> i = propertyDef.iterator(); 197 while (i.hasNext()) { 198 CmsPropertyDefinition currentPropertyDef = i.next(); 199 200 String propName = currentPropertyDef.getName(); 201 CmsProperty currentProperty = activeProperties.get(propName); 202 if (currentProperty == null) { 203 currentProperty = new CmsProperty(); 204 } 205 206 String individualValue = currentProperty.getStructureValue(); 207 String sharedValue = currentProperty.getResourceValue(); 208 209 if (individualValue == null) { 210 individualValue = ""; 211 } 212 213 if (sharedValue == null) { 214 sharedValue = ""; 215 } 216 217 individualValue = escapeString(individualValue); 218 sharedValue = escapeString(sharedValue); 219 220 content.append(propName); 221 content.append(SUFFIX_PROP_INDIVIDUAL); 222 content.append("="); 223 content.append(individualValue); 224 content.append("\n"); 225 226 content.append(propName); 227 content.append(SUFFIX_PROP_SHARED); 228 content.append("="); 229 content.append(sharedValue); 230 content.append("\n\n"); 231 } 232 233 CmsWrappedResource wrap = new CmsWrappedResource(res); 234 wrap.setRootPath(addFileExtension(cms, path, EXTENSION_PROPERTIES)); 235 int plainId = OpenCms.getResourceManager().getResourceType( 236 CmsResourceTypePlain.getStaticTypeName()).getTypeId(); 237 wrap.setTypeId(plainId); 238 wrap.setFolder(false); 239 240 CmsFile ret = wrap.getFile(); 241 try { 242 243 ret.setContents(content.toString().getBytes(CmsEncoder.ENCODING_UTF_8)); 244 } catch (UnsupportedEncodingException e) { 245 // this will never happen since UTF-8 is always supported 246 ret.setContents(content.toString().getBytes()); 247 } 248 249 return ret; 250 } 251 252 /** 253 * Removes an added file extension from the resource name.<p> 254 * 255 * <ul> 256 * <li>If there is only one extension, nothing will be removed.</li> 257 * <li>If there are two extensions, the last one will be removed.</li> 258 * <li>If there are more than two extensions the last one will be removed and 259 * if then the last extension is a number, the extension with the number 260 * will be removed too.</li> 261 * </ul> 262 * 263 * @see #addFileExtension(CmsObject, String, String) 264 * 265 * @param cms the initialized CmsObject 266 * @param resourcename the resource name to remove the file extension from 267 * @param extension the extension to remove 268 * 269 * @return the resource name without the removed file extension 270 */ 271 public static String removeFileExtension(CmsObject cms, String resourcename, String extension) { 272 273 if (resourcename.equals("")) { 274 resourcename = "/"; 275 } 276 277 // get the filename without the path 278 String name = CmsResource.getName(resourcename); 279 280 String[] tokens = name.split("\\."); 281 String suffix = null; 282 283 // check if there is more than one extension 284 if (tokens.length > 2) { 285 286 // check if last extension is "jsp" 287 if (extension.equalsIgnoreCase(tokens[tokens.length - 1])) { 288 289 suffix = "." + extension; 290 291 // check if there is another extension with a numeric index 292 if (tokens.length > 3) { 293 294 try { 295 int index = Integer.valueOf(tokens[tokens.length - 2]).intValue(); 296 297 suffix = "." + index + suffix; 298 } catch (NumberFormatException ex) { 299 // noop 300 } 301 } 302 } 303 } else if (tokens.length == 2) { 304 305 // there is only one extension!! 306 // only remove the last extension, if the resource without the extension exists 307 // and the extension fits 308 if ((cms.existsResource(CmsResource.getFolderPath(resourcename) + tokens[0])) 309 && (extension.equals(tokens[1]))) { 310 suffix = "." + tokens[1]; 311 } 312 } 313 314 if (suffix != null) { 315 316 String path = resourcename.substring(0, resourcename.length() - suffix.length()); 317 return path; 318 } 319 320 return resourcename; 321 } 322 323 /** 324 * Removes the UTF-8 marker from the beginning of the byte array.<p> 325 * 326 * @param content the byte array where to remove the UTF-8 marker 327 * 328 * @return the byte with the removed UTF-8 marker at the beginning 329 */ 330 public static byte[] removeUtf8Marker(byte[] content) { 331 332 if ((content != null) 333 && (content.length >= 3) 334 && (content[0] == UTF8_MARKER[0]) 335 && (content[1] == UTF8_MARKER[1]) 336 && (content[2] == UTF8_MARKER[2])) { 337 338 byte[] ret = new byte[content.length - UTF8_MARKER.length]; 339 System.arraycopy(content, 3, ret, 0, content.length - UTF8_MARKER.length); 340 341 return ret; 342 } 343 344 return content; 345 } 346 347 /** 348 * Takes the content which should be formatted as a property file and set them 349 * as properties to the resource.<p> 350 * 351 * @see #createPropertyFile(CmsObject, CmsResource, String) 352 * 353 * @param cms the initialized CmsObject 354 * @param resourcename the name of the resource where to set the properties 355 * @param content the properties to set (formatted as a property file) 356 * 357 * @throws CmsException if something goes wrong 358 */ 359 public static void writePropertyFile(CmsObject cms, String resourcename, byte[] content) throws CmsException { 360 361 Properties properties = new Properties(); 362 try { 363 String props = CmsEncoder.createString(content, CmsEncoder.ENCODING_UTF_8); 364 props = unescapeString(props); 365 props = CmsEncoder.encodeJavaEntities(props, CmsEncoder.ENCODING_ISO_8859_1); 366 byte[] modContent = props.getBytes(CmsEncoder.ENCODING_ISO_8859_1); 367 368 properties.load(new ByteArrayInputStream(modContent)); 369 370 List<CmsProperty> propList = new ArrayList<CmsProperty>(); 371 Iterator<Map.Entry<Object, Object>> it = properties.entrySet().iterator(); 372 while (it.hasNext()) { 373 Map.Entry<Object, Object> e = it.next(); 374 String key = (String)e.getKey(); 375 String value = (String)e.getValue(); 376 377 if (key.endsWith(SUFFIX_PROP_SHARED)) { 378 propList.add( 379 new CmsProperty(key.substring(0, key.length() - SUFFIX_PROP_SHARED.length()), null, value)); 380 } else if (key.endsWith(SUFFIX_PROP_INDIVIDUAL)) { 381 propList.add( 382 new CmsProperty(key.substring(0, key.length() - SUFFIX_PROP_INDIVIDUAL.length()), value, null)); 383 } 384 } 385 386 cms.writePropertyObjects(resourcename, propList); 387 String newType = properties.getProperty(PROPERTY_RESOURCE_TYPE); 388 if (newType != null) { 389 newType = newType.trim(); 390 if (OpenCms.getResourceManager().hasResourceType(newType)) { 391 I_CmsResourceType newTypeObj = OpenCms.getResourceManager().getResourceType(newType); 392 cms.chtype(resourcename, newTypeObj.getTypeId()); 393 } 394 } 395 } catch (IOException e) { 396 // noop 397 } 398 399 } 400 401 /** 402 * Escapes the value of a property in OpenCms to be displayed 403 * correctly in a property file.<p> 404 * 405 * Mainly handles all escaping sequences that start with a backslash.<p> 406 * 407 * @see #unescapeString(String) 408 * 409 * @param value the value with the string to be escaped 410 * 411 * @return the escaped string 412 */ 413 private static String escapeString(String value) { 414 415 Map<String, String> substitutions = new HashMap<String, String>(); 416 substitutions.put("\n", "\\n"); 417 substitutions.put("\t", "\\t"); 418 substitutions.put("\r", "\\r"); 419 420 return CmsStringUtil.substitute(value, substitutions); 421 } 422 423 /** 424 * Unescapes the value of a property in a property file to 425 * be saved correctly in OpenCms.<p> 426 * 427 * Mainly handles all escaping sequences that start with a backslash.<p> 428 * 429 * @see #escapeString(String) 430 * 431 * @param value the value taken form the property file 432 * 433 * @return the unescaped string value 434 */ 435 private static String unescapeString(String value) { 436 437 Matcher matcher = PATTERN_UNESCAPE.matcher(value); 438 return matcher.replaceAll("\\\\\\\\$1"); 439 } 440 441}