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