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