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.CmsResource;
034import org.opencms.file.CmsResource.CmsResourceDeleteMode;
035import org.opencms.file.CmsResourceFilter;
036import org.opencms.file.CmsVfsResourceAlreadyExistsException;
037import org.opencms.file.CmsVfsResourceNotFoundException;
038import org.opencms.lock.CmsLock;
039import org.opencms.main.CmsException;
040import org.opencms.main.CmsIllegalArgumentException;
041import org.opencms.main.CmsLog;
042import org.opencms.util.CmsUUID;
043
044import java.util.ArrayList;
045import java.util.Date;
046import java.util.Iterator;
047import java.util.List;
048
049import org.apache.commons.logging.Log;
050
051/**
052 * Adds a folder in every existing folder with the name "__properties" which
053 * contains property files for every resource in the existing folder.<p>
054 *
055 * Empty folders don't have the property folder visible.<p>
056 *
057 * The names of the property files are the same as the resource they belong to
058 * with the extension "properties". To keep the correct sorting the names of
059 * folders gets additionaly the prefix "__" to keep them at the beginning of the
060 * list.<p>
061 *
062 * When creating new folders, the property folder gets visible after a time period
063 * of 60 seconds. For new resources the property file appears after that period too.
064 * In this time period it is possible to create the property folder and the property
065 * files manually. The properties in the created property files will be set at the
066 * resource they belong to.<p>
067 *
068 * @since 6.5.6
069 */
070public class CmsResourceWrapperPropertyFile extends A_CmsResourceWrapper {
071
072    /** The logger instance for this class. */
073    private static final Log LOG = CmsLog.getLog(CmsResourceWrapperPropertyFile.class);
074
075    /** The prefix for folders to keep correct sorting. */
076    private static final String FOLDER_PREFIX = "__";
077
078    /** The name to use for the folder where all property files are listed in. */
079    private static final String PROPERTY_DIR = "__properties";
080
081    /** The time in seconds to wait till the properties (and the property folder) are visible. */
082    private static final int TIME_DELAY = 60;
083
084    /** Table with the states of the virtual files. */
085    private static final List<String> TMP_FILE_TABLE = new ArrayList<String>();
086
087    /**
088     * @see org.opencms.file.wrapper.A_CmsResourceWrapper#addResourcesToFolder(CmsObject, String, CmsResourceFilter)
089     */
090    @Override
091    public List<CmsResource> addResourcesToFolder(CmsObject cms, String resourcename, CmsResourceFilter filter)
092    throws CmsException {
093
094        String path = resourcename;
095        if (!path.endsWith("/")) {
096            path += "/";
097        }
098
099        if (path.endsWith(PROPERTY_DIR + "/")) {
100
101            String parent = CmsResource.getParentFolder(path);
102            List<CmsResource> ret = new ArrayList<CmsResource>();
103
104            // Iterate through all existing resources
105            List<CmsResource> resources = cms.getResourcesInFolder(parent, filter);
106            Iterator<CmsResource> iter = resources.iterator();
107            while (iter.hasNext()) {
108                CmsResource res = iter.next();
109
110                // check "existance" of resource
111                if (existsResource(res)) {
112
113                    // add the generated property file
114                    ret.add(CmsResourceWrapperUtils.createPropertyFile(cms, res, getPropertyFileName(res)));
115                }
116            }
117
118            return ret;
119        } else {
120
121            try {
122                CmsResource folder = cms.readResource(resourcename);
123                if (folder.isFolder()) {
124
125                    // check if folder is empty
126                    if (!cms.getResourcesInFolder(resourcename, CmsResourceFilter.DEFAULT).isEmpty()) {
127
128                        // check "existance" of folder
129                        if (existsResource(folder)) {
130                            List<CmsResource> ret = new ArrayList<CmsResource>();
131
132                            CmsWrappedResource wrap = new CmsWrappedResource(folder);
133                            wrap.setRootPath(folder.getRootPath() + PROPERTY_DIR + "/");
134
135                            ret.add(wrap.getResource());
136                            return ret;
137                        }
138                    }
139                }
140            } catch (CmsVfsResourceNotFoundException ex) {
141                return null;
142            }
143        }
144
145        return null;
146    }
147
148    /**
149     * @see org.opencms.file.wrapper.A_CmsResourceWrapper#createResource(org.opencms.file.CmsObject, java.lang.String, int, byte[], java.util.List)
150     */
151    @Override
152    public CmsResource createResource(
153        CmsObject cms,
154        String resourcename,
155        int type,
156        byte[] content,
157        List<CmsProperty> properties) throws CmsException, CmsIllegalArgumentException {
158
159        CmsResource res = getResource(cms, resourcename, CmsResourceFilter.DEFAULT);
160        if (res != null) {
161
162            // cut off trailing slash
163            if (resourcename.endsWith("/")) {
164                resourcename = resourcename.substring(0, resourcename.length() - 1);
165            }
166
167            // check "existance" of resource
168            if (existsResource(res)) {
169
170                throw new CmsVfsResourceAlreadyExistsException(org.opencms.db.generic.Messages.get().container(
171                    org.opencms.db.generic.Messages.ERR_RESOURCE_WITH_NAME_ALREADY_EXISTS_1,
172                    resourcename));
173            }
174
175            // mark file as created in tmp file table
176            TMP_FILE_TABLE.add(res.getRootPath());
177
178            // lock the resource because this is the expected behavior
179            cms.lockResource(cms.getRequestContext().removeSiteRoot(res.getRootPath()));
180
181            if (resourcename.endsWith(PROPERTY_DIR)) {
182
183                CmsWrappedResource wrap = new CmsWrappedResource(res);
184                wrap.setRootPath(res.getRootPath() + PROPERTY_DIR + "/");
185                wrap.setFolder(true);
186                return wrap.getResource();
187            } else if (resourcename.endsWith(CmsResourceWrapperUtils.EXTENSION_PROPERTIES)) {
188
189                CmsResourceWrapperUtils.writePropertyFile(
190                    cms,
191                    cms.getRequestContext().removeSiteRoot(res.getRootPath()),
192                    content);
193                return res;
194            }
195
196        }
197
198        return null;
199    }
200
201    /**
202     * @see org.opencms.file.wrapper.A_CmsResourceWrapper#deleteResource(org.opencms.file.CmsObject, java.lang.String, org.opencms.file.CmsResource.CmsResourceDeleteMode)
203     */
204    @Override
205    public boolean deleteResource(CmsObject cms, String resourcename, CmsResourceDeleteMode siblingMode)
206    throws CmsException {
207
208        CmsResource res = getResource(cms, resourcename, CmsResourceFilter.DEFAULT);
209        if (res != null) {
210            TMP_FILE_TABLE.remove(res.getRootPath());
211            return true;
212        }
213
214        return false;
215    }
216
217    /**
218     * @see org.opencms.file.wrapper.A_CmsResourceWrapper#getLock(org.opencms.file.CmsObject, org.opencms.file.CmsResource)
219     */
220    @Override
221    public CmsLock getLock(CmsObject cms, CmsResource resource) throws CmsException {
222
223        CmsResource org = getResource(cms, resource.getStructureId());
224        if (org != null) {
225            return cms.getLock(org);
226        }
227        return null;
228    }
229
230    /**
231     * @see org.opencms.file.wrapper.I_CmsResourceWrapper#isWrappedResource(org.opencms.file.CmsObject, org.opencms.file.CmsResource)
232     */
233    public boolean isWrappedResource(CmsObject cms, CmsResource res) {
234
235        String path = res.getRootPath();
236        if (path.endsWith("/")) {
237            path = path.substring(0, path.length() - 1);
238        }
239
240        if (path.endsWith(PROPERTY_DIR)) {
241            return true;
242        }
243
244        return false;
245    }
246
247    /**
248     * @see org.opencms.file.wrapper.A_CmsResourceWrapper#lockResource(org.opencms.file.CmsObject, java.lang.String, boolean)
249     */
250    @Override
251    public boolean lockResource(CmsObject cms, String resourcename, boolean temporary) throws CmsException {
252
253        CmsResource res = getResource(cms, resourcename, CmsResourceFilter.DEFAULT);
254        if (res != null) {
255            String path = cms.getRequestContext().removeSiteRoot(res.getRootPath());
256            if (temporary) {
257                cms.lockResourceTemporary(path);
258            } else {
259                cms.lockResource(path);
260            }
261            return true;
262        }
263
264        return false;
265    }
266
267    /**
268     * @see org.opencms.file.wrapper.A_CmsResourceWrapper#readFile(org.opencms.file.CmsObject, java.lang.String, org.opencms.file.CmsResourceFilter)
269     */
270    @Override
271    public CmsFile readFile(CmsObject cms, String resourcename, CmsResourceFilter filter) throws CmsException {
272
273        if (!resourcename.endsWith(PROPERTY_DIR)) {
274            CmsResource res = getResource(cms, resourcename, filter);
275            if (res != null) {
276
277                // Workaround for Dreamweaver:
278                // Dreamweaver copies folders through creating folder for folder and file for file.
279                // So there is first a call if the property folder already exists and afterwards create
280                // it. If the folder already exists, the copy action fails.
281                // In the first time after creating a folder, the property dir does not exists until it is
282                // created or the time expired.
283                if (!existsResource(res)) {
284                    return null;
285                }
286
287                return CmsResourceWrapperUtils.createPropertyFile(cms, res, getPropertyFileName(res));
288            }
289        }
290
291        return null;
292    }
293
294    /**
295     * @see org.opencms.file.wrapper.A_CmsResourceWrapper#readResource(org.opencms.file.CmsObject, java.lang.String, org.opencms.file.CmsResourceFilter)
296     */
297    @Override
298    public CmsResource readResource(CmsObject cms, String resourcename, CmsResourceFilter filter) throws CmsException {
299
300        CmsResource res = getResource(cms, resourcename, filter);
301        if (res != null) {
302
303            // Workaround for Dreamweaver:
304            // Dreamweaver copies folders through creating folder for folder and file for file.
305            // So there is first a call if the property folder already exists and afterwards create
306            // it. If the folder already exists, the copy action fails.
307            // In the first time after creating a folder, the property dir does not exists until it is
308            // created or the time expired.
309            if (!existsResource(res)) {
310                return null;
311            }
312
313            // cut off trailing slash
314            if (resourcename.endsWith("/")) {
315                resourcename = resourcename.substring(0, resourcename.length() - 1);
316            }
317
318            // create property file and return the resource for it
319            if (!resourcename.endsWith(PROPERTY_DIR)) {
320                return CmsResourceWrapperUtils.createPropertyFile(cms, res, getPropertyFileName(res));
321            }
322
323            // create a resource for the __property folder
324            CmsWrappedResource wrap = new CmsWrappedResource(res);
325            wrap.setRootPath(res.getRootPath() + PROPERTY_DIR);
326            wrap.setFolder(true);
327            return wrap.getResource();
328        }
329
330        return null;
331    }
332
333    /**
334     * @see org.opencms.file.wrapper.A_CmsResourceWrapper#restoreLink(org.opencms.file.CmsObject, java.lang.String)
335     */
336    @Override
337    public String restoreLink(CmsObject cms, String uri) {
338
339        try {
340            CmsResource res = getResource(cms, uri, CmsResourceFilter.DEFAULT);
341            if (res != null) {
342                return res.getRootPath();
343            }
344        } catch (CmsException ex) {
345            // noop
346        }
347
348        return null;
349    }
350
351    /**
352     * @see org.opencms.file.wrapper.A_CmsResourceWrapper#unlockResource(org.opencms.file.CmsObject, java.lang.String)
353     */
354    @Override
355    public boolean unlockResource(CmsObject cms, String resourcename) throws CmsException {
356
357        CmsResource res = getResource(cms, resourcename, CmsResourceFilter.DEFAULT);
358        if (res != null) {
359            cms.unlockResource(cms.getRequestContext().removeSiteRoot(res.getRootPath()));
360            return true;
361        }
362
363        return false;
364    }
365
366    /**
367     * @see org.opencms.file.wrapper.A_CmsResourceWrapper#writeFile(org.opencms.file.CmsObject, org.opencms.file.CmsFile)
368     */
369    @Override
370    public CmsFile writeFile(CmsObject cms, CmsFile resource) throws CmsException {
371
372        CmsResource res = getResource(cms, resource.getStructureId());
373        if (res != null) {
374            CmsResourceWrapperUtils.writePropertyFile(
375                cms,
376                cms.getRequestContext().removeSiteRoot(res.getRootPath()),
377                resource.getContents());
378            return resource;
379        }
380
381        return null;
382    }
383
384    /**
385     * Tries to the read the resource with the given structure id using the given CmsObject and returns it, or null if the resource can not be read.<p>
386     *
387     * @param cms the CmsObject to use
388     * @param structureId the structure id of the resource
389     * @return the resource which has been read
390     */
391    CmsResource getResource(CmsObject cms, CmsUUID structureId) {
392
393        try {
394            CmsResource result = cms.readResource(structureId);
395            return result;
396        } catch (CmsVfsResourceNotFoundException e) {
397            return null;
398        } catch (CmsException e) {
399            LOG.error(e.getLocalizedMessage(), e);
400            return null;
401        }
402    }
403
404    /**
405     * Checks if a resource exists depending on the creation date and the temp files saved.<p>
406     *
407     * Dreamweaver copies folders through creating folder for folder and file for file.
408     * So there is first a call if the property folder already exists and afterwards create
409     * it. If the folder already exists, the copy action fails.
410     * In the first time after creating a folder, the property dir does not exists until it is
411     * created or the time expired.<p>
412     *
413     * @param res the resource to check if it exists
414     *
415     * @return return if the folder exists otherwise false
416     */
417    private boolean existsResource(CmsResource res) {
418
419        long now = new Date().getTime();
420        long created = res.getDateCreated();
421        long diff = (now - created) / 1000;
422
423        if (diff <= TIME_DELAY) {
424
425            // check tmp file table
426            if (TMP_FILE_TABLE.contains(res.getRootPath())) {
427                return true;
428            }
429
430            return false;
431        } else {
432
433            // remove from tmp file table
434            TMP_FILE_TABLE.remove(res.getRootPath());
435        }
436
437        return true;
438    }
439
440    /**
441     * Creates the full path to the property file of the given resource.<p>
442     *
443     * @param res the resource where to create the path for the property file for
444     *
445     * @return the full path to the property file of the resource
446     */
447    private String getPropertyFileName(CmsResource res) {
448
449        StringBuffer ret = new StringBuffer();
450
451        // path to the parent folder
452        String parentFolder = CmsResource.getParentFolder(res.getRootPath());
453        ret.append(parentFolder);
454
455        // make sure ends with a slash
456        if (!parentFolder.endsWith("/")) {
457            ret.append("/");
458        }
459
460        // append name of the property folder
461        ret.append(PROPERTY_DIR);
462        ret.append("/");
463
464        // if resource is a folder add the prefix "__"
465        if (res.isFolder()) {
466            ret.append(FOLDER_PREFIX);
467        }
468
469        // append the name of the resource
470        ret.append(res.getName());
471
472        return ret.toString();
473    }
474
475    /**
476     * Reads the resource for the property file.<p>
477     *
478     * @param cms the initialized CmsObject
479     * @param resourcename the name of the property resource
480     * @param filter the filter to use
481     *
482     * @return the resource for the property file or null if not found
483     *
484     * @throws CmsException if something goes wrong
485     */
486    private CmsResource getResource(CmsObject cms, String resourcename, CmsResourceFilter filter) throws CmsException {
487
488        // the path without trailing slash
489        String path = CmsResource.getParentFolder(resourcename);
490        if (path == null) {
491            return null;
492        }
493
494        // the parent path
495        String parent = CmsResource.getParentFolder(path);
496
497        // the name of the resource
498        String name = CmsResource.getName(resourcename);
499        if (name.endsWith("/")) {
500            name = name.substring(0, name.length() - 1);
501        }
502
503        // read the resource for the property dir
504        if (name.equals(PROPERTY_DIR)) {
505
506            return cms.readResource(path, filter);
507        }
508
509        if ((path.endsWith(PROPERTY_DIR + "/")) && (name.endsWith(CmsResourceWrapperUtils.EXTENSION_PROPERTIES))) {
510            CmsResource res = null;
511
512            if (name.startsWith(FOLDER_PREFIX)) {
513                name = name.substring(2);
514            }
515
516            try {
517                String resPath = CmsResourceWrapperUtils.removeFileExtension(
518                    cms,
519                    parent + name,
520                    CmsResourceWrapperUtils.EXTENSION_PROPERTIES);
521
522                res = cms.readResource(resPath, filter);
523            } catch (CmsException ex) {
524                // noop
525            }
526
527            return res;
528        }
529
530        return null;
531    }
532
533}