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, 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.db.CmsResourceState;
031import org.opencms.file.CmsFile;
032import org.opencms.file.CmsObject;
033import org.opencms.file.CmsProject;
034import org.opencms.file.CmsProperty;
035import org.opencms.file.CmsResource;
036import org.opencms.file.CmsResourceFilter;
037import org.opencms.file.CmsVfsResourceNotFoundException;
038import org.opencms.file.types.CmsResourceTypeBinary;
039import org.opencms.file.types.CmsResourceTypeFolder;
040import org.opencms.jlan.CmsJlanDiskInterface;
041import org.opencms.loader.CmsLoaderException;
042import org.opencms.lock.CmsLock;
043import org.opencms.main.CmsException;
044import org.opencms.main.CmsIllegalArgumentException;
045import org.opencms.main.CmsLog;
046import org.opencms.main.OpenCms;
047import org.opencms.module.CmsModule;
048import org.opencms.security.CmsRole;
049import org.opencms.util.CmsFileUtil;
050import org.opencms.util.CmsStringUtil;
051import org.opencms.util.CmsUUID;
052
053import java.io.IOException;
054import java.util.Arrays;
055import java.util.Collections;
056import java.util.List;
057import java.util.Map;
058import java.util.concurrent.ConcurrentHashMap;
059
060import org.apache.commons.logging.Log;
061
062import com.google.common.collect.Lists;
063
064/**
065 * Resource wrapper used to import/export modules by copying them to/from virtual folders.<p>
066 */
067public class CmsResourceWrapperModules extends A_CmsResourceWrapper {
068
069    /** The logger instance to use for this class. */
070    private static final Log LOG = CmsLog.getLog(CmsResourceWrapperModules.class);
071
072    /** The base folder under which the virtual resources from this resource wrapper are available. */
073    public static final String BASE_PATH = "/modules";
074
075    /** The virtual folder which can be used to import modules. */
076    public static final String IMPORT_PATH = BASE_PATH + "/import";
077
078    /** The virtual folder which can be used to export modules. */
079    public static final String EXPORT_PATH = BASE_PATH + "/export";
080
081    /** The virtual folder which can be used to provide logs for module operations. */
082    public static final String LOG_PATH = BASE_PATH + "/log";
083
084    /** List of virtual folders made available by this resource wrapper. */
085    public static final List<String> FOLDERS = Collections.unmodifiableList(
086        Arrays.asList(BASE_PATH, IMPORT_PATH, EXPORT_PATH, LOG_PATH));
087
088    /** Cache for imported module files. */
089    private Map<String, CmsFile> m_importDataCache = new ConcurrentHashMap<String, CmsFile>();
090
091    /**
092     * Map containing the last update time for a given import folder path.<p>
093     *
094     * Why do we need this if we just want to write files in the import folder and not read them?
095     * The reason is that when using this wrapper with the JLAN CIFS connector, some clients check
096     * on the status of the import file before they write any data to it, and fail mysteriously if it isn't found,
097     * so we have to pretend that the file actually exists after creating it.
098     **/
099    ConcurrentHashMap<String, Long> m_importFileUpdateCache = new ConcurrentHashMap<String, Long>();
100
101    /**
102     * @see org.opencms.file.wrapper.A_CmsResourceWrapper#addResourcesToFolder(org.opencms.file.CmsObject, java.lang.String, org.opencms.file.CmsResourceFilter)
103     */
104    @Override
105    public List<CmsResource> addResourcesToFolder(CmsObject cms, String resourcename, CmsResourceFilter filter)
106    throws CmsException {
107
108        if (checkAccess(cms)) {
109            String resourceNameWithTrailingSlash = CmsStringUtil.joinPaths(resourcename, "/");
110            if (matchPath("/", resourceNameWithTrailingSlash)) {
111                return getVirtualResourcesForRoot(cms);
112            } else if (matchPath(BASE_PATH, resourceNameWithTrailingSlash)) {
113                return getVirtualResourcesForBasePath(cms);
114            } else if (matchPath(EXPORT_PATH, resourceNameWithTrailingSlash)) {
115                return getVirtualResourcesForExport(cms);
116            } else if (matchPath(IMPORT_PATH, resourceNameWithTrailingSlash)) {
117                return getVirtualResourcesForImport(cms);
118            } else if (matchPath(LOG_PATH, resourceNameWithTrailingSlash)) {
119                return getVirtualLogResources(cms);
120            }
121        }
122
123        return Collections.emptyList();
124    }
125
126    /**
127     * @see org.opencms.file.wrapper.A_CmsResourceWrapper#createResource(org.opencms.file.CmsObject, java.lang.String, int, byte[], java.util.List)
128     */
129    @Override
130    public CmsResource createResource(
131        CmsObject cms,
132        String resourcename,
133        int type,
134        byte[] content,
135        List<CmsProperty> properties)
136    throws CmsException, CmsIllegalArgumentException {
137
138        if (checkAccess(cms) && matchParentPath(IMPORT_PATH, resourcename)) {
139            CmsResource res = createFakeBinaryFile(resourcename, 0);
140            CmsFile file = new CmsFile(res);
141            file.setContents(content);
142            OpenCms.getModuleManager().getImportExportRepository().importModule(
143                CmsResource.getName(resourcename),
144                content);
145            m_importFileUpdateCache.put(resourcename, Long.valueOf(System.currentTimeMillis()));
146            return file;
147        } else {
148            return super.createResource(cms, resourcename, type, content, properties);
149        }
150    }
151
152    /**
153     * @see org.opencms.file.wrapper.A_CmsResourceWrapper#deleteResource(org.opencms.file.CmsObject, java.lang.String, org.opencms.file.CmsResource.CmsResourceDeleteMode)
154     */
155    @Override
156    public boolean deleteResource(CmsObject cms, String resourcename, CmsResource.CmsResourceDeleteMode siblingMode)
157    throws CmsException {
158
159        if (checkAccess(cms) && matchParentPath(EXPORT_PATH, resourcename)) {
160            String fileName = CmsResource.getName(resourcename);
161            boolean result = OpenCms.getModuleManager().getImportExportRepository().deleteModule(fileName);
162            return result;
163        } else {
164            return false;
165        }
166    }
167
168    /**
169     * @see org.opencms.file.wrapper.A_CmsResourceWrapper#getLock(org.opencms.file.CmsObject, org.opencms.file.CmsResource)
170     */
171    @Override
172    public CmsLock getLock(CmsObject cms, CmsResource resource) throws CmsException {
173
174        if (isFakePath(resource.getRootPath())) {
175            return CmsLock.getNullLock();
176        } else {
177            return super.getLock(cms, resource);
178        }
179    }
180
181    /**
182     * @see org.opencms.file.wrapper.I_CmsResourceWrapper#isWrappedResource(org.opencms.file.CmsObject, org.opencms.file.CmsResource)
183     */
184    public boolean isWrappedResource(CmsObject cms, CmsResource res) {
185
186        return CmsStringUtil.isPrefixPath(BASE_PATH, res.getRootPath());
187    }
188
189    /**
190     * @see org.opencms.file.wrapper.A_CmsResourceWrapper#lockResource(org.opencms.file.CmsObject, java.lang.String, boolean)
191     */
192    @Override
193    public boolean lockResource(CmsObject cms, String resourcename, boolean temporary) {
194
195        return isFakePath(resourcename);
196    }
197
198    /**
199     * @see org.opencms.file.wrapper.A_CmsResourceWrapper#readFile(org.opencms.file.CmsObject, java.lang.String, org.opencms.file.CmsResourceFilter)
200     */
201    @Override
202    public CmsFile readFile(CmsObject cms, String resourcename, CmsResourceFilter filter) throws CmsException {
203
204        // this method isn't actually called when using the JLAN repository, because readResource already returns a CmsFile when needed
205        cms.getRequestContext().removeAttribute(CmsJlanDiskInterface.NO_FILESIZE_REQUIRED);
206
207        CmsResource res = readResource(cms, resourcename, filter);
208        return (CmsFile)res;
209
210    }
211
212    /**
213     * @see org.opencms.file.wrapper.A_CmsResourceWrapper#readResource(org.opencms.file.CmsObject, java.lang.String, org.opencms.file.CmsResourceFilter)
214     */
215    @Override
216    public CmsResource readResource(CmsObject cms, String resourcepath, CmsResourceFilter filter) throws CmsException {
217
218        if (resourcepath.endsWith("desktop.ini")) {
219            return null;
220        }
221
222        if (checkAccess(cms)) {
223            for (String folder : FOLDERS) {
224                if (matchPath(resourcepath, folder)) {
225                    return createFakeFolder(folder);
226                }
227            }
228
229            if (matchParentPath(IMPORT_PATH, resourcepath)) {
230                if (hasImportFile(resourcepath)) {
231                    CmsFile importData = m_importDataCache.get(resourcepath);
232                    if (importData != null) {
233                        return importData;
234                    }
235                    return new CmsFile(createFakeBinaryFile(resourcepath));
236                }
237            }
238
239            if (matchParentPath(EXPORT_PATH, resourcepath)) {
240                CmsFile resultFile = new CmsFile(createFakeBinaryFile(resourcepath));
241                if (cms.getRequestContext().getAttribute(CmsJlanDiskInterface.NO_FILESIZE_REQUIRED) == null) {
242                    // we *do* require the file size, so we need to get the module data
243                    LOG.info("Getting data for " + resourcepath);
244                    byte[] data = OpenCms.getModuleManager().getImportExportRepository().getExportedModuleData(
245                        CmsResource.getName(resourcepath),
246                        cms.getRequestContext().getCurrentProject());
247                    resultFile.setContents(data);
248                }
249                return resultFile;
250            }
251
252            if (matchParentPath(LOG_PATH, resourcepath)) {
253                CmsFile resultFile = new CmsFile(createFakeBinaryFile(resourcepath));
254                // if (cms.getRequestContext().getAttribute(CmsJlanDiskInterface.NO_FILESIZE_REQUIRED) == null) {
255                String moduleName = CmsResource.getName(resourcepath).replaceFirst("\\.log$", "");
256                try {
257                    byte[] data = OpenCms.getModuleManager().getImportExportRepository().getModuleLog().readLog(
258                        moduleName);
259                    resultFile.setContents(data);
260                    return resultFile;
261                } catch (IOException e) {
262                    throw new CmsVfsResourceNotFoundException(
263                        org.opencms.db.Messages.get().container(
264                            org.opencms.db.Messages.ERR_READ_RESOURCE_1,
265                            resourcepath),
266                        e);
267                }
268            }
269        }
270        return super.readResource(cms, resourcepath, filter);
271    }
272
273    /**
274     * @see org.opencms.file.wrapper.A_CmsResourceWrapper#unlockResource(org.opencms.file.CmsObject, java.lang.String)
275     */
276    @Override
277    public boolean unlockResource(CmsObject cms, String resourcename) {
278
279        return isFakePath(resourcename);
280    }
281
282    /**
283     * @see org.opencms.file.wrapper.A_CmsResourceWrapper#writeFile(org.opencms.file.CmsObject, org.opencms.file.CmsFile)
284     */
285    @Override
286    public CmsFile writeFile(CmsObject cms, CmsFile resource) throws CmsException {
287
288        if (checkAccess(cms) && matchParentPath(IMPORT_PATH, resource.getRootPath())) {
289            OpenCms.getModuleManager().getImportExportRepository().importModule(
290                CmsResource.getName(resource.getRootPath()),
291                resource.getContents());
292            m_importFileUpdateCache.put(resource.getRootPath(), Long.valueOf(System.currentTimeMillis()));
293            m_importDataCache.put(resource.getRootPath(), resource);
294            return resource;
295        } else {
296            return super.writeFile(cms, resource);
297        }
298    }
299
300    /**
301     * Creates a fake CmsResource of type 'binary'.<p>
302     *
303     * @param rootPath  the root path
304     *
305     * @return the fake resource
306     *
307     * @throws CmsLoaderException if the binary type is missing
308     */
309    protected CmsResource createFakeBinaryFile(String rootPath) throws CmsLoaderException {
310
311        return createFakeBinaryFile(rootPath, 0);
312    }
313
314    /**
315     * Creates a fake CmsResource of type 'binary'.<p>
316     *
317     * @param rootPath  the root path
318     * @param dateLastModified the last modification date to use
319     *
320     * @return the fake resource
321     *
322     * @throws CmsLoaderException if the binary type is missing
323     */
324    protected CmsResource createFakeBinaryFile(String rootPath, long dateLastModified) throws CmsLoaderException {
325
326        CmsUUID structureId = CmsUUID.getConstantUUID("s-" + rootPath);
327        CmsUUID resourceId = CmsUUID.getConstantUUID("r-" + rootPath);
328        @SuppressWarnings("deprecation")
329        int type = OpenCms.getResourceManager().getResourceType(CmsResourceTypeBinary.getStaticTypeName()).getTypeId();
330        boolean isFolder = false;
331        int flags = 0;
332        CmsUUID projectId = CmsProject.ONLINE_PROJECT_ID;
333        CmsResourceState state = CmsResource.STATE_UNCHANGED;
334        long dateCreated = 0;
335        long dateReleased = 1;
336        long dateContent = 1;
337        int version = 0;
338
339        CmsUUID userCreated = CmsUUID.getNullUUID();
340        CmsUUID userLastModified = CmsUUID.getNullUUID();
341        long dateExpired = Integer.MAX_VALUE;
342        int linkCount = 0;
343        int size = 1;
344
345        CmsResource resource = new CmsResource(
346            structureId,
347            resourceId,
348            rootPath,
349            type,
350            isFolder,
351            flags,
352            projectId,
353            state,
354            dateCreated,
355            userCreated,
356            dateLastModified,
357            userLastModified,
358            dateReleased,
359            dateExpired,
360            linkCount,
361            size,
362            dateContent,
363            version);
364        return resource;
365    }
366
367    /**
368     * Creates a fake CmsResource of type 'folder'.<p>
369     *
370     * @param rootPath the root path
371     *
372     * @return the fake resource
373     *
374     * @throws CmsLoaderException if the 'folder' type can not be found
375     */
376    protected CmsResource createFakeFolder(String rootPath) throws CmsLoaderException {
377
378        if (rootPath.endsWith("/")) {
379            rootPath = CmsFileUtil.removeTrailingSeparator(rootPath);
380        }
381
382        CmsUUID structureId = CmsUUID.getConstantUUID("s-" + rootPath);
383        CmsUUID resourceId = CmsUUID.getConstantUUID("r-" + rootPath);
384        @SuppressWarnings("deprecation")
385        int type = OpenCms.getResourceManager().getResourceType(CmsResourceTypeFolder.getStaticTypeName()).getTypeId();
386        boolean isFolder = true;
387        int flags = 0;
388        CmsUUID projectId = CmsProject.ONLINE_PROJECT_ID;
389        CmsResourceState state = CmsResource.STATE_UNCHANGED;
390        long dateCreated = 0;
391        long dateLastModified = 0;
392        long dateReleased = 1;
393        long dateContent = 1;
394        int version = 0;
395        CmsUUID userCreated = CmsUUID.getNullUUID();
396        CmsUUID userLastModified = CmsUUID.getNullUUID();
397        long dateExpired = Integer.MAX_VALUE;
398        int linkCount = 0;
399        int size = -1;
400        CmsResource resource = new CmsResource(
401            structureId,
402            resourceId,
403            rootPath,
404            type,
405            isFolder,
406            flags,
407            projectId,
408            state,
409            dateCreated,
410            userCreated,
411            dateLastModified,
412            userLastModified,
413            dateReleased,
414            dateExpired,
415            linkCount,
416            size,
417            dateContent,
418            version);
419        return resource;
420    }
421
422    /**
423     * Checks whether the the current user should have access to the module functionality.<p>
424     *
425     * @param cms the current CMS context
426     * @return true if the user should have access
427     */
428    private boolean checkAccess(CmsObject cms) {
429
430        return OpenCms.getRoleManager().hasRole(cms, CmsRole.DATABASE_MANAGER);
431    }
432
433    /**
434     * Gets the virtual resources in the log folder.<p>
435     *
436     * @param cms the CMS context
437     * @return the list of virtual log resources
438     *
439     * @throws CmsException if something goes wrong
440     */
441    private List<CmsResource> getVirtualLogResources(CmsObject cms) throws CmsException {
442
443        List<CmsResource> virtualResources = Lists.newArrayList();
444        for (CmsModule module : OpenCms.getModuleManager().getAllInstalledModules()) {
445            String path = CmsStringUtil.joinPaths(LOG_PATH, module.getName() + ".log");
446            CmsResource res = createFakeBinaryFile(path);
447            virtualResources.add(res);
448        }
449        return virtualResources;
450    }
451
452    /**
453     * Gets the virtual resources for the base folder.<p>
454     *
455     * @param cms the current CMS context
456     * @return the virtual resources for the base folder
457     *
458     * @throws CmsException if something goes wrong
459     */
460    private List<CmsResource> getVirtualResourcesForBasePath(CmsObject cms) throws CmsException {
461
462        return Arrays.asList(createFakeFolder(IMPORT_PATH), createFakeFolder(EXPORT_PATH), createFakeFolder(LOG_PATH));
463    }
464
465    /**
466     * Gets the virtual resources for the export folder.<p>
467     *
468     * @param cms the CMS context
469     * @return the list of resources for the export folder
470     *
471     * @throws CmsException if something goes wrong
472     */
473    private List<CmsResource> getVirtualResourcesForExport(CmsObject cms) throws CmsException {
474
475        List<CmsResource> virtualResources = Lists.newArrayList();
476        for (String name : OpenCms.getModuleManager().getImportExportRepository().getModuleFileNames()) {
477            String path = CmsStringUtil.joinPaths(EXPORT_PATH, name);
478            CmsResource res = createFakeBinaryFile(path);
479            virtualResources.add(res);
480        }
481        return virtualResources;
482
483    }
484
485    /**
486     * Gets the virtual resources for the import folder.<p>
487     *
488     * @param cms the CMS context
489     *
490     * @return the virtual resources for the import folder
491     */
492    private List<CmsResource> getVirtualResourcesForImport(CmsObject cms) {
493
494        List<CmsResource> result = Lists.newArrayList();
495        return result;
496    }
497
498    /**
499     * Gets the virtual resources to add to the root folder.<p>
500     *
501     * @param cms the CMS context to use
502     * @return the virtual resources for the root folder
503     *
504     * @throws CmsException if something goes wrong
505     */
506    private List<CmsResource> getVirtualResourcesForRoot(CmsObject cms) throws CmsException {
507
508        CmsResource resource = createFakeFolder(BASE_PATH);
509        return Arrays.asList(resource);
510    }
511
512    /**
513     * Checks if the the import file is available.<p>
514     *
515     * @param resourcepath the resource path
516     *
517     * @return true if the import file is available
518     */
519    private boolean hasImportFile(String resourcepath) {
520
521        Long value = m_importFileUpdateCache.get(resourcepath);
522        if (value == null) {
523            return false;
524        }
525        long age = System.currentTimeMillis() - value.longValue();
526        return age < 5000;
527    }
528
529    /**
530     * Returns true if the given path is a fake path handled by this resource wrapper.<p>
531     *
532     * @param resourcename the path
533     *
534     * @return true if the path is a fake path handled by this resource wrapper
535     */
536    private boolean isFakePath(String resourcename) {
537
538        for (String folder : FOLDERS) {
539            if (matchPath(folder, resourcename) || matchParentPath(folder, resourcename)) {
540                return true;
541            }
542        }
543        return false;
544    }
545
546    /**
547     * Checks if a given path is a direct descendant of another path.<p>
548     *
549     * @param expectedParent the expected parent folder
550     * @param path a path
551     * @return true if the path is a direct child of expectedParent
552     */
553    private boolean matchParentPath(String expectedParent, String path) {
554
555        String parent = CmsResource.getParentFolder(path);
556        if (parent == null) {
557            return false;
558        }
559        return matchPath(expectedParent, parent);
560    }
561
562    /**
563     * Checks if a path matches another part.<p>
564     *
565     * This is basically an equality test, but ignores the presence/absence of trailing slashes.
566     *
567     * @param expected the expected path
568     * @param actual the actual path
569     * @return true if the actual path matches the expected path
570     */
571    private boolean matchPath(String expected, String actual) {
572
573        return CmsStringUtil.joinPaths(actual, "/").equals(CmsStringUtil.joinPaths(expected, "/"));
574    }
575}