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, 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.loader; 029 030import org.opencms.file.CmsObject; 031import org.opencms.file.CmsProject; 032import org.opencms.file.CmsResource; 033import org.opencms.file.CmsResourceFilter; 034import org.opencms.main.CmsException; 035import org.opencms.main.OpenCms; 036import org.opencms.util.CmsFileUtil; 037import org.opencms.util.CmsMacroResolver; 038import org.opencms.util.CmsStringUtil; 039import org.opencms.util.I_CmsMacroResolver; 040import org.opencms.util.PrintfFormat; 041import org.opencms.workplace.CmsWorkplace; 042import org.opencms.xml.content.CmsNumberSuffixNameSequence; 043 044import java.util.ArrayList; 045import java.util.HashSet; 046import java.util.Iterator; 047import java.util.List; 048import java.util.Set; 049 050import org.apache.commons.collections.Factory; 051 052/** 053 * The default class used for generating file names either for the <code>urlName</code> mapping 054 * or when using a "new" operation in the context of the direct edit interface.<p> 055 * 056 * @since 8.0.0 057 */ 058public class CmsDefaultFileNameGenerator implements I_CmsFileNameGenerator { 059 060 /** 061 * Factory to use for resolving the %(number) macro.<p> 062 */ 063 public class CmsNumberFactory implements Factory { 064 065 /** The actual number. */ 066 protected int m_number; 067 068 /** Format for file create parameter. */ 069 protected PrintfFormat m_numberFormat; 070 071 /** 072 * Create a new number factory.<p> 073 * 074 * @param digits the number of digits to use 075 */ 076 public CmsNumberFactory(int digits) { 077 078 m_numberFormat = new PrintfFormat("%0." + digits + "d"); 079 m_number = 0; 080 } 081 082 /** 083 * Create the number based on the number of digits set.<p> 084 * 085 * @see org.apache.commons.collections.Factory#create() 086 */ 087 public Object create() { 088 089 // formats the number with the amount of digits selected 090 return m_numberFormat.sprintf(m_number); 091 } 092 093 /** 094 * Sets the number to create to the given value.<p> 095 * 096 * @param number the number to set 097 */ 098 public void setNumber(int number) { 099 100 m_number = number; 101 } 102 } 103 104 /** Start sequence for macro with digits. */ 105 private static final String MACRO_NUMBER_DIGIT_SEPARATOR = ":"; 106 107 /** The copy file name insert. */ 108 public static final String COPY_FILE_NAME_INSERT = "-copy"; 109 110 /** 111 * Checks the given pattern for the number macro.<p> 112 * 113 * @param pattern the pattern to check 114 * 115 * @return <code>true</code> if the pattern contains the macro 116 */ 117 public static boolean hasNumberMacro(String pattern) { 118 119 // check both macro variants 120 return hasNumberMacro( 121 pattern, 122 "" + I_CmsMacroResolver.MACRO_DELIMITER + I_CmsMacroResolver.MACRO_START, 123 "" + I_CmsMacroResolver.MACRO_END) 124 || hasNumberMacro( 125 pattern, 126 "" + I_CmsMacroResolver.MACRO_DELIMITER_OLD + I_CmsMacroResolver.MACRO_START_OLD, 127 "" + I_CmsMacroResolver.MACRO_END_OLD); 128 } 129 130 /** 131 * Removes the file extension if it only consists of letters.<p> 132 * 133 * @param path the path from which to remove the file extension 134 * 135 * @return the path without the file extension 136 */ 137 public static String removeExtension(String path) { 138 139 return path.replaceFirst("\\.[a-zA-Z]*$", ""); 140 } 141 142 /** 143 * Checks the given pattern for the number macro.<p> 144 * 145 * @param pattern the pattern to check 146 * @param macroStart the macro start string 147 * @param macroEnd the macro end string 148 * 149 * @return <code>true</code> if the pattern contains the macro 150 */ 151 private static boolean hasNumberMacro(String pattern, String macroStart, String macroEnd) { 152 153 String macro = I_CmsFileNameGenerator.MACRO_NUMBER; 154 String macroPart = macroStart + macro + MACRO_NUMBER_DIGIT_SEPARATOR; 155 int prefixIndex = pattern.indexOf(macroPart); 156 if (prefixIndex >= 0) { 157 // this macro contains an individual digit setting 158 char n = pattern.charAt(prefixIndex + macroPart.length()); 159 macro = macro + MACRO_NUMBER_DIGIT_SEPARATOR + n; 160 } 161 return pattern.contains(macroStart + macro + macroEnd); 162 } 163 164 /** 165 * @see org.opencms.loader.I_CmsFileNameGenerator#getCopyFileName(org.opencms.file.CmsObject, java.lang.String, java.lang.String) 166 */ 167 public String getCopyFileName(CmsObject cms, String parentFolder, String baseName) { 168 169 String name = baseName; 170 int dot = name.lastIndexOf("."); 171 if (dot > 0) { 172 if (!name.substring(0, dot).endsWith(COPY_FILE_NAME_INSERT)) { 173 name = name.substring(0, dot) + COPY_FILE_NAME_INSERT + name.substring(dot); 174 } 175 } else { 176 if (!name.endsWith(COPY_FILE_NAME_INSERT)) { 177 name += COPY_FILE_NAME_INSERT; 178 } 179 } 180 return getUniqueFileName(cms, parentFolder, name); 181 } 182 183 /** 184 * @see org.opencms.loader.I_CmsFileNameGenerator#getNewFileName(org.opencms.file.CmsObject, java.lang.String, int) 185 */ 186 public String getNewFileName(CmsObject cms, String namePattern, int defaultDigits) throws CmsException { 187 188 return getNewFileName(cms, namePattern, defaultDigits, false); 189 } 190 191 /** 192 * Returns a new resource name based on the provided OpenCms user context and name pattern.<p> 193 * 194 * The pattern in this default implementation must be a path which may contain the macro <code>%(number)</code>. 195 * This will be replaced by the first "n" digit sequence for which the resulting file name is not already 196 * used. For example the pattern <code>"/file_%(number).xml"</code> would result in something like <code>"/file_00003.xml"</code>.<p> 197 * 198 * Alternatively, the macro can have the form <code>%(number:n)</code> with <code>n = {1...9}</code>, for example <code>%(number:6)</code>. 199 * In this case the default digits will be ignored and instead the digits provided as "n" will be used.<p> 200 * 201 * @param cms the current OpenCms user context 202 * @param namePattern the pattern to be used when generating the new resource name 203 * @param defaultDigits the default number of digits to use for numbering the created file names 204 * @param explorerMode if true, the file name is first tried without a numeric macro, also underscores are inserted automatically before the number macro and don't need to be part of the name pattern 205 * 206 * @return a new resource name based on the provided OpenCms user context and name pattern 207 * 208 * @throws CmsException in case something goes wrong 209 */ 210 public String getNewFileName(CmsObject cms, String namePattern, int defaultDigits, boolean explorerMode) 211 throws CmsException { 212 213 String checkPattern = cms.getRequestContext().removeSiteRoot(namePattern); 214 String folderName = CmsResource.getFolderPath(checkPattern); 215 216 // must check ALL resources in folder because name doesn't care for type 217 List<CmsResource> resources = cms.readResources(folderName, CmsResourceFilter.ALL, false); 218 219 // now create a list of all the file names 220 List<String> fileNames = new ArrayList<String>(resources.size()); 221 for (CmsResource res : resources) { 222 fileNames.add(cms.getSitePath(res)); 223 } 224 225 return getNewFileNameFromList(fileNames, checkPattern, defaultDigits, explorerMode); 226 } 227 228 /** 229 * @see org.opencms.loader.I_CmsFileNameGenerator#getUniqueFileName(org.opencms.file.CmsObject, java.lang.String, java.lang.String) 230 */ 231 public String getUniqueFileName(CmsObject cms, String parentFolder, String baseName) { 232 233 Iterator<String> nameIterator = getUrlNameSequence(baseName); 234 String result = nameIterator.next(); 235 CmsObject onlineCms = null; 236 237 try { 238 onlineCms = OpenCms.initCmsObject(cms); 239 onlineCms.getRequestContext().setCurrentProject(cms.readProject(CmsProject.ONLINE_PROJECT_ID)); 240 } catch (CmsException e) { 241 // should not happen, nothing to do 242 } 243 String path = CmsStringUtil.joinPaths(parentFolder, result); 244 // use CmsResourceFilter.ALL because we also want to skip over deleted resources 245 while (cms.existsResource(path, CmsResourceFilter.ALL) 246 || ((onlineCms != null) && onlineCms.existsResource(path, CmsResourceFilter.ALL))) { 247 result = nameIterator.next(); 248 path = CmsStringUtil.joinPaths(parentFolder, result); 249 } 250 return result; 251 } 252 253 /** 254 * This default implementation will just generate a 5 digit sequence that is appended to the resource name in case 255 * of a collision of names.<p> 256 * 257 * @see org.opencms.loader.I_CmsFileNameGenerator#getUrlNameSequence(java.lang.String) 258 */ 259 public Iterator<String> getUrlNameSequence(String baseName) { 260 261 String translatedTitle = OpenCms.getResourceManager().getFileTranslator().translateResource(baseName).replace( 262 "/", 263 "-"); 264 return new CmsNumberSuffixNameSequence(translatedTitle); 265 } 266 267 /** 268 * Internal method for file name generation, decoupled for testing.<p> 269 * 270 * @param fileNames the list of file names already existing in the folder 271 * @param checkPattern the pattern to be used when generating the new resource name 272 * @param defaultDigits the default number of digits to use for numbering the created file names 273 * @param explorerMode if true, first the file name without a number is tried, and also an underscore is automatically inserted before the number macro 274 * 275 * @return a new resource name based on the provided OpenCms user context and name pattern 276 */ 277 protected String getNewFileNameFromList( 278 List<String> fileNames, 279 String checkPattern, 280 int defaultDigits, 281 final boolean explorerMode) { 282 283 if (!hasNumberMacro(checkPattern)) { 284 throw new IllegalArgumentException( 285 Messages.get().getBundle().key(Messages.ERR_FILE_NAME_PATTERN_WITHOUT_NUMBER_MACRO_1, checkPattern)); 286 } 287 288 String checkFileName, checkTempFileName; 289 CmsMacroResolver resolver = CmsMacroResolver.newInstance(); 290 Set<String> extensionlessNames = new HashSet<String>(); 291 for (String name : fileNames) { 292 if (name.length() > 1) { 293 name = CmsFileUtil.removeTrailingSeparator(name); 294 } 295 extensionlessNames.add(removeExtension(name)); 296 } 297 298 String macro = I_CmsFileNameGenerator.MACRO_NUMBER; 299 int useDigits = defaultDigits; 300 String macroStart = "" 301 + I_CmsMacroResolver.MACRO_DELIMITER 302 + I_CmsMacroResolver.MACRO_START 303 + macro 304 + MACRO_NUMBER_DIGIT_SEPARATOR; 305 int prefixIndex = checkPattern.indexOf(macroStart); 306 if (prefixIndex < 0) { 307 macroStart = "" 308 + I_CmsMacroResolver.MACRO_DELIMITER_OLD 309 + I_CmsMacroResolver.MACRO_START_OLD 310 + macro 311 + MACRO_NUMBER_DIGIT_SEPARATOR; 312 prefixIndex = checkPattern.indexOf(macroStart); 313 } 314 if (prefixIndex >= 0) { 315 // this macro contains an individual digit setting 316 char n = checkPattern.charAt(prefixIndex + macroStart.length()); 317 macro = macro + ':' + n; 318 useDigits = Character.getNumericValue(n); 319 } 320 321 CmsNumberFactory numberFactory = new CmsNumberFactory(useDigits) { 322 323 @Override 324 public Object create() { 325 326 if (explorerMode) { 327 if (m_number == 1) { 328 return ""; 329 } else { 330 return "_" + m_numberFormat.sprintf(m_number - 1); 331 } 332 } else { 333 return super.create(); 334 } 335 } 336 337 }; 338 resolver.addDynamicMacro(macro, numberFactory); 339 Set<String> checked = new HashSet<String>(); 340 int j = 0; 341 do { 342 numberFactory.setNumber(++j); 343 // resolve macros in file name 344 checkFileName = resolver.resolveMacros(checkPattern); 345 if (checked.contains(checkFileName)) { 346 // the file name has been checked before, abort the search 347 throw new RuntimeException( 348 Messages.get().getBundle().key(Messages.ERR_NO_FILE_NAME_AVAILABLE_FOR_PATTERN_1, checkPattern)); 349 } 350 checked.add(checkFileName); 351 // get name of the resolved temp file 352 checkTempFileName = CmsWorkplace.getTemporaryFileName(checkFileName); 353 } while (extensionlessNames.contains(removeExtension(checkFileName)) 354 || extensionlessNames.contains(removeExtension(checkTempFileName))); 355 356 return checkFileName; 357 } 358 359}