001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (https://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: https://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: https://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)
158    throws CmsException, CmsIllegalArgumentException {
159
160        CmsResource res = getResource(cms, resourcename, CmsResourceFilter.DEFAULT);
161        if (res != null) {
162
163            // cut off trailing slash
164            if (resourcename.endsWith("/")) {
165                resourcename = resourcename.substring(0, resourcename.length() - 1);
166            }
167
168            // check "existance" of resource
169            if (existsResource(res)) {
170
171                throw new CmsVfsResourceAlreadyExistsException(
172                    org.opencms.db.generic.Messages.get().container(
173                        org.opencms.db.generic.Messages.ERR_RESOURCE_WITH_NAME_ALREADY_EXISTS_1,
174                        resourcename));
175            }
176
177            // mark file as created in tmp file table
178            TMP_FILE_TABLE.add(res.getRootPath());
179
180            // lock the resource because this is the expected behavior
181            cms.lockResource(cms.getRequestContext().removeSiteRoot(res.getRootPath()));
182
183            if (resourcename.endsWith(PROPERTY_DIR)) {
184
185                CmsWrappedResource wrap = new CmsWrappedResource(res);
186                wrap.setRootPath(res.getRootPath() + PROPERTY_DIR + "/");
187                wrap.setFolder(true);
188                return wrap.getResource();
189            } else if (resourcename.endsWith(CmsResourceWrapperUtils.EXTENSION_PROPERTIES)) {
190
191                CmsResourceWrapperUtils.writePropertyFile(
192                    cms,
193                    cms.getRequestContext().removeSiteRoot(res.getRootPath()),
194                    content);
195                return res;
196            }
197
198        }
199
200        return null;
201    }
202
203    /**
204     * @see org.opencms.file.wrapper.A_CmsResourceWrapper#deleteResource(org.opencms.file.CmsObject, java.lang.String, org.opencms.file.CmsResource.CmsResourceDeleteMode)
205     */
206    @Override
207    public boolean deleteResource(CmsObject cms, String resourcename, CmsResourceDeleteMode siblingMode)
208    throws CmsException {
209
210        CmsResource res = getResource(cms, resourcename, CmsResourceFilter.DEFAULT);
211        if (res != null) {
212            TMP_FILE_TABLE.remove(res.getRootPath());
213            return true;
214        }
215
216        return false;
217    }
218
219    /**
220     * @see org.opencms.file.wrapper.A_CmsResourceWrapper#getLock(org.opencms.file.CmsObject, org.opencms.file.CmsResource)
221     */
222    @Override
223    public CmsLock getLock(CmsObject cms, CmsResource resource) throws CmsException {
224
225        CmsResource org = getResource(cms, resource.getStructureId());
226        if (org != null) {
227            return cms.getLock(org);
228        }
229        return null;
230    }
231
232    /**
233     * @see org.opencms.file.wrapper.I_CmsResourceWrapper#isWrappedResource(org.opencms.file.CmsObject, org.opencms.file.CmsResource)
234     */
235    public boolean isWrappedResource(CmsObject cms, CmsResource res) {
236
237        String path = res.getRootPath();
238        if (path.endsWith("/")) {
239            path = path.substring(0, path.length() - 1);
240        }
241
242        if (path.endsWith(PROPERTY_DIR)) {
243            return true;
244        }
245
246        return false;
247    }
248
249    /**
250     * @see org.opencms.file.wrapper.A_CmsResourceWrapper#lockResource(org.opencms.file.CmsObject, java.lang.String, boolean)
251     */
252    @Override
253    public boolean lockResource(CmsObject cms, String resourcename, boolean temporary) throws CmsException {
254
255        CmsResource res = getResource(cms, resourcename, CmsResourceFilter.DEFAULT);
256        if (res != null) {
257            String path = cms.getRequestContext().removeSiteRoot(res.getRootPath());
258            if (temporary) {
259                cms.lockResourceTemporary(path);
260            } else {
261                cms.lockResource(path);
262            }
263            return true;
264        }
265
266        return false;
267    }
268
269    /**
270     * @see org.opencms.file.wrapper.A_CmsResourceWrapper#readFile(org.opencms.file.CmsObject, java.lang.String, org.opencms.file.CmsResourceFilter)
271     */
272    @Override
273    public CmsFile readFile(CmsObject cms, String resourcename, CmsResourceFilter filter) throws CmsException {
274
275        if (!resourcename.endsWith(PROPERTY_DIR)) {
276            CmsResource res = getResource(cms, resourcename, filter);
277            if (res != null) {
278
279                // Workaround for Dreamweaver:
280                // Dreamweaver copies folders through creating folder for folder and file for file.
281                // So there is first a call if the property folder already exists and afterwards create
282                // it. If the folder already exists, the copy action fails.
283                // In the first time after creating a folder, the property dir does not exists until it is
284                // created or the time expired.
285                if (!existsResource(res)) {
286                    return null;
287                }
288
289                return CmsResourceWrapperUtils.createPropertyFile(cms, res, getPropertyFileName(res));
290            }
291        }
292
293        return null;
294    }
295
296    /**
297     * @see org.opencms.file.wrapper.A_CmsResourceWrapper#readResource(org.opencms.file.CmsObject, java.lang.String, org.opencms.file.CmsResourceFilter)
298     */
299    @Override
300    public CmsResource readResource(CmsObject cms, String resourcename, CmsResourceFilter filter) throws CmsException {
301
302        CmsResource res = getResource(cms, resourcename, filter);
303        if (res != null) {
304
305            // Workaround for Dreamweaver:
306            // Dreamweaver copies folders through creating folder for folder and file for file.
307            // So there is first a call if the property folder already exists and afterwards create
308            // it. If the folder already exists, the copy action fails.
309            // In the first time after creating a folder, the property dir does not exists until it is
310            // created or the time expired.
311            if (!existsResource(res)) {
312                return null;
313            }
314
315            // cut off trailing slash
316            if (resourcename.endsWith("/")) {
317                resourcename = resourcename.substring(0, resourcename.length() - 1);
318            }
319
320            // create property file and return the resource for it
321            if (!resourcename.endsWith(PROPERTY_DIR)) {
322                return CmsResourceWrapperUtils.createPropertyFile(cms, res, getPropertyFileName(res));
323            }
324
325            // create a resource for the __property folder
326            CmsWrappedResource wrap = new CmsWrappedResource(res);
327            wrap.setRootPath(res.getRootPath() + PROPERTY_DIR);
328            wrap.setFolder(true);
329            return wrap.getResource();
330        }
331
332        return null;
333    }
334
335    /**
336     * @see org.opencms.file.wrapper.A_CmsResourceWrapper#restoreLink(org.opencms.file.CmsObject, java.lang.String)
337     */
338    @Override
339    public String restoreLink(CmsObject cms, String uri) {
340
341        try {
342            CmsResource res = getResource(cms, uri, CmsResourceFilter.DEFAULT);
343            if (res != null) {
344                return res.getRootPath();
345            }
346        } catch (CmsException ex) {
347            // noop
348        }
349
350        return null;
351    }
352
353    /**
354     * @see org.opencms.file.wrapper.A_CmsResourceWrapper#unlockResource(org.opencms.file.CmsObject, java.lang.String)
355     */
356    @Override
357    public boolean unlockResource(CmsObject cms, String resourcename) throws CmsException {
358
359        CmsResource res = getResource(cms, resourcename, CmsResourceFilter.DEFAULT);
360        if (res != null) {
361            cms.unlockResource(cms.getRequestContext().removeSiteRoot(res.getRootPath()));
362            return true;
363        }
364
365        return false;
366    }
367
368    /**
369     * @see org.opencms.file.wrapper.A_CmsResourceWrapper#writeFile(org.opencms.file.CmsObject, org.opencms.file.CmsFile)
370     */
371    @Override
372    public CmsFile writeFile(CmsObject cms, CmsFile resource) throws CmsException {
373
374        CmsResource res = getResource(cms, resource.getStructureId());
375        if (res != null) {
376            CmsResourceWrapperUtils.writePropertyFile(
377                cms,
378                cms.getRequestContext().removeSiteRoot(res.getRootPath()),
379                resource.getContents());
380            return resource;
381        }
382
383        return null;
384    }
385
386    /**
387     * 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>
388     *
389     * @param cms the CmsObject to use
390     * @param structureId the structure id of the resource
391     * @return the resource which has been read
392     */
393    CmsResource getResource(CmsObject cms, CmsUUID structureId) {
394
395        try {
396            CmsResource result = cms.readResource(structureId);
397            return result;
398        } catch (CmsVfsResourceNotFoundException e) {
399            return null;
400        } catch (CmsException e) {
401            LOG.error(e.getLocalizedMessage(), e);
402            return null;
403        }
404    }
405
406    /**
407     * Checks if a resource exists depending on the creation date and the temp files saved.<p>
408     *
409     * Dreamweaver copies folders through creating folder for folder and file for file.
410     * So there is first a call if the property folder already exists and afterwards create
411     * it. If the folder already exists, the copy action fails.
412     * In the first time after creating a folder, the property dir does not exists until it is
413     * created or the time expired.<p>
414     *
415     * @param res the resource to check if it exists
416     *
417     * @return return if the folder exists otherwise false
418     */
419    private boolean existsResource(CmsResource res) {
420
421        long now = new Date().getTime();
422        long created = res.getDateCreated();
423        long diff = (now - created) / 1000;
424
425        if (diff <= TIME_DELAY) {
426
427            // check tmp file table
428            if (TMP_FILE_TABLE.contains(res.getRootPath())) {
429                return true;
430            }
431
432            return false;
433        } else {
434
435            // remove from tmp file table
436            TMP_FILE_TABLE.remove(res.getRootPath());
437        }
438
439        return true;
440    }
441
442    /**
443     * Creates the full path to the property file of the given resource.<p>
444     *
445     * @param res the resource where to create the path for the property file for
446     *
447     * @return the full path to the property file of the resource
448     */
449    private String getPropertyFileName(CmsResource res) {
450
451        StringBuffer ret = new StringBuffer();
452
453        // path to the parent folder
454        String parentFolder = CmsResource.getParentFolder(res.getRootPath());
455        ret.append(parentFolder);
456
457        // make sure ends with a slash
458        if (!parentFolder.endsWith("/")) {
459            ret.append("/");
460        }
461
462        // append name of the property folder
463        ret.append(PROPERTY_DIR);
464        ret.append("/");
465
466        // if resource is a folder add the prefix "__"
467        if (res.isFolder()) {
468            ret.append(FOLDER_PREFIX);
469        }
470
471        // append the name of the resource
472        ret.append(res.getName());
473
474        return ret.toString();
475    }
476
477    /**
478     * Reads the resource for the property file.<p>
479     *
480     * @param cms the initialized CmsObject
481     * @param resourcename the name of the property resource
482     * @param filter the filter to use
483     *
484     * @return the resource for the property file or null if not found
485     *
486     * @throws CmsException if something goes wrong
487     */
488    private CmsResource getResource(CmsObject cms, String resourcename, CmsResourceFilter filter) throws CmsException {
489
490        // the path without trailing slash
491        String path = CmsResource.getParentFolder(resourcename);
492        if (path == null) {
493            return null;
494        }
495
496        // the parent path
497        String parent = CmsResource.getParentFolder(path);
498
499        // the name of the resource
500        String name = CmsResource.getName(resourcename);
501        if (name.endsWith("/")) {
502            name = name.substring(0, name.length() - 1);
503        }
504
505        // read the resource for the property dir
506        if (name.equals(PROPERTY_DIR)) {
507
508            return cms.readResource(path, filter);
509        }
510
511        if ((path.endsWith(PROPERTY_DIR + "/")) && (name.endsWith(CmsResourceWrapperUtils.EXTENSION_PROPERTIES))) {
512            CmsResource res = null;
513
514            if (name.startsWith(FOLDER_PREFIX)) {
515                name = name.substring(2);
516            }
517
518            try {
519                String resPath = CmsResourceWrapperUtils.removeFileExtension(
520                    cms,
521                    parent + name,
522                    CmsResourceWrapperUtils.EXTENSION_PROPERTIES);
523
524                res = cms.readResource(resPath, filter);
525            } catch (CmsException ex) {
526                // noop
527            }
528
529            return res;
530        }
531
532        return null;
533    }
534
535}