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.CmsRequestContext;
034import org.opencms.file.CmsResource;
035import org.opencms.file.CmsResource.CmsResourceCopyMode;
036import org.opencms.file.CmsResource.CmsResourceDeleteMode;
037import org.opencms.file.CmsResourceFilter;
038import org.opencms.file.CmsUser;
039import org.opencms.file.I_CmsResource;
040import org.opencms.file.types.CmsResourceTypeJsp;
041import org.opencms.file.types.CmsResourceTypePlain;
042import org.opencms.file.types.CmsResourceTypeXmlContent;
043import org.opencms.file.types.CmsResourceTypeXmlPage;
044import org.opencms.file.types.I_CmsResourceType;
045import org.opencms.i18n.CmsEncoder;
046import org.opencms.i18n.CmsLocaleManager;
047import org.opencms.loader.CmsLoaderException;
048import org.opencms.lock.CmsLock;
049import org.opencms.main.CmsException;
050import org.opencms.main.CmsIllegalArgumentException;
051import org.opencms.main.CmsLog;
052import org.opencms.main.OpenCms;
053import org.opencms.util.CmsUUID;
054
055import java.util.ArrayList;
056import java.util.Collections;
057import java.util.Iterator;
058import java.util.List;
059import java.util.Map;
060
061import org.apache.commons.logging.Log;
062
063/**
064 * This class contains a subset of the methods of {@link CmsObject} and uses the
065 * configured resource wrappers ({@link I_CmsResourceWrapper}) to change the view
066 * to the existing resources in the VFS.<p>
067 *
068 * Almost every method in this class iterates through the configured list of
069 * {@link I_CmsResourceWrapper} and calls the same method there. The first resource
070 * wrapper in the list which feels responsible for that action handles it and the
071 * iteration ends. So the resource wrappers should check in every method if it is
072 * responsible or not. Be careful if there are more than one resource wrapper for
073 * the same resource in the VFS, because the first in the list wins. If the iteration is
074 * finished and no resource wrapper felt responsible the default action is to call the
075 * method in the {@link CmsObject}.<p>
076 *
077 * It is possible to create an unchanged access to the resource in the VFS by creating
078 * a new instance of the CmsObjectWrapper with an empty list of resource wrappers.<p>
079 *
080 * @since 6.2.4
081 */
082public class CmsObjectWrapper {
083
084    /** The name of the attribute in the {@link CmsRequestContext} where the current CmsObjectWrapper can be found. */
085    public static final String ATTRIBUTE_NAME = "org.opencms.file.wrapper.CmsObjectWrapper";
086
087    /** The log object for this class. */
088    private static final Log LOG = CmsLog.getLog(CmsObjectWrapper.class);
089
090    /** Flag to contro whether byte order marks should be added to plaintext files. */
091    private boolean m_addByteOrderMark = true;
092
093    /** The initialized CmsObject. */
094    private CmsObject m_cms;
095
096    /** The list with the configured wrappers (entries of type {@link I_CmsResourceWrapper}). */
097    private List<I_CmsResourceWrapper> m_wrappers;
098
099    /**
100     * Constructor with the CmsObject to wrap and the resource wrappers to use.<p>
101     *
102     * @param cms the initialized CmsObject
103     * @param wrappers the configured wrappers to use (entries of type {@link I_CmsResourceWrapper})
104     */
105    public CmsObjectWrapper(CmsObject cms, List<I_CmsResourceWrapper> wrappers) {
106
107        m_cms = cms;
108        m_wrappers = wrappers;
109    }
110
111    /**
112     * Copies a resource.<p>
113     *
114     * Iterates through all configured resource wrappers till the first returns <code>true</code>.<p>
115     *
116     * @see I_CmsResourceWrapper#copyResource(CmsObject, String, String, CmsResource.CmsResourceCopyMode)
117     * @see CmsObject#copyResource(String, String, CmsResource.CmsResourceCopyMode)
118     *
119     * @param source the name of the resource to copy (full path)
120     * @param destination the name of the copy destination (full path)
121     * @param siblingMode indicates how to handle siblings during copy
122     *
123     * @throws CmsException if something goes wrong
124     * @throws CmsIllegalArgumentException if the <code>destination</code> argument is null or of length 0
125     */
126    public void copyResource(String source, String destination, CmsResourceCopyMode siblingMode)
127    throws CmsException, CmsIllegalArgumentException {
128
129        boolean exec = false;
130
131        // iterate through all wrappers and call "copyResource" till one does not return null
132        List<I_CmsResourceWrapper> wrappers = getWrappers();
133        Iterator<I_CmsResourceWrapper> iter = wrappers.iterator();
134        while (iter.hasNext()) {
135            I_CmsResourceWrapper wrapper = iter.next();
136            exec = wrapper.copyResource(m_cms, source, destination, siblingMode);
137            if (exec) {
138                break;
139            }
140        }
141
142        // delegate the call to the CmsObject
143        if (!exec) {
144            m_cms.copyResource(source, destination, siblingMode);
145        }
146
147    }
148
149    /**
150     * Creates a new resource of the given resource type with empty content and no properties.<p>
151     *
152     * @see #createResource(String, int, byte[], List)
153     *
154     * @param resourcename the name of the resource to create (full path)
155     * @param type the type of the resource to create
156     *
157     * @return the created resource
158     *
159     * @throws CmsException if something goes wrong
160     * @throws CmsIllegalArgumentException if the given <code>resourcename</code> is null or of length 0
161     */
162    public CmsResource createResource(String resourcename, int type) throws CmsException, CmsIllegalArgumentException {
163
164        return createResource(resourcename, type, new byte[0], new ArrayList<CmsProperty>(0));
165    }
166
167    /**
168     * Creates a new resource of the given resource type with the provided content and properties.<p>
169     *
170     * Iterates through all configured resource wrappers till the first returns not <code>null</code>.<p>
171     *
172     * @see I_CmsResourceWrapper#createResource(CmsObject, String, int, byte[], List)
173     * @see CmsObject#createResource(String, int, byte[], List)
174     *
175     * @param resourcename the name of the resource to create (full path)
176     * @param type the type of the resource to create
177     * @param content the contents for the new resource
178     * @param properties the properties for the new resource
179     *
180     * @return the created resource
181     *
182     * @throws CmsException if something goes wrong
183     * @throws CmsIllegalArgumentException if the <code>resourcename</code> argument is null or of length 0
184     */
185    public CmsResource createResource(String resourcename, int type, byte[] content, List<CmsProperty> properties)
186    throws CmsException, CmsIllegalArgumentException {
187
188        CmsResource res = null;
189
190        // iterate through all wrappers and call "createResource" till one does not return null
191        List<I_CmsResourceWrapper> wrappers = getWrappers();
192        Iterator<I_CmsResourceWrapper> iter = wrappers.iterator();
193        while (iter.hasNext()) {
194            I_CmsResourceWrapper wrapper = iter.next();
195            res = wrapper.createResource(m_cms, resourcename, type, content, properties);
196            if (res != null) {
197                break;
198            }
199        }
200
201        // delegate the call to the CmsObject
202        if (res == null) {
203            res = m_cms.createResource(resourcename, type, content, properties);
204        }
205
206        return res;
207    }
208
209    /**
210     * Deletes a resource given its name.<p>
211     *
212     * Iterates through all configured resource wrappers till the first returns <code>true</code>.<p>
213     *
214     * @see I_CmsResourceWrapper#deleteResource(CmsObject, String, CmsResource.CmsResourceDeleteMode)
215     * @see CmsObject#deleteResource(String, CmsResource.CmsResourceDeleteMode)
216     *
217     * @param resourcename the name of the resource to delete (full path)
218     * @param siblingMode indicates how to handle siblings of the deleted resource
219     *
220     * @throws CmsException if something goes wrong
221     */
222    public void deleteResource(String resourcename, CmsResourceDeleteMode siblingMode) throws CmsException {
223
224        boolean exec = false;
225
226        // iterate through all wrappers and call "deleteResource" till one does not return false
227        List<I_CmsResourceWrapper> wrappers = getWrappers();
228        Iterator<I_CmsResourceWrapper> iter = wrappers.iterator();
229        while (iter.hasNext()) {
230            I_CmsResourceWrapper wrapper = iter.next();
231            exec = wrapper.deleteResource(m_cms, resourcename, siblingMode);
232            if (exec) {
233                break;
234            }
235        }
236
237        // delegate the call to the CmsObject
238        if (!exec) {
239            m_cms.deleteResource(resourcename, siblingMode);
240        }
241    }
242
243    /**
244     * Checks the availability of a resource in the VFS,
245     * using the {@link CmsResourceFilter#DEFAULT} filter.<p>
246     *
247     * Here it will be first checked if the resource exists in the VFS by calling
248     * {@link org.opencms.file.CmsObject#existsResource(String)}. Only if it doesn't exist
249     * in the VFS the method {@link I_CmsResourceWrapper#readResource(CmsObject, String, CmsResourceFilter)}
250     * in the configured resource wrappers are called till the first does not throw an exception or returns
251     * <code>null</code>.<p>
252     *
253     * @param resourcename the name of the resource to check (full path)
254     *
255     * @return <code>true</code> if the resource is available
256     */
257    public boolean existsResource(String resourcename) {
258
259        // first try to find the resource
260        boolean ret = m_cms.existsResource(resourcename);
261
262        // if not exists, ask the resource type wrappers
263        if (!ret) {
264
265            List<I_CmsResourceWrapper> wrappers = getWrappers();
266            Iterator<I_CmsResourceWrapper> iter = wrappers.iterator();
267            while (iter.hasNext()) {
268                I_CmsResourceWrapper wrapper = iter.next();
269                try {
270                    CmsResource res = wrapper.readResource(m_cms, resourcename, CmsResourceFilter.DEFAULT);
271                    if (res != null) {
272                        ret = true;
273                        break;
274                    }
275                } catch (CmsException ex) {
276                    // noop
277                }
278            }
279
280        }
281
282        return ret;
283    }
284
285    /**
286     * Returns the lock state for a specified resource.<p>
287     *
288     * Iterates through all configured resource wrappers till the first returns not <code>null</code>.<p>
289     *
290     * @see I_CmsResourceWrapper#getLock(CmsObject, CmsResource)
291     * @see CmsObject#getLock(CmsResource)
292     *
293     * @param resource the resource to return the lock state for
294     *
295     * @return the lock state for the specified resource
296     *
297     * @throws CmsException if something goes wrong
298     */
299    public CmsLock getLock(CmsResource resource) throws CmsException {
300
301        CmsLock lock = null;
302
303        // iterate through all wrappers and call "getLock" till one does not return null
304        List<I_CmsResourceWrapper> wrappers = getWrappers();
305        Iterator<I_CmsResourceWrapper> iter = wrappers.iterator();
306        while (iter.hasNext()) {
307            I_CmsResourceWrapper wrapper = iter.next();
308            lock = wrapper.getLock(m_cms, resource);
309            if (lock != null) {
310                break;
311            }
312        }
313
314        // delegate the call to the CmsObject
315        if (lock == null) {
316            lock = m_cms.getLock(resource);
317        }
318
319        return lock;
320    }
321
322    /**
323     * Delegate method for {@link CmsObject#getRequestContext()}.<p>
324     *
325     * @see CmsObject#getRequestContext()
326    
327     * @return the current users request context
328     */
329    public CmsRequestContext getRequestContext() {
330
331        return m_cms.getRequestContext();
332    }
333
334    /**
335     * Returns all child resources of a resource, that is the resources
336     * contained in a folder.<p>
337     *
338     * First fetch all child resources from VFS by calling {@link CmsObject#getResourcesInFolder(String, CmsResourceFilter)}.
339     * After that all resource wrapper are called {@link I_CmsResourceWrapper#addResourcesToFolder(CmsObject, String, CmsResourceFilter)}
340     * to have the chance to add additional resources to those already existing. In that list every resource is given to
341     * the appropriate resource wrapper ({@link I_CmsResourceWrapper#wrapResource(CmsObject, CmsResource)}) to have the
342     * possibility to change the existing resources. The matching resource wrapper for a resource is found by a call to
343     * {@link I_CmsResourceWrapper#isWrappedResource(CmsObject, CmsResource)}.<p>
344     *
345     * @see I_CmsResourceWrapper#addResourcesToFolder(CmsObject, String, CmsResourceFilter)
346     * @see CmsObject#getResourcesInFolder(String, CmsResourceFilter)
347     *
348     * @param resourcename the full path of the resource to return the child resources for
349     * @param filter the resource filter to use
350     *
351     * @return a list of all child <code>{@link CmsResource}</code>s
352     *
353     * @throws CmsException if something goes wrong
354     */
355    public List<CmsResource> getResourcesInFolder(String resourcename, CmsResourceFilter filter) throws CmsException {
356
357        List<CmsResource> list = new ArrayList<CmsResource>();
358
359        // read children existing in the VFS
360        try {
361            list.addAll(m_cms.getResourcesInFolder(resourcename, filter));
362        } catch (CmsException ex) {
363            //noop
364        }
365
366        // iterate through all wrappers and call "addResourcesToFolder" and add the results to the list
367        List<I_CmsResourceWrapper> wrappers = getWrappers();
368        Iterator<I_CmsResourceWrapper> iter1 = wrappers.iterator();
369        while (iter1.hasNext()) {
370            I_CmsResourceWrapper wrapper = iter1.next();
371            List<CmsResource> added = wrapper.addResourcesToFolder(m_cms, resourcename, filter);
372            if (added != null) {
373                list.addAll(added);
374            }
375        }
376
377        // create a new list to add all resources
378        ArrayList<CmsResource> wrapped = new ArrayList<CmsResource>();
379
380        // eventually wrap the found resources
381        Iterator<CmsResource> iter2 = list.iterator();
382        while (iter2.hasNext()) {
383            CmsResource res = iter2.next();
384
385            // correct the length of the content if an UTF-8 marker would be added later
386            if (needUtf8Marker(res) && !startsWithUtf8Marker(res)) {
387                CmsWrappedResource wrap = new CmsWrappedResource(res);
388                wrap.setLength(res.getLength() + CmsResourceWrapperUtils.UTF8_MARKER.length);
389
390                res = wrap.getResource();
391            }
392
393            // get resource type wrapper for the resource
394            I_CmsResourceWrapper resWrapper = getResourceTypeWrapper(res);
395
396            if (resWrapper != null) {
397
398                // adds the wrapped resources
399                wrapped.add(resWrapper.wrapResource(m_cms, res));
400            } else {
401
402                // add the resource unwrapped
403                wrapped.add(res);
404            }
405        }
406
407        // sort the wrapped list correctly
408        Collections.sort(wrapped, I_CmsResource.COMPARE_ROOT_PATH_IGNORE_CASE_FOLDERS_FIRST);
409
410        return wrapped;
411    }
412
413    /**
414     * Delegate method for {@link CmsObject#getSitePath(CmsResource)}.<p>
415     *
416     * @see CmsObject#getSitePath(org.opencms.file.CmsResource)
417     *
418     * @param resource the resource to get the adjusted site root path for
419     *
420     * @return the absolute resource path adjusted for the current site
421     */
422    public String getSitePath(CmsResource resource) {
423
424        return m_cms.getSitePath(resource);
425    }
426
427    /**
428     * Returns the configured resource wrappers used by this instance.<p>
429     *
430     * Entries in list are from type {@link I_CmsResourceWrapper}.<p>
431     *
432     * @return the configured resource wrappers for this instance
433     */
434    public List<I_CmsResourceWrapper> getWrappers() {
435
436        return m_wrappers;
437    }
438
439    /**
440     * Locks a resource.<p>
441     *
442     * Iterates through all configured resource wrappers till the first returns <code>true</code>.<p>
443     *
444     * @see CmsObject#lockResource(String)
445     *
446     * @param resourcename the name of the resource to lock (full path)
447     *
448     * @throws CmsException if something goes wrong
449     */
450    public void lockResource(String resourcename) throws CmsException {
451
452        boolean exec = false;
453
454        // iterate through all wrappers and call "lockResource" till one does not return false
455        List<I_CmsResourceWrapper> wrappers = getWrappers();
456        Iterator<I_CmsResourceWrapper> iter = wrappers.iterator();
457        while (iter.hasNext()) {
458            I_CmsResourceWrapper wrapper = iter.next();
459            exec = wrapper.lockResource(m_cms, resourcename, false);
460            if (exec) {
461                break;
462            }
463        }
464
465        // delegate the call to the CmsObject
466        if (!exec) {
467            m_cms.lockResource(resourcename);
468        }
469    }
470
471    /**
472     * Locks a resource temporarily.<p>
473     *
474     * @param resourceName the name of the resource to lock
475     *
476     * @throws CmsException if something goes wrong
477     */
478    public void lockResourceTemporary(String resourceName) throws CmsException {
479
480        boolean exec = false;
481        // iterate through all wrappers and call "lockResource" till one does not return false
482        List<I_CmsResourceWrapper> wrappers = getWrappers();
483        for (I_CmsResourceWrapper wrapper : wrappers) {
484            exec = wrapper.lockResource(m_cms, resourceName, true);
485            if (exec) {
486                break;
487            }
488        }
489
490        // delegate the call to the CmsObject
491        if (!exec) {
492            m_cms.lockResourceTemporary(resourceName);
493        }
494    }
495
496    /**
497     * Moves a resource to the given destination.<p>
498     *
499     * Iterates through all configured resource wrappers till the first returns <code>true</code>.<p>
500     *
501     * @see I_CmsResourceWrapper#moveResource(CmsObject, String, String)
502     * @see CmsObject#moveResource(String, String)
503     *
504     * @param source the name of the resource to move (full path)
505     * @param destination the destination resource name (full path)
506     *
507     * @throws CmsException if something goes wrong
508     */
509    public void moveResource(String source, String destination) throws CmsException {
510
511        boolean exec = false;
512
513        // iterate through all wrappers and call "moveResource" till one does not return false
514        List<I_CmsResourceWrapper> wrappers = getWrappers();
515        Iterator<I_CmsResourceWrapper> iter = wrappers.iterator();
516        while (iter.hasNext()) {
517            I_CmsResourceWrapper wrapper = iter.next();
518            exec = wrapper.moveResource(m_cms, source, destination);
519            if (exec) {
520                break;
521            }
522        }
523
524        // delegate the call to the CmsObject
525        if (!exec) {
526            m_cms.moveResource(source, destination);
527        }
528    }
529
530    /**
531     * Reads a file resource (including it's binary content) from the VFS,
532     * using the specified resource filter.<p>
533     *
534     * Iterates through all configured resource wrappers till the first returns not <code>null</code>.<p>
535     *
536     * If the resource contains textual content and the encoding is UTF-8, then the byte order mask
537     * for UTF-8 is added at the start of the content to make sure that a client using this content
538     * displays it correctly.<p>
539     *
540     * @see I_CmsResourceWrapper#readFile(CmsObject, String, CmsResourceFilter)
541     * @see CmsObject#readFile(String, CmsResourceFilter)
542     *
543     * @param resourcename the name of the resource to read (full path)
544     * @param filter the resource filter to use while reading
545     *
546     * @return the file resource that was read
547     *
548     * @throws CmsException if the file resource could not be read for any reason
549     */
550    public CmsFile readFile(String resourcename, CmsResourceFilter filter) throws CmsException {
551
552        CmsFile res = null;
553
554        // iterate through all wrappers and call "readFile" till one does not return null
555        List<I_CmsResourceWrapper> wrappers = getWrappers();
556        Iterator<I_CmsResourceWrapper> iter = wrappers.iterator();
557        while (iter.hasNext()) {
558            I_CmsResourceWrapper wrapper = iter.next();
559            res = wrapper.readFile(m_cms, resourcename, filter);
560            if (res != null) {
561                break;
562            }
563        }
564
565        // delegate the call to the CmsObject
566        if (res == null) {
567            res = m_cms.readFile(resourcename, filter);
568        }
569
570        // for text based resources which are encoded in UTF-8 add the UTF marker at the start
571        // of the content
572        if (needUtf8Marker(res)) {
573
574            if (LOG.isDebugEnabled()) {
575                LOG.debug(Messages.get().getBundle().key(Messages.LOG_ADD_UTF8_MARKER_1, res.getRootPath()));
576            }
577
578            res.setContents(CmsResourceWrapperUtils.addUtf8Marker(res.getContents()));
579        }
580
581        return res;
582    }
583
584    /**
585     * Reads the properties for a resource with a specific path, if it exists.
586     *
587     * @param path a VFS path
588     * @return the map of properties for the resource
589     *
590     * @throws CmsException if something goes wrong
591     */
592    public Map<String, CmsProperty> readProperties(String path) throws CmsException {
593
594        if (m_cms.existsResource(path, CmsResourceFilter.IGNORE_EXPIRATION)) {
595            return CmsProperty.toObjectMap(m_cms.readPropertyObjects(path, false));
596        } else {
597            return Collections.emptyMap();
598        }
599
600    }
601
602    /**
603     * Delegate method for {@link CmsObject#readPropertyObject(CmsResource, String, boolean)}.<p>
604     *
605     * @see CmsObject#readPropertyObject(CmsResource, String, boolean)
606     *
607     * @param resource the resource where the property is attached to
608     * @param property the property name
609     * @param search if true, the property is searched on all parent folders of the resource,
610     *      if it's not found attached directly to the resource
611     *
612     * @return the required property, or <code>{@link CmsProperty#getNullProperty()}</code> if the property was not found
613     *
614     * @throws CmsException if something goes wrong
615     */
616    public CmsProperty readPropertyObject(CmsResource resource, String property, boolean search) throws CmsException {
617
618        return m_cms.readPropertyObject(resource, property, search);
619    }
620
621    /**
622     * Delegate method for {@link CmsObject#readResource(CmsUUID, CmsResourceFilter)}.<p>
623     *
624     * @see CmsObject#readResource(CmsUUID, CmsResourceFilter)
625     *
626     * @param structureID the ID of the structure to read
627     * @param filter the resource filter to use while reading
628     *
629     * @return the resource that was read
630     *
631     * @throws CmsException if the resource could not be read for any reason
632     */
633    public CmsResource readResource(CmsUUID structureID, CmsResourceFilter filter) throws CmsException {
634
635        return m_cms.readResource(structureID, filter);
636    }
637
638    /**
639     * Reads a resource from the VFS,
640     * using the <code>{@link CmsResourceFilter#DEFAULT}</code> filter.<p>
641     *
642     * Iterates through all configured resource wrappers till the first returns not <code>null</code>.<p>
643     *
644     * @see I_CmsResourceWrapper#readResource(CmsObject, String, CmsResourceFilter)
645     * @see CmsObject#readResource(String, CmsResourceFilter)
646     *
647     * @param resourcename The name of the resource to read (full path)
648     * @param filter the resource filter to use while reading
649     *
650     * @return the resource that was read
651     *
652     * @throws CmsException if the resource could not be read for any reason
653     */
654    public CmsResource readResource(String resourcename, CmsResourceFilter filter) throws CmsException {
655
656        CmsResource res = null;
657
658        // iterate through all wrappers and call "readResource" till one does not return null
659        List<I_CmsResourceWrapper> wrappers = getWrappers();
660        Iterator<I_CmsResourceWrapper> iter = wrappers.iterator();
661        while (iter.hasNext()) {
662            I_CmsResourceWrapper wrapper = iter.next();
663            res = wrapper.readResource(m_cms, resourcename, filter);
664            if (res != null) {
665                break;
666            }
667        }
668
669        // delegate the call to the CmsObject
670        if (res == null) {
671            res = m_cms.readResource(resourcename, filter);
672        }
673
674        // correct the length of the content if an UTF-8 marker would be added later
675        if (needUtf8Marker(res) && !startsWithUtf8Marker(res)) {
676            CmsWrappedResource wrap = new CmsWrappedResource(res);
677            wrap.setLength(res.getLength() + CmsResourceWrapperUtils.UTF8_MARKER.length);
678
679            return wrap.getResource();
680        }
681
682        return res;
683    }
684
685    /**
686     * Delegate method for {@link CmsObject#readUser(CmsUUID)}.<p>
687     *
688     * @see CmsObject#readUser(CmsUUID)
689     *
690     * @param userId the id of the user to be read
691     *
692     * @return the user with the given id
693     *
694     * @throws CmsException if something goes wrong
695     */
696    public CmsUser readUser(CmsUUID userId) throws CmsException {
697
698        return m_cms.readUser(userId);
699    }
700
701    /**
702     * Returns a link to an existing resource in the VFS.<p>
703     *
704     * Because it is possible through the <code>CmsObjectWrapper</code> to create "virtual" resources,
705     * which can not be found in the VFS, it is necessary to change the links in pages
706     * as well, so that they point to resources which really exists in the VFS.<p>
707     *
708     * Iterates through all configured resource wrappers till the first returns not <code>null</code>.<p>
709     *
710     * @see #rewriteLink(String)
711     * @see I_CmsResourceWrapper#restoreLink(CmsObject, String)
712     *
713     * @param path the path to the resource
714     *
715     * @return the path for the resource which exists in the VFS
716     */
717    public String restoreLink(String path) {
718
719        if ((path != null) && (path.startsWith("#"))) {
720            return path;
721        }
722
723        String ret = null;
724
725        // iterate through all wrappers and call "restoreLink" till one does not return null
726        List<I_CmsResourceWrapper> wrappers = getWrappers();
727        Iterator<I_CmsResourceWrapper> iter = wrappers.iterator();
728        while (iter.hasNext()) {
729            I_CmsResourceWrapper wrapper = iter.next();
730            ret = wrapper.restoreLink(m_cms, m_cms.getRequestContext().removeSiteRoot(path));
731            if (ret != null) {
732                return ret;
733            }
734        }
735
736        return path;
737    }
738
739    /**
740     * Returns a link to a resource after it was wrapped by the CmsObjectWrapper.<p>
741     *
742     * Because it is possible to change the names of resources inside the VFS by this
743     * <code>CmsObjectWrapper</code>, it is necessary to change the links used in pages
744     * as well, so that they point to the changed name of the resource.<p>
745     *
746     * For example: <code>/sites/default/index.html</code> becomes to
747     * <code>/sites/default/index.html.jsp</code>, because it is a jsp page, the links
748     * in pages where corrected so that they point to the new name (with extension "jsp").<p>
749     *
750     * Used for the link processing in the class {@link org.opencms.relations.CmsLink}.<p>
751     *
752     * Iterates through all configured resource wrappers till the first returns not <code>null</code>.<p>
753     *
754     * @see #restoreLink(String)
755     * @see I_CmsResourceWrapper#rewriteLink(CmsObject, CmsResource)
756     *
757     * @param path the full path where to find the resource
758     *
759     * @return the rewritten link for the resource
760     */
761    public String rewriteLink(String path) {
762
763        CmsResource res = null;
764
765        try {
766            res = readResource(m_cms.getRequestContext().removeSiteRoot(path), CmsResourceFilter.ALL);
767            if (res != null) {
768                String ret = null;
769
770                // iterate through all wrappers and call "rewriteLink" till one does not return null
771                List<I_CmsResourceWrapper> wrappers = getWrappers();
772                Iterator<I_CmsResourceWrapper> iter = wrappers.iterator();
773                while (iter.hasNext()) {
774                    I_CmsResourceWrapper wrapper = iter.next();
775                    ret = wrapper.rewriteLink(m_cms, res);
776                    if (ret != null) {
777                        return ret;
778                    }
779                }
780            }
781        } catch (CmsException ex) {
782            // noop
783        }
784
785        return path;
786    }
787
788    /**
789     * Enables or disables the automatic adding of byte order marks to plaintext files.<p>
790     *
791     * @param addByteOrderMark true if byte order marks should be added to plaintext files automatically
792     */
793    public void setAddByteOrderMark(boolean addByteOrderMark) {
794
795        m_addByteOrderMark = addByteOrderMark;
796    }
797
798    /**
799     * Unlocks a resource.<p>
800     *
801     * Iterates through all configured resource wrappers till the first returns <code>true</code>.<p>
802     *
803     * @see I_CmsResourceWrapper#unlockResource(CmsObject, String)
804     * @see CmsObject#unlockResource(String)
805     *
806     * @param resourcename the name of the resource to unlock (full path)
807     *
808     * @throws CmsException if something goes wrong
809     */
810    public void unlockResource(String resourcename) throws CmsException {
811
812        boolean exec = false;
813
814        // iterate through all wrappers and call "lockResource" till one does not return false
815        List<I_CmsResourceWrapper> wrappers = getWrappers();
816        Iterator<I_CmsResourceWrapper> iter = wrappers.iterator();
817        while (iter.hasNext()) {
818            I_CmsResourceWrapper wrapper = iter.next();
819            exec = wrapper.unlockResource(m_cms, resourcename);
820            if (exec) {
821                break;
822            }
823        }
824
825        // delegate the call to the CmsObject
826        if (!exec) {
827            m_cms.unlockResource(resourcename);
828        }
829    }
830
831    /**
832     * Writes a resource to the OpenCms VFS, including it's content.<p>
833     *
834     * Iterates through all configured resource wrappers till the first returns not <code>null</code>.<p>
835     *
836     * @see I_CmsResourceWrapper#writeFile(CmsObject, CmsFile)
837     * @see CmsObject#writeFile(CmsFile)
838     *
839     * @param resource the resource to write
840     *
841     * @return the written resource (may have been modified)
842     *
843     * @throws CmsException if something goes wrong
844     */
845    public CmsFile writeFile(CmsFile resource) throws CmsException {
846
847        CmsFile res = null;
848
849        // remove the added UTF-8 marker
850        if (needUtf8Marker(resource)) {
851            resource.setContents(CmsResourceWrapperUtils.removeUtf8Marker(resource.getContents()));
852        }
853
854        String resourcename = m_cms.getSitePath(resource);
855        if (!m_cms.existsResource(resourcename)) {
856
857            // iterate through all wrappers and call "writeFile" till one does not return null
858            List<I_CmsResourceWrapper> wrappers = getWrappers();
859            Iterator<I_CmsResourceWrapper> iter = wrappers.iterator();
860            while (iter.hasNext()) {
861                I_CmsResourceWrapper wrapper = iter.next();
862                res = wrapper.writeFile(m_cms, resource);
863                if (res != null) {
864                    break;
865                }
866            }
867
868            // delegate the call to the CmsObject
869            if (res == null) {
870                res = m_cms.writeFile(resource);
871            }
872        } else {
873            res = m_cms.writeFile(resource);
874        }
875
876        return res;
877    }
878
879    /**
880     * Writes properties to the resource with the given path, if it exists
881     *
882     * @param path the path
883     * @param props the properties to write
884     *
885     * @throws CmsException if something goes wrong
886     */
887    public void writeProperties(String path, Map<String, CmsProperty> props) throws CmsException {
888
889        if (m_cms.existsResource(path, CmsResourceFilter.IGNORE_EXPIRATION)) {
890            m_cms.writePropertyObjects(path, new ArrayList<>(props.values()));
891        }
892
893    }
894
895    /**
896     * Try to find a resource type wrapper for the resource.<p>
897     *
898     * Takes all configured resource type wrappers and ask if one of them is responsible
899     * for that resource. The first in the list which feels responsible is returned.
900     * If no wrapper could be found null will be returned.<p>
901     *
902     * @see I_CmsResourceWrapper#isWrappedResource(CmsObject, CmsResource)
903     *
904     * @param res the resource to find a resource type wrapper for
905     *
906     * @return the found resource type wrapper for the resource or null if not found
907     */
908    private I_CmsResourceWrapper getResourceTypeWrapper(CmsResource res) {
909
910        Iterator<I_CmsResourceWrapper> iter = getWrappers().iterator();
911        while (iter.hasNext()) {
912            I_CmsResourceWrapper wrapper = iter.next();
913
914            if (wrapper.isWrappedResource(m_cms, res)) {
915                return wrapper;
916            }
917        }
918
919        return null;
920    }
921
922    /**
923     * Checks if the resource type needs an UTF-8 marker.<p>
924     *
925     * If the encoding of the resource is "UTF-8" and the resource
926     * type is one of the following:<br/>
927     * <ul>
928     * <li>{@link CmsResourceTypeJsp}</li>
929     * <li>{@link CmsResourceTypePlain}</li>
930     * <li>{@link CmsResourceTypeXmlContent}</li>
931     * <li>{@link CmsResourceTypeXmlPage}</li>
932     * </ul>
933     *
934     * it needs an UTF-8 marker.<p>
935     *
936     * @param res the resource to check if the content needs a UTF-8 marker
937     *
938     * @return <code>true</code> if the resource needs an UTF-8 maker otherwise <code>false</code>
939     */
940    private boolean needUtf8Marker(CmsResource res) {
941
942        if (!m_addByteOrderMark) {
943            return false;
944        }
945        // if the encoding of the resource is not UTF-8 return false
946        String encoding = CmsLocaleManager.getResourceEncoding(m_cms, res);
947        boolean result = false;
948        if (CmsEncoder.ENCODING_UTF_8.equals(encoding)) {
949            try {
950                I_CmsResourceType resType = OpenCms.getResourceManager().getResourceType(res.getTypeId());
951                if (resType instanceof CmsResourceTypeJsp) {
952                    result = true;
953                } else if (resType instanceof CmsResourceTypePlain) {
954                    result = true;
955                } else if (resType instanceof CmsResourceTypeXmlContent) {
956                    result = true;
957                } else if (resType instanceof CmsResourceTypeXmlPage) {
958                    result = true;
959                }
960            } catch (CmsLoaderException e) {
961                LOG.debug(e.getLocalizedMessage(), e);
962            }
963        }
964        return result;
965    }
966
967    /**
968     * Checks if the file content already contains the UTF8 marker.<p>
969     *
970     * @param res the resource to check
971     *
972     * @return <code>true</code> if the file content already contains the UTF8 marker
973     */
974    private boolean startsWithUtf8Marker(CmsResource res) {
975
976        boolean result = false;
977        try {
978            if (res.isFile()) {
979                CmsFile file = m_cms.readFile(res);
980                if ((file.getContents().length >= 3)
981                    && (file.getContents()[0] == CmsResourceWrapperUtils.UTF8_MARKER[0])
982                    && (file.getContents()[1] == CmsResourceWrapperUtils.UTF8_MARKER[1])
983                    && (file.getContents()[2] == CmsResourceWrapperUtils.UTF8_MARKER[2])) {
984                    result = true;
985                }
986            }
987        } catch (CmsException e) {
988            LOG.debug(e.getLocalizedMessage(), e);
989        }
990        return result;
991    }
992}