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.loader.CmsLoaderException;
041import org.opencms.lock.CmsLock;
042import org.opencms.main.CmsException;
043import org.opencms.main.CmsIllegalArgumentException;
044import org.opencms.main.CmsLog;
045import org.opencms.main.OpenCms;
046import org.opencms.module.CmsModule;
047import org.opencms.module.CmsModuleImportExportRepository;
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 CmsResourceWrapperModulesNonLazy extends A_CmsResourceWrapper {
068
069    /** The logger instance to use for this class. */
070    private static final Log LOG = CmsLog.getLog(CmsResourceWrapperModulesNonLazy.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        CmsResource res = readResource(cms, resourcename, filter);
205        return (CmsFile)res;
206
207    }
208
209    /**
210     * @see org.opencms.file.wrapper.A_CmsResourceWrapper#readResource(org.opencms.file.CmsObject, java.lang.String, org.opencms.file.CmsResourceFilter)
211     */
212    @Override
213    public CmsResource readResource(CmsObject cms, String resourcepath, CmsResourceFilter filter) throws CmsException {
214
215        if (resourcepath.endsWith("desktop.ini")) {
216            return null;
217        }
218
219        if (checkAccess(cms)) {
220            for (String folder : FOLDERS) {
221                if (matchPath(resourcepath, folder)) {
222                    return createFakeFolder(folder);
223                }
224            }
225
226            if (matchParentPath(IMPORT_PATH, resourcepath)) {
227                if (hasImportFile(resourcepath)) {
228                    CmsFile importData = m_importDataCache.get(resourcepath);
229                    if (importData != null) {
230                        return importData;
231                    }
232                    return new CmsFile(createFakeBinaryFile(resourcepath));
233                }
234            }
235
236            if (matchParentPath(EXPORT_PATH, resourcepath)) {
237                CmsFile resultFile = new CmsFile(createFakeBinaryFile(resourcepath));
238                // we *do* require the file size, so we need to get the module data
239                LOG.info("Getting data for " + resourcepath);
240                CmsModuleImportExportRepository.ModuleExportData data = OpenCms.getModuleManager().getImportExportRepository().getExportedModuleData(
241                    CmsResource.getName(resourcepath),
242                    cms.getRequestContext().getCurrentProject());
243                if (data != null) {
244                    resultFile.setContents(data.getContent());
245                    resultFile.setDateLastModified(data.getDateLastModified());
246                }
247                return resultFile;
248            }
249
250            if (matchParentPath(LOG_PATH, resourcepath)) {
251                CmsFile resultFile = new CmsFile(createFakeBinaryFile(resourcepath));
252                String moduleName = CmsResource.getName(resourcepath).replaceFirst("\\.log$", "");
253                try {
254                    byte[] data = OpenCms.getModuleManager().getImportExportRepository().getModuleLog().readLog(
255                        moduleName);
256                    resultFile.setContents(data);
257                    return resultFile;
258                } catch (IOException e) {
259                    throw new CmsVfsResourceNotFoundException(
260                        org.opencms.db.Messages.get().container(
261                            org.opencms.db.Messages.ERR_READ_RESOURCE_1,
262                            resourcepath),
263                        e);
264                }
265            }
266        }
267        return super.readResource(cms, resourcepath, filter);
268    }
269
270    /**
271     * @see org.opencms.file.wrapper.A_CmsResourceWrapper#unlockResource(org.opencms.file.CmsObject, java.lang.String)
272     */
273    @Override
274    public boolean unlockResource(CmsObject cms, String resourcename) {
275
276        return isFakePath(resourcename);
277    }
278
279    /**
280     * @see org.opencms.file.wrapper.A_CmsResourceWrapper#writeFile(org.opencms.file.CmsObject, org.opencms.file.CmsFile)
281     */
282    @Override
283    public CmsFile writeFile(CmsObject cms, CmsFile resource) throws CmsException {
284
285        if (checkAccess(cms) && matchParentPath(IMPORT_PATH, resource.getRootPath())) {
286            OpenCms.getModuleManager().getImportExportRepository().importModule(
287                CmsResource.getName(resource.getRootPath()),
288                resource.getContents());
289            m_importFileUpdateCache.put(resource.getRootPath(), Long.valueOf(System.currentTimeMillis()));
290            m_importDataCache.put(resource.getRootPath(), resource);
291            return resource;
292        } else {
293            return super.writeFile(cms, resource);
294        }
295    }
296
297    /**
298     * Creates a fake CmsResource of type 'binary'.<p>
299     *
300     * @param rootPath  the root path
301     *
302     * @return the fake resource
303     *
304     * @throws CmsLoaderException if the binary type is missing
305     */
306    protected CmsResource createFakeBinaryFile(String rootPath) throws CmsLoaderException {
307
308        return createFakeBinaryFile(rootPath, 0);
309    }
310
311    /**
312     * Creates a fake CmsResource of type 'binary'.<p>
313     *
314     * @param rootPath  the root path
315     * @param dateLastModified the last modification date to use
316     *
317     * @return the fake resource
318     *
319     * @throws CmsLoaderException if the binary type is missing
320     */
321    protected CmsResource createFakeBinaryFile(String rootPath, long dateLastModified) throws CmsLoaderException {
322
323        CmsUUID structureId = CmsUUID.getConstantUUID("s-" + rootPath);
324        CmsUUID resourceId = CmsUUID.getConstantUUID("r-" + rootPath);
325        @SuppressWarnings("deprecation")
326        int type = OpenCms.getResourceManager().getResourceType(CmsResourceTypeBinary.getStaticTypeName()).getTypeId();
327        boolean isFolder = false;
328        int flags = 0;
329        CmsUUID projectId = CmsProject.ONLINE_PROJECT_ID;
330        CmsResourceState state = CmsResource.STATE_UNCHANGED;
331        long dateCreated = 0;
332        long dateReleased = 1;
333        long dateContent = 1;
334        int version = 0;
335
336        CmsUUID userCreated = CmsUUID.getNullUUID();
337        CmsUUID userLastModified = CmsUUID.getNullUUID();
338        long dateExpired = Integer.MAX_VALUE;
339        int linkCount = 0;
340        int size = 1;
341
342        CmsResource resource = new CmsResource(
343            structureId,
344            resourceId,
345            rootPath,
346            type,
347            isFolder,
348            flags,
349            projectId,
350            state,
351            dateCreated,
352            userCreated,
353            dateLastModified,
354            userLastModified,
355            dateReleased,
356            dateExpired,
357            linkCount,
358            size,
359            dateContent,
360            version);
361        return resource;
362    }
363
364    /**
365     * Creates a fake CmsResource of type 'folder'.<p>
366     *
367     * @param rootPath the root path
368     *
369     * @return the fake resource
370     *
371     * @throws CmsLoaderException if the 'folder' type can not be found
372     */
373    protected CmsResource createFakeFolder(String rootPath) throws CmsLoaderException {
374
375        if (rootPath.endsWith("/")) {
376            rootPath = CmsFileUtil.removeTrailingSeparator(rootPath);
377        }
378
379        CmsUUID structureId = CmsUUID.getConstantUUID("s-" + rootPath);
380        CmsUUID resourceId = CmsUUID.getConstantUUID("r-" + rootPath);
381        @SuppressWarnings("deprecation")
382        int type = OpenCms.getResourceManager().getResourceType(CmsResourceTypeFolder.getStaticTypeName()).getTypeId();
383        boolean isFolder = true;
384        int flags = 0;
385        CmsUUID projectId = CmsProject.ONLINE_PROJECT_ID;
386        CmsResourceState state = CmsResource.STATE_UNCHANGED;
387        long dateCreated = 0;
388        long dateLastModified = 0;
389        long dateReleased = 1;
390        long dateContent = 1;
391        int version = 0;
392        CmsUUID userCreated = CmsUUID.getNullUUID();
393        CmsUUID userLastModified = CmsUUID.getNullUUID();
394        long dateExpired = Integer.MAX_VALUE;
395        int linkCount = 0;
396        int size = -1;
397        CmsResource resource = new CmsResource(
398            structureId,
399            resourceId,
400            rootPath,
401            type,
402            isFolder,
403            flags,
404            projectId,
405            state,
406            dateCreated,
407            userCreated,
408            dateLastModified,
409            userLastModified,
410            dateReleased,
411            dateExpired,
412            linkCount,
413            size,
414            dateContent,
415            version);
416        return resource;
417    }
418
419    /**
420     * Checks whether the the current user should have access to the module functionality.<p>
421     *
422     * @param cms the current CMS context
423     * @return true if the user should have access
424     */
425    private boolean checkAccess(CmsObject cms) {
426
427        return OpenCms.getRoleManager().hasRole(cms, CmsRole.DATABASE_MANAGER);
428    }
429
430    /**
431     * Gets the virtual resources in the log folder.<p>
432     *
433     * @param cms the CMS context
434     * @return the list of virtual log resources
435     *
436     * @throws CmsException if something goes wrong
437     */
438    private List<CmsResource> getVirtualLogResources(CmsObject cms) throws CmsException {
439
440        List<CmsResource> virtualResources = Lists.newArrayList();
441        for (CmsModule module : OpenCms.getModuleManager().getAllInstalledModules()) {
442            try {
443                String path = CmsStringUtil.joinPaths(LOG_PATH, module.getName() + ".log");
444                CmsResource res = readResource(cms, path, CmsResourceFilter.ALL);
445                virtualResources.add(res);
446            } catch (Exception e) {
447                LOG.error(e.getLocalizedMessage(), e);
448            }
449        }
450        return virtualResources;
451    }
452
453    /**
454     * Gets the virtual resources for the base folder.<p>
455     *
456     * @param cms the current CMS context
457     * @return the virtual resources for the base folder
458     *
459     * @throws CmsException if something goes wrong
460     */
461    private List<CmsResource> getVirtualResourcesForBasePath(CmsObject cms) throws CmsException {
462
463        return Arrays.asList(createFakeFolder(IMPORT_PATH), createFakeFolder(EXPORT_PATH), createFakeFolder(LOG_PATH));
464    }
465
466    /**
467     * Gets the virtual resources for the export folder.<p>
468     *
469     * @param cms the CMS context
470     * @return the list of resources for the export folder
471     *
472     * @throws CmsException if something goes wrong
473     */
474    private List<CmsResource> getVirtualResourcesForExport(CmsObject cms) throws CmsException {
475
476        List<CmsResource> virtualResources = Lists.newArrayList();
477        for (String name : OpenCms.getModuleManager().getImportExportRepository().getModuleFileNames()) {
478            try {
479                String path = CmsStringUtil.joinPaths(EXPORT_PATH, name);
480                CmsResource res = readResource(cms, path, CmsResourceFilter.ALL);
481                virtualResources.add(res);
482            } catch (Exception e) {
483                LOG.error(e.getLocalizedMessage(), e);
484            }
485        }
486        return virtualResources;
487
488    }
489
490    /**
491     * Gets the virtual resources for the import folder.<p>
492     *
493     * @param cms the CMS context
494     *
495     * @return the virtual resources for the import folder
496     */
497    private List<CmsResource> getVirtualResourcesForImport(CmsObject cms) {
498
499        List<CmsResource> result = Lists.newArrayList();
500        return result;
501    }
502
503    /**
504     * Gets the virtual resources to add to the root folder.<p>
505     *
506     * @param cms the CMS context to use
507     * @return the virtual resources for the root folder
508     *
509     * @throws CmsException if something goes wrong
510     */
511    private List<CmsResource> getVirtualResourcesForRoot(CmsObject cms) throws CmsException {
512
513        CmsResource resource = createFakeFolder(BASE_PATH);
514        return Arrays.asList(resource);
515    }
516
517    /**
518     * Checks if the the import file is available.<p>
519     *
520     * @param resourcepath the resource path
521     *
522     * @return true if the import file is available
523     */
524    private boolean hasImportFile(String resourcepath) {
525
526        Long value = m_importFileUpdateCache.get(resourcepath);
527        if (value == null) {
528            return false;
529        }
530        long age = System.currentTimeMillis() - value.longValue();
531        return age < 5000;
532    }
533
534    /**
535     * Returns true if the given path is a fake path handled by this resource wrapper.<p>
536     *
537     * @param resourcename the path
538     *
539     * @return true if the path is a fake path handled by this resource wrapper
540     */
541    private boolean isFakePath(String resourcename) {
542
543        for (String folder : FOLDERS) {
544            if (matchPath(folder, resourcename) || matchParentPath(folder, resourcename)) {
545                return true;
546            }
547        }
548        return false;
549    }
550
551    /**
552     * Checks if a given path is a direct descendant of another path.<p>
553     *
554     * @param expectedParent the expected parent folder
555     * @param path a path
556     * @return true if the path is a direct child of expectedParent
557     */
558    private boolean matchParentPath(String expectedParent, String path) {
559
560        String parent = CmsResource.getParentFolder(path);
561        if (parent == null) {
562            return false;
563        }
564        return matchPath(expectedParent, parent);
565    }
566
567    /**
568     * Checks if a path matches another part.<p>
569     *
570     * This is basically an equality test, but ignores the presence/absence of trailing slashes.
571     *
572     * @param expected the expected path
573     * @param actual the actual path
574     * @return true if the actual path matches the expected path
575     */
576    private boolean matchPath(String expected, String actual) {
577
578        return CmsStringUtil.joinPaths(actual, "/").equals(CmsStringUtil.joinPaths(expected, "/"));
579    }
580}