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