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}