001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (C) Alkacon Software (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, 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.ade.galleries;
029
030import org.opencms.ade.galleries.shared.I_CmsGalleryTreeEntry;
031import org.opencms.file.CmsObject;
032import org.opencms.file.CmsResource;
033import org.opencms.file.CmsResourceFilter;
034import org.opencms.file.CmsVfsResourceNotFoundException;
035import org.opencms.main.CmsException;
036import org.opencms.main.OpenCms;
037import org.opencms.security.CmsPermissionViolationException;
038import org.opencms.site.CmsSiteManagerImpl;
039import org.opencms.util.CmsStringUtil;
040
041import java.util.ArrayList;
042import java.util.Collection;
043import java.util.HashMap;
044import java.util.HashSet;
045import java.util.List;
046import java.util.Map;
047import java.util.Set;
048
049import com.google.common.base.Objects;
050import com.google.common.collect.ArrayListMultimap;
051import com.google.common.collect.Multimap;
052import com.google.common.collect.Sets;
053
054/**
055 * Abstract class which is used to generate the data for showing an already opened tree in the gallery dialog.<p>
056 *
057 * @param <T> the type of tree entry bean produced by this class
058 */
059public abstract class A_CmsTreeTabDataPreloader<T extends I_CmsGalleryTreeEntry<T>> {
060
061    /** Keeps track of which resources are children of each other. */
062    private Multimap<CmsResource, CmsResource> m_childMap = ArrayListMultimap.create();
063
064    /** The CMS context used for file operations. */
065    private CmsObject m_cms;
066
067    /** The common root path. */
068    private String m_commonRoot;
069
070    /** The filter used for reading resources. */
071    private CmsResourceFilter m_filter = CmsResourceFilter.ONLY_VISIBLE_NO_DELETED;
072
073    /** The set of resources which have already been read. */
074    private Set<CmsResource> m_knownResources = new HashSet<CmsResource>();
075
076    /** Resources whose children should be loaded. */
077    private Set<CmsResource> m_mustLoadChildren = new HashSet<CmsResource>();
078
079    /** The root resource. */
080    private CmsResource m_rootResource;
081
082    /**
083     * Creates the preload data for a collection of resources which correspond to "opened" tree items.<p>
084     *
085     * @param cms the CMS context to use
086     * @param openResources the resources which correspond to opened tree items
087     * @param selectedResources resources which should be part of the tree, but not opened
088     *
089     * @return the root tree entry bean which was created
090     *
091     * @throws CmsException if something goes wrong
092     */
093    public T preloadData(CmsObject cms, Set<CmsResource> openResources, Set<CmsResource> selectedResources)
094    throws CmsException {
095
096        assert m_cms == null : "Instance can't be used more than once!";
097        if (openResources == null) {
098            openResources = Sets.newHashSet();
099        }
100        if (selectedResources == null) {
101            selectedResources = Sets.newHashSet();
102        }
103
104        boolean ignoreOpen = false;
105        if (!selectedResources.isEmpty() && !openResources.isEmpty()) {
106            // if selected and opened resources are in different sites,
107            // we do *not* want to start from the common root folder (usually '/'),
108            // so ignore the 'open' resources.
109            String siteForSelected = getCommonSite(selectedResources);
110            String siteForOpen = getCommonSite(openResources);
111            if (!Objects.equal(siteForSelected, siteForOpen)) {
112                ignoreOpen = true;
113            }
114        }
115
116        Set<CmsResource> allParamResources = Sets.newHashSet();
117        if (!ignoreOpen) {
118            allParamResources.addAll(openResources);
119        }
120        allParamResources.addAll(selectedResources);
121        m_cms = OpenCms.initCmsObject(cms);
122        m_cms.getRequestContext().setSiteRoot("");
123        // first determine the common root of all open resources
124        findRoot(allParamResources);
125
126        m_mustLoadChildren.add(m_rootResource);
127        m_mustLoadChildren.addAll(openResources);
128        // now load ancestors of all open resources
129        for (CmsResource resource : allParamResources) {
130            loadAncestors(resource);
131        }
132        // ensure that all children of ancestors of open resources are loaded
133        loadChildren();
134        // finally create the beans for the loaded resources
135        return createBeans();
136    }
137
138    /**
139     * Creates a tree entry bean from a resource.<p>
140     *
141     * @param cms the current CMS context
142     * @param resource the resource for which to create the tree entry bean
143     *
144     * @return the created tree entry bean
145     * @throws CmsException if something goes wrong
146     */
147    protected abstract T createEntry(CmsObject cms, CmsResource resource) throws CmsException;
148
149    /**
150     * Finds the common root folder for a collection of resources.<p>
151     *
152     * @param resources the collection of resources
153     *
154     * @throws CmsException if something goes wrong
155     */
156    protected void findRoot(Collection<CmsResource> resources) throws CmsException {
157
158        m_commonRoot = getCommonSite(resources);
159        String commonPath = getCommonAncestorPath(resources);
160        try {
161            m_rootResource = m_cms.readResource(m_commonRoot, m_filter);
162        } catch (CmsVfsResourceNotFoundException | CmsPermissionViolationException e) {
163            String currentPath = commonPath;
164            String lastWorkingPath = null;
165            while (m_cms.existsResource(currentPath, m_filter)) {
166                lastWorkingPath = currentPath;
167                currentPath = CmsResource.getParentFolder(currentPath);
168            }
169            m_rootResource = m_cms.readResource(lastWorkingPath, m_filter);
170            m_commonRoot = lastWorkingPath;
171        }
172        m_knownResources.add(m_rootResource);
173    }
174
175    /**
176     * Gets the children of a resource.<p>
177     *
178     * @param resource the resource for which the children should be read
179     * @return the children of the resource
180     *
181     * @throws CmsException if something goes wrong
182     */
183    protected List<CmsResource> getChildren(CmsResource resource) throws CmsException {
184
185        return m_cms.getSubFolders(resource.getRootPath(), m_filter);
186    }
187
188    /**
189     * Gets the common site root of a set of resources.<p>
190     * @param resourceSet the resources
191     *
192     * @return the common site root (may also be the shared folder or root folder)
193     */
194    protected String getCommonSite(Collection<CmsResource> resourceSet) {
195
196        String commonPath = getCommonAncestorPath(resourceSet);
197        String result = null;
198        CmsSiteManagerImpl siteManager = OpenCms.getSiteManager();
199        if (siteManager.startsWithShared(commonPath)) {
200            result = siteManager.getSharedFolder();
201        } else {
202            String siteRoot = siteManager.getSiteRoot(CmsStringUtil.joinPaths(commonPath, "/"));
203            if (siteRoot == null) {
204                result = "/";
205            } else {
206                result = siteRoot;
207            }
208        }
209        return result;
210
211    }
212
213    /**
214     * Creates the beans for the loaded resources, and returns the root bean.<p>
215     *
216     * @return the root bean
217     * @throws CmsException if something goes wrong
218     */
219    private T createBeans() throws CmsException {
220
221        // create the beans for the resources
222        Map<CmsResource, T> beans = new HashMap<CmsResource, T>();
223        for (CmsResource resource : m_knownResources) {
224            T bean = createEntry(m_cms, resource);
225            if (bean != null) {
226                beans.put(resource, bean);
227            }
228        }
229
230        // attach beans for child resources to the beans for their parents
231        for (Map.Entry<CmsResource, T> entry : beans.entrySet()) {
232            CmsResource key = entry.getKey();
233            T bean = entry.getValue();
234            for (CmsResource child : m_childMap.get(key)) {
235                T childEntry = beans.get(child);
236                if (childEntry != null) {
237                    bean.addChild(childEntry);
238                }
239            }
240        }
241        return beans.get(m_rootResource);
242    }
243
244    /**
245     * Gets the common ancestor path of a collection of resources.<p>
246     *
247     * @param resources the resources for which to get the ancestor path
248     *
249     * @return the common ancestor path for the resources
250     */
251    private String getCommonAncestorPath(Collection<CmsResource> resources) {
252
253        if (resources.isEmpty()) {
254            return "/";
255        }
256        String commonPath = null;
257        for (CmsResource resource : resources) {
258            commonPath = getCommonAncestorPath(commonPath, resource.getRootPath());
259        }
260        return commonPath;
261    }
262
263    /**
264     * Gets the common ancestor of two paths.<p>
265     *
266     * @param rootPath1 the first path
267     * @param rootPath2 the second path
268     *
269     * @return the common ancestor path
270     */
271    private String getCommonAncestorPath(String rootPath1, String rootPath2) {
272
273        if (rootPath1 == null) {
274            return rootPath2;
275        }
276        if (rootPath2 == null) {
277            return rootPath1;
278        }
279        rootPath1 = CmsStringUtil.joinPaths("/", rootPath1, "/");
280        rootPath2 = CmsStringUtil.joinPaths("/", rootPath2, "/");
281        int minLength = Math.min(rootPath1.length(), rootPath2.length());
282        int i;
283        for (i = 0; i < minLength; i++) {
284            char char1 = rootPath1.charAt(i);
285            char char2 = rootPath2.charAt(i);
286            if (char1 != char2) {
287                break;
288            }
289        }
290        String result = rootPath1.substring(0, i);
291        if ("/".equals(result)) {
292            return result;
293        }
294        int slashIndex = result.lastIndexOf('/');
295        result = result.substring(0, slashIndex);
296        return result;
297    }
298
299    /**
300     * Loads the ancestors of a resource.<p>
301     *
302     * @param resource the resource for which to load the ancestors
303     *
304     * @throws CmsException if something goes wrong
305     */
306    private void loadAncestors(CmsResource resource) throws CmsException {
307
308        CmsResource currentResource = resource;
309        while ((currentResource != null) && m_cms.existsResource(currentResource.getStructureId(), m_filter)) {
310            if (!m_knownResources.add(currentResource)) {
311                break;
312            }
313
314            if (CmsStringUtil.comparePaths(currentResource.getRootPath(), m_commonRoot)) {
315                break;
316            }
317            CmsResource parent = m_cms.readParentFolder(currentResource.getStructureId());
318            if (parent != null) {
319                m_mustLoadChildren.add(parent);
320
321            }
322            currentResource = parent;
323        }
324    }
325
326    /**
327     * Loads the children of the already loaded resources.<p>
328     *
329     * @throws CmsException if something goes wrong
330     */
331    private void loadChildren() throws CmsException {
332
333        for (CmsResource resource : new ArrayList<CmsResource>(m_knownResources)) {
334            if (resource.isFolder()) {
335                if (!m_mustLoadChildren.contains(resource)) {
336                    continue;
337                }
338                List<CmsResource> children = getChildren(resource);
339                for (CmsResource child : children) {
340                    m_knownResources.add(child);
341                    m_childMap.put(resource, child);
342                }
343            }
344        }
345    }
346
347}