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.util;
029
030import org.opencms.db.CmsUserSettings;
031import org.opencms.file.CmsFile;
032import org.opencms.file.CmsObject;
033import org.opencms.file.CmsProperty;
034import org.opencms.file.CmsPropertyDefinition;
035import org.opencms.file.CmsResource;
036import org.opencms.file.CmsResourceFilter;
037import org.opencms.file.types.CmsResourceTypeBinary;
038import org.opencms.file.types.CmsResourceTypeImage;
039import org.opencms.flex.CmsFlexController;
040import org.opencms.i18n.CmsMessageContainer;
041import org.opencms.i18n.CmsMessages;
042import org.opencms.i18n.CmsMultiMessages;
043import org.opencms.main.CmsException;
044import org.opencms.main.CmsIllegalArgumentException;
045import org.opencms.main.CmsLog;
046import org.opencms.main.OpenCms;
047import org.opencms.report.I_CmsReport;
048import org.opencms.security.CmsOrganizationalUnit;
049import org.opencms.ui.apps.sitemanager.CmsSiteManager;
050import org.opencms.ui.editors.messagebundle.CmsMessageBundleEditorTypes.Descriptor;
051import org.opencms.xml.CmsXmlException;
052import org.opencms.xml.content.CmsXmlContent;
053import org.opencms.xml.content.CmsXmlContentFactory;
054import org.opencms.xml.content.CmsXmlContentValueSequence;
055
056import java.io.UnsupportedEncodingException;
057import java.nio.charset.Charset;
058import java.util.Arrays;
059import java.util.Collections;
060import java.util.HashMap;
061import java.util.Iterator;
062import java.util.LinkedHashMap;
063import java.util.List;
064import java.util.Map;
065import java.util.Properties;
066
067import javax.servlet.jsp.PageContext;
068
069import org.apache.commons.collections.Factory;
070import org.apache.commons.logging.Log;
071
072import com.google.common.base.Function;
073
074/**
075 * Resolves macros in the form of <code>%(key)</code> or <code>${key}</code> in an input String.<p>
076 *
077 * Starting with OpenCms 7.0, the preferred form of a macro is <code>%(key)</code>. This is to
078 * avoid conflicts / confusion with the JSP EL, which also uses the <code>${key}</code> syntax.<p>
079 *
080 * The macro names that can be resolved depend of the context objects provided to the resolver
081 * using the <code>set...</code> methods.<p>
082 *
083 * @since 6.0.0
084 */
085public class CmsMacroResolver implements I_CmsMacroResolver {
086
087    /** The prefix indicating that the key represents an OpenCms runtime attribute. */
088    public static final String KEY_ATTRIBUTE = "attribute.";
089
090    /** Key used to specify the context path as macro value. */
091    public static final String KEY_CONTEXT_PATH = "contextPath";
092
093    /** Key used to specify the description of the current organizational unit as macro value. */
094    public static final String KEY_CURRENT_ORGUNIT_DESCRIPTION = "currentou.description";
095
096    /** Key used to specify the full qualified name of the current organizational unit as macro value. */
097    public static final String KEY_CURRENT_ORGUNIT_FQN = "currentou.fqn";
098
099    /** Key used to specify the current time as macro value. */
100    public static final String KEY_CURRENT_TIME = "currenttime";
101
102    /** Key used to specify the city of the current user as macro value. */
103    public static final String KEY_CURRENT_USER_CITY = "currentuser.city";
104
105    /** Key used to specify the country of the current user as macro value. */
106    public static final String KEY_CURRENT_USER_COUNTRY = "currentuser.country";
107
108    /** Key used to specify the display name of the current user as macro value. */
109    public static final String KEY_CURRENT_USER_DISPLAYNAME = "currentuser.displayname";
110
111    /** Key used to specify the email address of the current user as macro value. */
112    public static final String KEY_CURRENT_USER_EMAIL = "currentuser.email";
113
114    /** Key used to specify the first name of the current user as macro value. */
115    public static final String KEY_CURRENT_USER_FIRSTNAME = "currentuser.firstname";
116
117    /** Key used to specify the full name of the current user as macro value. */
118    public static final String KEY_CURRENT_USER_FULLNAME = "currentuser.fullname";
119
120    /** Key used to specify the institution of the current user as macro value. */
121    public static final String KEY_CURRENT_USER_INSTITUTION = "currentuser.institution";
122
123    /** Key used to specify the last login date of the current user as macro value. */
124    public static final String KEY_CURRENT_USER_LASTLOGIN = "currentuser.lastlogin";
125
126    /** Key used to specify the last name of the current user as macro value. */
127    public static final String KEY_CURRENT_USER_LASTNAME = "currentuser.lastname";
128
129    /** Key used to specify the user name of the current user as macro value. */
130    public static final String KEY_CURRENT_USER_NAME = "currentuser.name";
131
132    /** Key used to specify the street of the current user as macro value. */
133    public static final String KEY_CURRENT_USER_STREET = "currentuser.street";
134
135    /** Key used to specify the zip code of the current user as macro value. */
136    public static final String KEY_CURRENT_USER_ZIP = "currentuser.zip";
137
138    /** Key prefix used to specify the value of a localized key as macro value. */
139    public static final String KEY_LOCALIZED_PREFIX = "key.";
140
141    /** Identifier for "magic" parameter names. */
142    public static final String KEY_OPENCMS = "opencms.";
143
144    /** The prefix indicating that the key represents a page context object. */
145    public static final String KEY_PAGE_CONTEXT = "pageContext.";
146
147    /** Key used to specify the project id as macro value. */
148    public static final String KEY_PROJECT_ID = "projectid";
149
150    /** The prefix indicating that the key represents a property to be read on the current request URI. */
151    public static final String KEY_PROPERTY = "property.";
152
153    /** The prefix indicating that the key represents a property to be read on the current element. */
154    public static final String KEY_PROPERTY_ELEMENT = "elementProperty.";
155
156    /** Key used to specify a random id as macro value. */
157    public static final String KEY_RANDOM_ID = "randomId";
158
159    /** Key used to specify the request encoding as macro value. */
160    public static final String KEY_REQUEST_ENCODING = "request.encoding";
161
162    /** Key used to specify the folder of the request URI as macro value. */
163    public static final String KEY_REQUEST_FOLDER = "request.folder";
164
165    /** Key user to specify the request locale as macro value. */
166    public static final String KEY_REQUEST_LOCALE = "request.locale";
167
168    /** The prefix indicating that the key represents a HTTP request parameter. */
169    public static final String KEY_REQUEST_PARAM = "param.";
170
171    /** Key used to specify the request site root as macro value. */
172    public static final String KEY_REQUEST_SITEROOT = "request.siteroot";
173
174    /** Key used to specify the request uri as macro value. */
175    public static final String KEY_REQUEST_URI = "request.uri";
176
177    /** Key used to specify the validation path as macro value. */
178    public static final String KEY_VALIDATION_PATH = "validation.path";
179
180    /** Key used to specify the validation regex as macro value. */
181    public static final String KEY_VALIDATION_REGEX = "validation.regex";
182
183    /** Key used to specify the validation value as macro value. */
184    public static final String KEY_VALIDATION_VALUE = "validation.value";
185
186    /** Identified for "magic" parameter commands. */
187    static final String[] VALUE_NAMES_ARRAY = {
188        "uri", // 0
189        "filename", // 1
190        "folder", // 2
191        "default.encoding", // 3
192        "remoteaddress", // 4
193        "webapp", // 5
194        "webbasepath", // 6
195        "version", // 7
196        "versionid" // 8
197    };
198
199    /** The "magic" commands wrapped in a List. */
200    public static final List<String> VALUE_NAMES = Collections.unmodifiableList(Arrays.asList(VALUE_NAMES_ARRAY));
201
202    /** The log object for this class. */
203    private static final Log LOG = CmsLog.getLog(CmsMacroResolver.class);
204
205    /** A map of additional values provided by the calling class. */
206    protected Map<String, String> m_additionalMacros;
207
208    /** The OpenCms user context to use for resolving macros. */
209    protected CmsObject m_cms;
210
211    /** The JSP's page context to use for resolving macros. */
212    protected PageContext m_jspPageContext;
213
214    /** Indicates if unresolved macros should be kept "as is" or replaced by an empty String. */
215    protected boolean m_keepEmptyMacros;
216
217    /** The messages resource bundle to resolve localized keys with. */
218    protected CmsMessages m_messages;
219
220    /** The request parameter map, used for better compatibility with multi part requests. */
221    protected Map<String, String[]> m_parameterMap;
222
223    /** The resource name to use for resolving macros. */
224    protected String m_resourceName;
225
226    /** A map from names of dynamic macros to the factories which generate their values. */
227    private Map<String, Factory> m_factories;
228
229    /**
230     * Copies resources, adjust internal links (if adjustLinks==true) and resolves macros (if keyValue map is set).<p>
231     *
232     * @param cms CmsObject
233     * @param source path
234     * @param destination path
235     * @param keyValue map to be used for macro resolver
236     * @param adjustLinks boolean, true means internal links get adjusted.
237     * @throws CmsException exception
238     */
239    public static void copyAndResolveMacro(
240        CmsObject cms,
241        String source,
242        String destination,
243        Map<String, String> keyValue,
244        boolean adjustLinks)
245    throws CmsException {
246
247        copyAndResolveMacro(cms, source, destination, keyValue, adjustLinks, CmsResource.COPY_AS_NEW);
248    }
249
250    /**
251     * Copies resources, adjust internal links (if adjustLinks==true) and resolves macros (if keyValue map is set).<p>
252     *
253     * @param cms CmsObject
254     * @param source path
255     * @param destination path
256     * @param keyValue map to be used for macro resolver
257     * @param adjustLinks boolean, true means internal links get adjusted.
258     * @param copyMode copyMode
259     * @throws CmsException exception
260     */
261
262    public static void copyAndResolveMacro(
263        CmsObject cms,
264        String source,
265        String destination,
266        Map<String, String> keyValue,
267        boolean adjustLinks,
268        CmsResource.CmsResourceCopyMode copyMode)
269    throws CmsException {
270
271        copyAndResolveMacro(cms, source, destination, keyValue, adjustLinks, copyMode, null);
272
273    }
274
275    /**
276     * Copies resources, adjust internal links (if adjustLinks==true) and resolves macros (if keyValue map is set).<p>
277     *
278     * @param cms CmsObject
279     * @param source path
280     * @param destination path
281     * @param keyValue map to be used for macro resolver
282     * @param adjustLinks boolean, true means internal links get adjusted.
283     * @param copyMode copy Mode
284     * @param report report to write logs to
285     * @throws CmsException exception
286     */
287    public static void copyAndResolveMacro(
288        CmsObject cms,
289        String source,
290        String destination,
291        Map<String, String> keyValue,
292        boolean adjustLinks,
293        CmsResource.CmsResourceCopyMode copyMode,
294        I_CmsReport report)
295    throws CmsException {
296
297        if (report != null) {
298            report.println(
299                org.opencms.ui.apps.Messages.get().container(
300                    org.opencms.ui.apps.Messages.RPT_MACRORESOLVER_COPY_RESOURCES_1,
301                    source));
302        }
303        cms.copyResource(source, destination, copyMode);
304        if (report != null) {
305            report.println(
306                org.opencms.ui.apps.Messages.get().container(
307                    org.opencms.ui.apps.Messages.RPT_MACRORESOLVER_LINK_ADJUST_0));
308        }
309        if (adjustLinks) {
310            cms.adjustLinks(source, destination);
311        }
312        //Guards to check if keyValue is set correctly, otherwise no adjustment is done
313        if (keyValue == null) {
314            return;
315        }
316        if (keyValue.isEmpty()) {
317            return;
318        }
319
320        if (report != null) {
321            report.println(
322                org.opencms.ui.apps.Messages.get().container(
323                    org.opencms.ui.apps.Messages.RPT_MACRORESOLVER_APPLY_MACROS_0));
324        }
325
326        CmsMacroResolver macroResolver = new CmsMacroResolver();
327        macroResolver.setKeepEmptyMacros(true);
328        for (String key : keyValue.keySet()) {
329
330            macroResolver.addMacro(key, keyValue.get(key));
331        }
332
333        //Collect all resources to loop over
334        List<CmsResource> resoucesToCopy = cms.readResources(destination, CmsResourceFilter.ALL, true);
335        for (CmsResource resource : resoucesToCopy) {
336            if (resource.isFile()
337                && (resource.getTypeId() != CmsResourceTypeBinary.getStaticTypeId())
338                && (resource.getTypeId() != CmsResourceTypeImage.getStaticTypeId())) {
339                CmsFile file = cms.readFile(resource);
340                CmsMacroResolver.updateFile(cms, file, macroResolver);
341            }
342            CmsMacroResolver.updateProperties(cms, resource, macroResolver);
343        }
344
345        // apply macro to the folder itself
346        CmsResource resource = cms.readResource(destination, CmsResourceFilter.ALL);
347
348        CmsMacroResolver.updateProperties(cms, resource, macroResolver);
349
350        if (cms.existsResource(ensureFoldername(destination) + CmsSiteManager.MACRO_FOLDER)) {
351            cms.deleteResource(
352                ensureFoldername(destination) + CmsSiteManager.MACRO_FOLDER,
353                CmsResource.CmsResourceDeleteMode.valueOf(-1));
354        }
355
356    }
357
358    /**
359     * Adds macro delimiters to the given input,
360     * for example <code>key</code> becomes <code>%(key)</code>.<p>
361     *
362     * @param input the input to format as a macro
363     *
364     * @return the input formatted as a macro
365     */
366    public static String formatMacro(String input) {
367
368        StringBuffer result = new StringBuffer(input.length() + 4);
369        result.append(I_CmsMacroResolver.MACRO_DELIMITER);
370        result.append(I_CmsMacroResolver.MACRO_START);
371        result.append(input);
372        result.append(I_CmsMacroResolver.MACRO_END);
373        return result.toString();
374    }
375
376    /**
377     * Reads a bundle (key, value, descriptor) from Descriptor Resource and property resource.<p>
378     *
379     * @param resourceBundle property resource
380     * @param descriptor resource
381     * @param clonedCms cms instance
382     * @return Map <key, [value, descriptor]>
383     * @throws CmsXmlException exception
384     * @throws CmsException exception
385     */
386    public static Map<String, String[]> getBundleMapFromResources(
387        Properties resourceBundle,
388        CmsResource descriptor,
389        CmsObject clonedCms)
390    throws CmsXmlException, CmsException {
391
392        Map<String, String[]> ret = new LinkedHashMap<String, String[]>();
393
394        //Read XML content of descriptor
395        CmsXmlContent xmlContentDesc = CmsXmlContentFactory.unmarshal(clonedCms, clonedCms.readFile(descriptor));
396        CmsXmlContentValueSequence messages = xmlContentDesc.getValueSequence(Descriptor.N_MESSAGE, Descriptor.LOCALE);
397
398        //Iterate through content
399        for (int i = 0; i < messages.getElementCount(); i++) {
400
401            //Read key and default text from descriptor, label from bundle (localized)
402            String prefix = messages.getValue(i).getPath() + "/";
403            String key = xmlContentDesc.getValue(prefix + Descriptor.N_KEY, Descriptor.LOCALE).getStringValue(
404                clonedCms);
405            String label = resourceBundle.getProperty(key);
406            String defaultText = xmlContentDesc.getValue(
407                prefix + Descriptor.N_DESCRIPTION,
408                Descriptor.LOCALE).getStringValue(clonedCms);
409
410            ret.put(key, new String[] {label, defaultText});
411        }
412        return ret;
413    }
414
415    /**
416     * Returns <code>true</code> if the given input String if formatted like a macro,
417     * that is it starts with <code>{@link I_CmsMacroResolver#MACRO_DELIMITER_OLD} +
418     * {@link I_CmsMacroResolver#MACRO_START_OLD}</code> and ends with
419     * <code>{@link I_CmsMacroResolver#MACRO_END_OLD}</code>.<p>
420     *
421     * @param input the input to check for a macro
422     * @return <code>true</code> if the given input String if formatted like a macro
423     */
424    public static boolean isMacro(String input) {
425
426        if (CmsStringUtil.isEmpty(input) || (input.length() < 3)) {
427            return false;
428        }
429
430        return (((input.charAt(0) == I_CmsMacroResolver.MACRO_DELIMITER_OLD)
431            && ((input.charAt(1) == I_CmsMacroResolver.MACRO_START_OLD)
432                && (input.charAt(input.length() - 1) == I_CmsMacroResolver.MACRO_END_OLD)))
433            || ((input.charAt(0) == I_CmsMacroResolver.MACRO_DELIMITER)
434                && ((input.charAt(1) == I_CmsMacroResolver.MACRO_START)
435                    && (input.charAt(input.length() - 1) == I_CmsMacroResolver.MACRO_END))));
436    }
437
438    /**
439     * Returns <code>true</code> if the given input String is a macro equal to the given macro name.<p>
440     *
441     * @param input the input to check for a macro
442     * @param macroName the macro name to check for
443     *
444     * @return <code>true</code> if the given input String is a macro equal to the given macro name
445     */
446    public static boolean isMacro(String input, String macroName) {
447
448        if (isMacro(input)) {
449            return input.substring(2, input.length() - 1).equals(macroName);
450        }
451        return false;
452    }
453
454    /**
455     * Returns a macro for the given localization key with the given parameters.<p>
456     *
457     * @param keyName the name of the localized key
458     * @param params the optional parameter array
459     *
460     * @return a macro for the given localization key with the given parameters
461     */
462    public static String localizedKeyMacro(String keyName, Object[] params) {
463
464        String parameters = "";
465        if ((params != null) && (params.length > 0)) {
466            for (int i = 0; i < params.length; i++) {
467                if (params[i] != null) {
468                    parameters += "|" + params[i].toString();
469                }
470            }
471        }
472        return ""
473            + I_CmsMacroResolver.MACRO_DELIMITER
474            + I_CmsMacroResolver.MACRO_START
475            + CmsMacroResolver.KEY_LOCALIZED_PREFIX
476            + keyName
477            + parameters
478            + I_CmsMacroResolver.MACRO_END;
479    }
480
481    /**
482     * Factory method to create a new {@link CmsMacroResolver} instance.<p>
483     *
484     * @return a new instance of a {@link CmsMacroResolver}
485     */
486    public static CmsMacroResolver newInstance() {
487
488        return new CmsMacroResolver();
489    }
490
491    /** Returns a new macro resolver that loads message keys from the workplace bundle in the user setting's language.
492     * @param cms the CmsObject.
493     * @return a new macro resolver with messages from the workplace bundle in the current users locale.
494     */
495    public static I_CmsMacroResolver newWorkplaceLocaleResolver(final CmsObject cms) {
496
497        // Resolve macros in the property configuration
498        CmsMacroResolver resolver = new CmsMacroResolver();
499        resolver.setCmsObject(cms);
500        CmsUserSettings userSettings = new CmsUserSettings(cms.getRequestContext().getCurrentUser());
501        CmsMultiMessages multimessages = new CmsMultiMessages(userSettings.getLocale());
502        multimessages.addMessages(OpenCms.getWorkplaceManager().getMessages(userSettings.getLocale()));
503        resolver.setMessages(multimessages);
504        resolver.setKeepEmptyMacros(true);
505
506        return resolver;
507    }
508
509    /**
510     * Resolves the macros in the given input using the provided parameters.<p>
511     *
512     * A macro in the form <code>%(key)</code> or <code>${key}</code> in the content is replaced with it's assigned value
513     * returned by the <code>{@link I_CmsMacroResolver#getMacroValue(String)}</code> method of the given
514     * <code>{@link I_CmsMacroResolver}</code> instance.<p>
515     *
516     * If a macro is found that can not be mapped to a value by the given macro resolver,
517     * it is left untouched in the input.<p>
518     *
519     * @param input the input in which to resolve the macros
520     * @param cms the OpenCms user context to use when resolving macros
521     * @param messages the message resource bundle to use when resolving macros
522     *
523     * @return the input with the macros resolved
524     */
525    public static String resolveMacros(String input, CmsObject cms, CmsMessages messages) {
526
527        CmsMacroResolver resolver = new CmsMacroResolver();
528        resolver.m_cms = cms;
529        resolver.m_messages = messages;
530        resolver.m_keepEmptyMacros = true;
531        return resolver.resolveMacros(input);
532    }
533
534    /**
535     * Resolves macros in the provided input String using the given macro resolver.<p>
536     *
537     * A macro in the form <code>%(key)</code> or <code>${key}</code> in the content is replaced with it's assigned value
538     * returned by the <code>{@link I_CmsMacroResolver#getMacroValue(String)}</code> method of the given
539     * <code>{@link I_CmsMacroResolver}</code> instance.<p>
540     *
541     * If a macro is found that can not be mapped to a value by the given macro resolver,
542     * <code>{@link I_CmsMacroResolver#isKeepEmptyMacros()}</code> controls if the macro is replaced by
543     * an empty String, or is left untouched in the input.<p>
544     *
545     * @param input the input in which to resolve the macros
546     * @param resolver the macro resolver to use
547     *
548     * @return the input with all macros resolved
549     */
550    public static String resolveMacros(final String input, I_CmsMacroResolver resolver) {
551
552        if ((input == null) || (input.length() < 3)) {
553            // macro must have at last 3 chars "${}" or "%()"
554            return input;
555        }
556
557        int pn = input.indexOf(I_CmsMacroResolver.MACRO_DELIMITER);
558        int po = input.indexOf(I_CmsMacroResolver.MACRO_DELIMITER_OLD);
559
560        if ((po == -1) && (pn == -1)) {
561            // no macro delimiter found in input
562            return input;
563        }
564
565        int len = input.length();
566        StringBuffer result = new StringBuffer(len << 1);
567        int np, pp1, pp2, e;
568        String macro, value;
569        boolean keep = resolver.isKeepEmptyMacros();
570        boolean resolvedNone = true;
571        char ds, de;
572        int p;
573
574        if ((po == -1) || ((pn > -1) && (pn < po))) {
575            p = pn;
576            ds = I_CmsMacroResolver.MACRO_START;
577            de = I_CmsMacroResolver.MACRO_END;
578        } else {
579            p = po;
580            ds = I_CmsMacroResolver.MACRO_START_OLD;
581            de = I_CmsMacroResolver.MACRO_END_OLD;
582        }
583
584        // append chars before the first delimiter found
585        result.append(input.substring(0, p));
586        do {
587            pp1 = p + 1;
588            pp2 = pp1 + 1;
589            if (pp2 >= len) {
590                // remaining chars can't be a macro (minimum size is 3)
591                result.append(input.substring(p, len));
592                break;
593            }
594            // get the next macro delimiter
595            if ((pn > -1) && (pn < pp1)) {
596                pn = input.indexOf(I_CmsMacroResolver.MACRO_DELIMITER, pp1);
597            }
598            if ((po > -1) && (po < pp1)) {
599                po = input.indexOf(I_CmsMacroResolver.MACRO_DELIMITER_OLD, pp1);
600            }
601            if ((po == -1) && (pn == -1)) {
602                // none found, make sure remaining chars in this segment are appended
603                np = len;
604            } else {
605                // check if the next delimiter is old or new style
606                if ((po == -1) || ((pn > -1) && (pn < po))) {
607                    np = pn;
608                } else {
609                    np = po;
610                }
611            }
612            // check if the next char is a "macro start"
613            char st = input.charAt(pp1);
614            if (st == ds) {
615                // we have a starting macro sequence "${" or "%(", now check if this segment contains a "}" or ")"
616                e = input.indexOf(de, p);
617                if ((e > 0) && (e < np)) {
618                    // this segment contains a closing macro delimiter "}" or "]", so we may have found a macro
619                    macro = input.substring(pp2, e);
620                    // resolve macro
621                    value = resolver.getMacroValue(macro);
622                    e++;
623                    if (value != null) {
624                        // macro was successfully resolved
625                        result.append(value);
626                        resolvedNone = false;
627                    } else if (keep) {
628                        // macro was unknown, but should be kept
629                        result.append(input.substring(p, e));
630                    }
631                } else {
632                    // no complete macro "${...}" or "%(...)" in this segment
633                    e = p;
634                }
635            } else {
636                // no macro start char after the "$" or "%"
637                e = p;
638            }
639            // set macro style for next delimiter found
640            if (np == pn) {
641                ds = I_CmsMacroResolver.MACRO_START;
642                de = I_CmsMacroResolver.MACRO_END;
643            } else {
644                ds = I_CmsMacroResolver.MACRO_START_OLD;
645                de = I_CmsMacroResolver.MACRO_END_OLD;
646            }
647            // append the remaining chars after the macro to the start of the next macro
648            result.append(input.substring(e, np));
649            // this is a nerdy joke ;-)
650            p = np;
651        } while (p < len);
652
653        if (resolvedNone && keep) {
654            // nothing was resolved and macros should be kept, return original input
655            return input;
656        }
657
658        // input was changed during resolving of macros
659        return result.toString();
660    }
661
662    /**
663     * Strips the macro delimiters from the given input,
664     * for example <code>%(key)</code> or <code>${key}</code> becomes <code>key</code>.<p>
665     *
666     * In case the input is not a macro, <code>null</code> is returned.<p>
667     *
668     * @param input the input to strip
669     *
670     * @return the macro stripped from the input, or <code>null</code>
671     */
672    public static String stripMacro(String input) {
673
674        if (isMacro(input)) {
675            return input.substring(2, input.length() - 1);
676        }
677        return null;
678    }
679
680    /**
681     * Checks if there are at least one character in the folder name,
682     * also ensures that it ends with a '/' and doesn't start with '/'.<p>
683     *
684     * @param resourcename folder name to check (complete path)
685     * @return the validated folder name
686     * @throws CmsIllegalArgumentException if the folder name is empty or <code>null</code>
687     */
688    private static String ensureFoldername(String resourcename) throws CmsIllegalArgumentException {
689
690        if (CmsStringUtil.isEmpty(resourcename)) {
691            throw new CmsIllegalArgumentException(
692                org.opencms.db.Messages.get().container(org.opencms.db.Messages.ERR_BAD_RESOURCENAME_1, resourcename));
693        }
694        if (!CmsResource.isFolder(resourcename)) {
695            resourcename = resourcename.concat("/");
696        }
697        if (resourcename.charAt(0) == '/') {
698            resourcename = resourcename.substring(1);
699        }
700        return resourcename;
701    }
702
703    /**
704     * Updates a single file with the given macro resolver.<p>
705     *
706     * @param cms the cms context
707     * @param file the file to update
708     * @param macroResolver the macro resolver to update with
709     *
710     * @throws CmsException if something goes wrong
711     */
712    private static void updateFile(CmsObject cms, CmsFile file, CmsMacroResolver macroResolver) throws CmsException {
713
714        String encoding = cms.readPropertyObject(file, CmsPropertyDefinition.PROPERTY_CONTENT_ENCODING, true).getValue(
715            OpenCms.getSystemInfo().getDefaultEncoding());
716        String content;
717        try {
718            content = macroResolver.resolveMacros(new String(file.getContents(), encoding));
719        } catch (UnsupportedEncodingException e) {
720            try {
721                content = macroResolver.resolveMacros(
722                    new String(file.getContents(), Charset.defaultCharset().toString()));
723            } catch (UnsupportedEncodingException e1) {
724                content = macroResolver.resolveMacros(new String(file.getContents()));
725            }
726        }
727        // update the content
728        try {
729            file.setContents(content.getBytes(encoding));
730        } catch (UnsupportedEncodingException e) {
731            try {
732                file.setContents(content.getBytes(Charset.defaultCharset().toString()));
733            } catch (UnsupportedEncodingException e1) {
734                file.setContents(content.getBytes());
735            }
736        }
737        // write the target file
738        cms.writeFile(file);
739    }
740
741    /**
742     * Updates all properties of the given resource with the given macro resolver.<p>
743     *
744     * @param cms the cms context
745     * @param resource the resource to update the properties for
746     * @param macroResolver the macro resolver to use
747     *
748     * @throws CmsException if something goes wrong
749     */
750    private static void updateProperties(CmsObject cms, CmsResource resource, CmsMacroResolver macroResolver)
751    throws CmsException {
752
753        Iterator<CmsProperty> it = cms.readPropertyObjects(resource, false).iterator();
754        while (it.hasNext()) {
755            CmsProperty property = it.next();
756            String resValue = null;
757            if (property.getResourceValue() != null) {
758                resValue = macroResolver.resolveMacros(property.getResourceValue());
759            }
760            String strValue = null;
761            if (property.getStructureValue() != null) {
762                strValue = macroResolver.resolveMacros(property.getStructureValue());
763            }
764            CmsProperty newProperty = new CmsProperty(property.getName(), strValue, resValue);
765            cms.writePropertyObject(cms.getSitePath(resource), newProperty);
766        }
767    }
768
769    /**
770     * Adds a macro whose value will be dynamically generated at macro resolution time.<p>
771     *
772     * The value will be generated for each occurence of the macro in a string.<p>
773     *
774     * @param name the name of the macro
775     * @param factory the macro value generator
776     */
777    public void addDynamicMacro(String name, Factory factory) {
778
779        if (m_factories == null) {
780            m_factories = new HashMap<String, Factory>();
781        }
782        m_factories.put(name, factory);
783    }
784
785    /**
786     * Adds a customized macro to this macro resolver.<p>
787     *
788     * @param key the macro to add
789     * @param value the value to return if the macro is encountered
790     */
791    public void addMacro(String key, String value) {
792
793        if (m_additionalMacros == null) {
794            // use lazy initializing
795            m_additionalMacros = new HashMap<String, String>();
796        }
797        m_additionalMacros.put(key, value);
798    }
799
800    /**
801     * @see org.opencms.util.I_CmsMacroResolver#getMacroValue(java.lang.String)
802     */
803    public String getMacroValue(String macro) {
804
805        if (m_messages != null) {
806            if (macro.startsWith(CmsMacroResolver.KEY_LOCALIZED_PREFIX)) {
807                String keyName = macro.substring(CmsMacroResolver.KEY_LOCALIZED_PREFIX.length());
808                return m_messages.keyWithParams(keyName);
809            }
810        }
811
812        if (m_factories != null) {
813            Factory factory = m_factories.get(macro);
814            if (factory != null) {
815                String value = (String)factory.create();
816                return value;
817            }
818        }
819
820        if (m_jspPageContext != null) {
821
822            if (m_jspPageContext.getRequest() != null) {
823                if (macro.startsWith(CmsMacroResolver.KEY_REQUEST_PARAM)) {
824                    // the key is a request parameter
825                    macro = macro.substring(CmsMacroResolver.KEY_REQUEST_PARAM.length());
826                    String result = null;
827                    if (m_parameterMap != null) {
828                        String[] param = m_parameterMap.get(macro);
829                        if ((param != null) && (param.length >= 1)) {
830                            result = param[0];
831                        }
832                    } else {
833                        result = m_jspPageContext.getRequest().getParameter(macro);
834                    }
835                    if ((result == null) && macro.equals(KEY_PROJECT_ID)) {
836                        result = m_cms.getRequestContext().getCurrentProject().getUuid().toString();
837                    }
838                    return result;
839                }
840
841                if ((m_cms != null) && macro.startsWith(CmsMacroResolver.KEY_PROPERTY_ELEMENT)) {
842
843                    // the key is a cms property to be read on the current element
844
845                    macro = macro.substring(CmsMacroResolver.KEY_PROPERTY_ELEMENT.length());
846                    CmsFlexController controller = CmsFlexController.getController(m_jspPageContext.getRequest());
847                    try {
848                        CmsProperty property = m_cms.readPropertyObject(
849                            controller.getCurrentRequest().getElementUri(),
850                            macro,
851                            false);
852                        if (property != CmsProperty.getNullProperty()) {
853                            return property.getValue();
854                        }
855                    } catch (CmsException e) {
856                        if (LOG.isWarnEnabled()) {
857                            LOG.warn(
858                                Messages.get().getBundle().key(
859                                    Messages.LOG_PROPERTY_READING_FAILED_2,
860                                    macro,
861                                    controller.getCurrentRequest().getElementUri()),
862                                e);
863                        }
864                    }
865                }
866            }
867
868            if (macro.startsWith(CmsMacroResolver.KEY_PAGE_CONTEXT)) {
869                // the key is a page context object
870                macro = macro.substring(CmsMacroResolver.KEY_PAGE_CONTEXT.length());
871                int scope = m_jspPageContext.getAttributesScope(macro);
872                return m_jspPageContext.getAttribute(macro, scope).toString();
873            }
874        }
875
876        if (m_cms != null) {
877
878            if (macro.startsWith(CmsMacroResolver.KEY_PROPERTY)) {
879                // the key is a cms property to be read on the current request URI
880                macro = macro.substring(CmsMacroResolver.KEY_PROPERTY.length());
881                try {
882                    CmsProperty property = m_cms.readPropertyObject(m_cms.getRequestContext().getUri(), macro, true);
883                    if (property != CmsProperty.getNullProperty()) {
884                        return property.getValue();
885                    }
886                } catch (CmsException e) {
887                    if (LOG.isWarnEnabled()) {
888                        CmsMessageContainer message = Messages.get().container(
889                            Messages.LOG_PROPERTY_READING_FAILED_2,
890                            macro,
891                            m_cms.getRequestContext().getUri());
892                        LOG.warn(message.key(), e);
893                    }
894                }
895                return null;
896            }
897
898            if (macro.startsWith(CmsMacroResolver.KEY_ATTRIBUTE)) {
899                // the key is an OpenCms runtime attribute
900                macro = macro.substring(CmsMacroResolver.KEY_ATTRIBUTE.length());
901                Object attribute = m_cms.getRequestContext().getAttribute(macro);
902                if (attribute != null) {
903                    return attribute.toString();
904                }
905                return null;
906            }
907
908            if (macro.startsWith(CmsMacroResolver.KEY_OPENCMS)) {
909
910                // the key is a shortcut for a cms runtime value
911
912                String originalKey = macro;
913                macro = macro.substring(CmsMacroResolver.KEY_OPENCMS.length());
914                int index = VALUE_NAMES.indexOf(macro);
915                String value = null;
916
917                switch (index) {
918                    case 0:
919                        // "uri"
920                        value = m_cms.getRequestContext().getUri();
921                        break;
922                    case 1:
923                        // "filename"
924                        value = m_resourceName;
925                        break;
926                    case 2:
927                        // folder
928                        value = m_cms.getRequestContext().getFolderUri();
929                        break;
930                    case 3:
931                        // default.encoding
932                        value = OpenCms.getSystemInfo().getDefaultEncoding();
933                        break;
934                    case 4:
935                        // remoteaddress
936                        value = m_cms.getRequestContext().getRemoteAddress();
937                        break;
938                    case 5:
939                        // webapp
940                        value = OpenCms.getSystemInfo().getWebApplicationName();
941                        break;
942                    case 6:
943                        // webbasepath
944                        value = OpenCms.getSystemInfo().getWebApplicationRfsPath();
945                        break;
946                    case 7:
947                        // version
948                        value = OpenCms.getSystemInfo().getVersionNumber();
949                        break;
950                    case 8:
951                        // versionid
952                        value = OpenCms.getSystemInfo().getVersionId();
953                        break;
954                    default:
955                        // return the key "as is"
956                        value = originalKey;
957                        break;
958                }
959
960                return value;
961            }
962
963            if (CmsMacroResolver.KEY_CURRENT_USER_NAME.equals(macro)) {
964                // the key is the current users login name
965                return m_cms.getRequestContext().getCurrentUser().getName();
966            }
967
968            if (CmsMacroResolver.KEY_CURRENT_USER_FIRSTNAME.equals(macro)) {
969                // the key is the current users first name
970                return m_cms.getRequestContext().getCurrentUser().getFirstname();
971            }
972
973            if (CmsMacroResolver.KEY_CURRENT_USER_LASTNAME.equals(macro)) {
974                // the key is the current users last name
975                return m_cms.getRequestContext().getCurrentUser().getLastname();
976            }
977
978            if (CmsMacroResolver.KEY_CURRENT_USER_DISPLAYNAME.equals(macro)) {
979                // the key is the current users display name
980                try {
981                    if (m_messages != null) {
982                        return m_cms.getRequestContext().getCurrentUser().getDisplayName(m_cms, m_messages.getLocale());
983                    } else {
984                        return m_cms.getRequestContext().getCurrentUser().getDisplayName(
985                            m_cms,
986                            m_cms.getRequestContext().getLocale());
987                    }
988                } catch (CmsException e) {
989                    // ignore, macro can not be resolved
990                }
991            }
992
993            if (CmsMacroResolver.KEY_CURRENT_ORGUNIT_FQN.equals(macro)) {
994                // the key is the current organizational unit fully qualified name
995                return m_cms.getRequestContext().getOuFqn();
996            }
997
998            if (CmsMacroResolver.KEY_CURRENT_ORGUNIT_DESCRIPTION.equals(macro)) {
999                // the key is the current organizational unit description
1000                try {
1001                    CmsOrganizationalUnit ou = OpenCms.getOrgUnitManager().readOrganizationalUnit(
1002                        m_cms,
1003                        m_cms.getRequestContext().getOuFqn());
1004                    if (m_messages != null) {
1005                        return ou.getDescription(m_messages.getLocale());
1006                    } else {
1007                        return ou.getDescription(m_cms.getRequestContext().getLocale());
1008                    }
1009                } catch (CmsException e) {
1010                    // ignore, macro can not be resolved
1011                }
1012            }
1013
1014            if (CmsMacroResolver.KEY_CURRENT_USER_FULLNAME.equals(macro)) {
1015                // the key is the current users full name
1016                return m_cms.getRequestContext().getCurrentUser().getFullName();
1017            }
1018
1019            if (CmsMacroResolver.KEY_CURRENT_USER_EMAIL.equals(macro)) {
1020                // the key is the current users email address
1021                return m_cms.getRequestContext().getCurrentUser().getEmail();
1022            }
1023
1024            if (CmsMacroResolver.KEY_CURRENT_USER_STREET.equals(macro)) {
1025                // the key is the current users address
1026                return m_cms.getRequestContext().getCurrentUser().getAddress();
1027            }
1028
1029            if (CmsMacroResolver.KEY_CURRENT_USER_ZIP.equals(macro)) {
1030                // the key is the current users zip code
1031                return m_cms.getRequestContext().getCurrentUser().getZipcode();
1032            }
1033
1034            if (CmsMacroResolver.KEY_CURRENT_USER_COUNTRY.equals(macro)) {
1035                // the key is the current users country
1036                return m_cms.getRequestContext().getCurrentUser().getCountry();
1037            }
1038
1039            if (CmsMacroResolver.KEY_CURRENT_USER_CITY.equals(macro)) {
1040                // the key is the current users city
1041                return m_cms.getRequestContext().getCurrentUser().getCity();
1042            }
1043
1044            if (CmsMacroResolver.KEY_CURRENT_USER_LASTLOGIN.equals(macro) && (m_messages != null)) {
1045                // the key is the current users last login timestamp
1046                return m_messages.getDateTime(m_cms.getRequestContext().getCurrentUser().getLastlogin());
1047            }
1048
1049            if (CmsMacroResolver.KEY_REQUEST_SITEROOT.equals(macro)) {
1050                // the key is the currently requested site root
1051                return m_cms.getRequestContext().getSiteRoot();
1052            }
1053
1054            if (CmsMacroResolver.KEY_REQUEST_URI.equals(macro)) {
1055                // the key is the currently requested uri
1056                return m_cms.getRequestContext().getUri();
1057            }
1058
1059            if (CmsMacroResolver.KEY_REQUEST_FOLDER.equals(macro)) {
1060                // the key is the currently requested folder
1061                return CmsResource.getParentFolder(m_cms.getRequestContext().getUri());
1062            }
1063
1064            if (CmsMacroResolver.KEY_REQUEST_ENCODING.equals(macro)) {
1065                // the key is the current encoding of the request
1066                return m_cms.getRequestContext().getEncoding();
1067            }
1068
1069            if (CmsMacroResolver.KEY_REQUEST_LOCALE.equals(macro)) {
1070                // the key is the current locale of the request
1071                return m_cms.getRequestContext().getLocale().toString();
1072            }
1073
1074            if (CmsMacroResolver.KEY_CONTEXT_PATH.equals(macro)) {
1075                // the key is the OpenCms context path
1076                return OpenCms.getSystemInfo().getContextPath();
1077            }
1078
1079            if (CmsMacroResolver.KEY_CURRENT_USER_INSTITUTION.equals(macro)) {
1080                // the key is the current users institution
1081                return m_cms.getRequestContext().getCurrentUser().getInstitution();
1082            }
1083
1084        }
1085
1086        if (CmsMacroResolver.KEY_CURRENT_TIME.equals(macro)) {
1087            // the key is the current system time
1088            return String.valueOf(System.currentTimeMillis());
1089        } else if (macro.startsWith(CmsMacroResolver.KEY_CURRENT_TIME)) {
1090            // the key starts with the current system time
1091            macro = macro.substring(CmsMacroResolver.KEY_CURRENT_TIME.length()).trim();
1092            char operator = macro.charAt(0);
1093            macro = macro.substring(1).trim();
1094            long delta = 0;
1095            try {
1096                delta = Long.parseLong(macro);
1097            } catch (NumberFormatException e) {
1098                // ignore, there will be no delta
1099            }
1100            long resultTime = System.currentTimeMillis();
1101            switch (operator) {
1102                case '+':
1103                    // add delta to current time
1104                    resultTime += delta;
1105                    break;
1106                case '-':
1107                    // subtract delta from current time
1108                    resultTime -= delta;
1109                    break;
1110                default:
1111                    break;
1112            }
1113            return String.valueOf(resultTime);
1114        }
1115
1116        if (CmsMacroResolver.KEY_RANDOM_ID.equals(macro)) {
1117            // a random id value is requested
1118            String id = CmsUUID.getConstantUUID("randomId." + Math.random()).toString();
1119            // full UUIDs are to long, the first part should be enough
1120            return id.substring(0, id.indexOf('-'));
1121        }
1122
1123        if (m_additionalMacros != null) {
1124            return m_additionalMacros.get(macro);
1125        }
1126
1127        return null;
1128    }
1129
1130    /**
1131     * @see org.opencms.util.I_CmsMacroResolver#isKeepEmptyMacros()
1132     */
1133    public boolean isKeepEmptyMacros() {
1134
1135        return m_keepEmptyMacros;
1136    }
1137
1138    /**
1139     * Resolves the macros in the given input.<p>
1140     *
1141     * Calls <code>{@link #resolveMacros(String)}</code> until no more macros can
1142     * be resolved in the input. This way "nested" macros in the input are resolved as well.<p>
1143     *
1144     * @see org.opencms.util.I_CmsMacroResolver#resolveMacros(java.lang.String)
1145     */
1146    public String resolveMacros(String input) {
1147
1148        String result = input;
1149
1150        if (input != null) {
1151            String lastResult;
1152            do {
1153                // save result for next comparison
1154                lastResult = result;
1155                // resolve the macros
1156                result = CmsMacroResolver.resolveMacros(result, this);
1157                // if nothing changes then the final result is found
1158            } while (!result.equals(lastResult));
1159        }
1160
1161        // return the result
1162        return result;
1163    }
1164
1165    /**
1166     * Provides a set of additional macros to this macro resolver.<p>
1167     *
1168     * Macros added with {@link #addMacro(String, String)} are added to the same set
1169     *
1170     * @param additionalMacros the additional macros to add
1171     *
1172     * @return this instance of the macro resolver
1173     */
1174    public CmsMacroResolver setAdditionalMacros(Map<String, String> additionalMacros) {
1175
1176        m_additionalMacros = additionalMacros;
1177        return this;
1178    }
1179
1180    /**
1181     * Provides an OpenCms user context to this macro resolver, required to resolve certain macros.<p>
1182     *
1183     * @param cms the OpenCms user context
1184     *
1185     * @return this instance of the macro resolver
1186     */
1187    public CmsMacroResolver setCmsObject(CmsObject cms) {
1188
1189        m_cms = cms;
1190        return this;
1191    }
1192
1193    /**
1194     * Provides a JSP page context to this macro resolver, required to resolve certain macros.<p>
1195     *
1196     * @param jspPageContext the JSP page context to use
1197     *
1198     * @return this instance of the macro resolver
1199     */
1200    public CmsMacroResolver setJspPageContext(PageContext jspPageContext) {
1201
1202        m_jspPageContext = jspPageContext;
1203        return this;
1204    }
1205
1206    /**
1207     * Controls of macros that can't be resolved are left unchanged in the input,
1208     * or are replaced with an empty String.<p>
1209     *
1210     * @param keepEmptyMacros the replacement flag to use
1211     *
1212     * @return this instance of the macro resolver
1213     *
1214     * @see #isKeepEmptyMacros()
1215     */
1216    public CmsMacroResolver setKeepEmptyMacros(boolean keepEmptyMacros) {
1217
1218        m_keepEmptyMacros = keepEmptyMacros;
1219        return this;
1220    }
1221
1222    /**
1223     * Provides a set of <code>{@link CmsMessages}</code> to this macro resolver,
1224     * required to resolve localized macros.<p>
1225     *
1226     * @param messages the message resource bundle to use
1227     *
1228     * @return this instance of the macro resolver
1229     */
1230    public CmsMacroResolver setMessages(CmsMessages messages) {
1231
1232        m_messages = messages;
1233        return this;
1234    }
1235
1236    /**
1237     * Sets the parameter map.<p>
1238     *
1239     * @param parameterMap the parameter map to set
1240     */
1241    public void setParameterMap(Map<String, String[]> parameterMap) {
1242
1243        m_parameterMap = parameterMap;
1244    }
1245
1246    /**
1247     * Provides a resource name to this macro resolver, required to resolve certain macros.<p>
1248     *
1249     * @param resourceName the resource name to use
1250     *
1251     * @return this instance of the macro resolver
1252     */
1253    public CmsMacroResolver setResourceName(String resourceName) {
1254
1255        m_resourceName = resourceName;
1256        return this;
1257    }
1258
1259    /**
1260     * Returns a function which applies the macro substitution of this resolver to its argument.<p>
1261     *
1262     * @return a function performing string substitution with this resolver
1263     */
1264    public Function<String, String> toFunction() {
1265
1266        return new Function<String, String>() {
1267
1268            public String apply(String input) {
1269
1270                return resolveMacros(input);
1271
1272            }
1273        };
1274    }
1275}