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, 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.ui.components.fileselect; 029 030import org.opencms.file.CmsObject; 031import org.opencms.file.CmsResource; 032import org.opencms.file.CmsResourceFilter; 033import org.opencms.main.CmsException; 034import org.opencms.main.CmsLog; 035import org.opencms.main.OpenCms; 036import org.opencms.site.CmsSite; 037import org.opencms.ui.A_CmsUI; 038import org.opencms.ui.CmsVaadinUtils; 039import org.opencms.ui.components.CmsErrorDialog; 040import org.opencms.ui.components.CmsResourceTableProperty; 041import org.opencms.util.CmsStringUtil; 042import org.opencms.util.CmsUUID; 043import org.opencms.workplace.CmsWorkplace; 044 045import java.lang.reflect.Field; 046import java.util.ArrayList; 047import java.util.Arrays; 048import java.util.Collections; 049import java.util.Comparator; 050import java.util.HashMap; 051import java.util.HashSet; 052import java.util.List; 053import java.util.Map; 054import java.util.Set; 055import java.util.stream.Collectors; 056 057import org.apache.commons.logging.Log; 058 059import com.google.common.collect.Lists; 060import com.vaadin.event.Action; 061import com.vaadin.event.Action.Handler; 062import com.vaadin.event.ShortcutAction; 063import com.vaadin.ui.CustomComponent; 064import com.vaadin.ui.Window; 065import com.vaadin.v7.data.Container; 066import com.vaadin.v7.data.Container.Filter; 067import com.vaadin.v7.data.Item; 068import com.vaadin.v7.data.Property.ValueChangeEvent; 069import com.vaadin.v7.data.Property.ValueChangeListener; 070import com.vaadin.v7.data.util.IndexedContainer; 071import com.vaadin.v7.shared.ui.combobox.FilteringMode; 072import com.vaadin.v7.ui.ComboBox; 073import com.vaadin.v7.ui.TreeTable; 074 075/** 076 * Dialog with a site selector and file tree which can be used to select resources.<p> 077 */ 078public class CmsResourceSelectDialog extends CustomComponent { 079 080 /** 081 * Class for site select options.<p> 082 */ 083 public static class Options { 084 085 /**Indexed container.*/ 086 private IndexedContainer m_siteSelectionContainer; 087 088 /** 089 * Returns the siteSelectionContainer.<p> 090 * 091 * @return the siteSelectionContainer 092 */ 093 public IndexedContainer getSiteSelectionContainer() { 094 095 return m_siteSelectionContainer; 096 } 097 098 /** 099 * Sets the siteSelectionContainer.<p> 100 * 101 * @param siteSelectionContainer the siteSelectionContainer to set 102 */ 103 public void setSiteSelectionContainer(IndexedContainer siteSelectionContainer) { 104 105 m_siteSelectionContainer = siteSelectionContainer; 106 } 107 108 } 109 110 /** 111 * Information needed for filtering the resource tree. 112 */ 113 static class FilterData { 114 115 /** The set of folders to open. */ 116 private Set<CmsResource> m_openFolders = new HashSet<>(); 117 118 /** The set of root paths directly matching the filter. */ 119 private Set<String> m_matchedPaths = new HashSet<>(); 120 121 /** 122 * Gets the set of root paths of resources directly matching the filter. 123 * 124 * @return the set of paths of matching resources 125 */ 126 public Set<String> getMatchedPaths() { 127 128 return m_matchedPaths; 129 } 130 131 /** 132 * Gets the set of folders to open + 133 * 134 * @return the set of folders to open 135 */ 136 public Set<CmsResource> getOpenFolders() { 137 138 return m_openFolders; 139 } 140 } 141 142 /** 143 * Converts resource selection to path (string) selection - either as root paths or site paths.<p> 144 */ 145 class PathSelectionAdapter implements I_CmsSelectionHandler<CmsResource> { 146 147 /** The wrapped string selection handler. */ 148 private I_CmsSelectionHandler<String> m_pathHandler; 149 150 /** If true, pass site paths to the wrapped path handler, else root paths. */ 151 private boolean m_useSitePaths; 152 153 /** 154 * Creates a new instance.<p> 155 * 156 * @param pathHandler the selection handler to call 157 * @param useSitePaths true if we want changes as site paths 158 */ 159 public PathSelectionAdapter(I_CmsSelectionHandler<String> pathHandler, boolean useSitePaths) { 160 161 m_pathHandler = pathHandler; 162 m_useSitePaths = useSitePaths; 163 } 164 165 /** 166 * @see org.opencms.ui.components.fileselect.I_CmsSelectionHandler#onSelection(java.lang.Object) 167 */ 168 @SuppressWarnings("synthetic-access") 169 public void onSelection(CmsResource selected) { 170 171 String path = selected.getRootPath(); 172 if (m_useSitePaths) { 173 try { 174 CmsObject cms = OpenCms.initCmsObject(A_CmsUI.getCmsObject()); 175 cms.getRequestContext().setSiteRoot(m_siteRoot); 176 path = cms.getRequestContext().removeSiteRoot(path); 177 } catch (CmsException e) { 178 LOG.error(e.getLocalizedMessage(), e); 179 180 } 181 } 182 m_pathHandler.onSelection(path); 183 } 184 } 185 186 /** The property used for the site caption. */ 187 public static final String PROPERTY_SITE_CAPTION = "caption"; 188 189 /** Logger instance for this class. */ 190 private static final Log LOG = CmsLog.getLog(CmsResourceSelectDialog.class); 191 192 /** Serial version id. */ 193 private static final long serialVersionUID = 1L; 194 195 /** The CMS context. */ 196 protected CmsObject m_currentCms; 197 198 /** The resource filter. */ 199 protected CmsResourceFilter m_resourceFilter; 200 201 /** The resource initially displayed at the root of the tree. */ 202 protected CmsResource m_root; 203 204 /** The file tree (wrapped in an array, because Vaadin Declarative tries to bind it otherwise) .*/ 205 private CmsResourceTreeTable m_fileTree; 206 207 /** Boolean flag indicating whether the tree is currently filtered. */ 208 private boolean m_isSitemapView = true; 209 210 /** The site root. */ 211 private String m_siteRoot; 212 213 /** Contains the data for the tree. */ 214 private CmsResourceTreeContainer m_treeData; 215 216 /** The currently applied filter. */ 217 private Filter m_currentFilter; 218 219 /** 220 * Creates a new instance.<p> 221 * 222 * @param filter the resource filter to use 223 * 224 * @throws CmsException if something goes wrong 225 */ 226 public CmsResourceSelectDialog(CmsResourceFilter filter) 227 throws CmsException { 228 229 this(filter, A_CmsUI.getCmsObject()); 230 } 231 232 /** 233 * public constructor with given CmsObject.<p> 234 * 235 * @param filter filter the resource filter to use 236 * @param cms CmsObejct to use 237 * @throws CmsException if something goes wrong 238 */ 239 public CmsResourceSelectDialog(CmsResourceFilter filter, CmsObject cms) 240 throws CmsException { 241 242 this(filter, cms, new Options()); 243 } 244 245 /** 246 * public constructor.<p> 247 * 248 * @param filter resource filter 249 * @param cms CmsObject 250 * @param options options 251 * @throws CmsException exception 252 */ 253 public CmsResourceSelectDialog(CmsResourceFilter filter, final CmsObject _cms, Options options) 254 throws CmsException { 255 256 m_resourceFilter = filter; 257 setCompositionRoot(new CmsResourceSelectDialogContents()); 258 IndexedContainer container = options.getSiteSelectionContainer() != null 259 ? options.getSiteSelectionContainer() 260 : CmsVaadinUtils.getAvailableSitesContainer(_cms, PROPERTY_SITE_CAPTION); 261 getSiteSelector().setContainerDataSource(container); 262 263 final CmsObject cms; 264 if (!_cms.existsResource("/", CmsResourceFilter.IGNORE_EXPIRATION)) { 265 cms = OpenCms.initCmsObject(_cms); 266 cms.getRequestContext().setSiteRoot("/system/"); 267 } else { 268 cms = _cms; 269 } 270 m_siteRoot = cms.getRequestContext().getSiteRoot(); 271 272 getSiteSelector().setValue( 273 CmsVaadinUtils.getPathItemId(getSiteSelector().getContainerDataSource(), m_siteRoot)); 274 getSiteSelector().setNullSelectionAllowed(false); 275 getSiteSelector().setItemCaptionPropertyId(PROPERTY_SITE_CAPTION); 276 getSiteSelector().setFilteringMode(FilteringMode.CONTAINS); 277 getSiteSelector().addValueChangeListener(new ValueChangeListener() { 278 279 /** Serial version id. */ 280 private static final long serialVersionUID = 1L; 281 282 public void valueChange(ValueChangeEvent event) { 283 284 String site = (String)(event.getProperty().getValue()); 285 onSiteChange(site); 286 } 287 }); 288 289 CmsResource root = cms.readResource("/"); 290 m_fileTree = createTree(cms, root); 291 m_fileTree.setColumnExpandRatio(CmsResourceTreeTable.CAPTION_FOLDERS, 5); 292 m_fileTree.setColumnExpandRatio(CmsResourceTableProperty.PROPERTY_NAVIGATION_TEXT, 1); 293 m_treeData = m_fileTree.getTreeContainer(); 294 updateRoot(cms, root); 295 296 getContents().getTreeContainer().addComponent(m_fileTree); 297 m_fileTree.setSizeFull(); 298 getContents().addAttachListener(event -> { 299 Window window = CmsVaadinUtils.getWindow(this); 300 if (window != null) { 301 window.addActionHandler(new Handler() { 302 303 private final Action enterKeyShortcutAction = new ShortcutAction( 304 null, 305 ShortcutAction.KeyCode.ENTER, 306 null); 307 308 @Override 309 public Action[] getActions(Object target, Object sender) { 310 311 return new Action[] {enterKeyShortcutAction}; 312 } 313 314 @Override 315 public void handleAction(Action action, Object sender, Object target) { 316 317 if (enterKeyShortcutAction.equals(action)) { 318 if (target == getContents().getFilterBox()) { 319 updateFilter(); 320 } 321 } 322 323 } 324 }); 325 } 326 }); 327 328 updateView(); 329 330 getContents().getFilterButton().addClickListener(event -> { 331 updateFilter(); 332 }); 333 334 } 335 336 /** 337 * Adds a resource selection handler.<p> 338 * 339 * @param handler the handler 340 */ 341 public void addSelectionHandler(I_CmsSelectionHandler<CmsResource> handler) { 342 343 m_fileTree.addResourceSelectionHandler(handler); 344 } 345 346 /** 347 * Disables the option to select resources from other sites.<p> 348 */ 349 public void disableSiteSwitch() { 350 351 getSiteSelector().setEnabled(false); 352 } 353 354 /** 355 * Opens the given path.<p> 356 * 357 * @param path the path to open 358 */ 359 public void openPath(String path) { 360 361 if (!CmsStringUtil.isPrefixPath(m_root.getRootPath(), path)) { 362 CmsSite site = OpenCms.getSiteManager().getSiteForRootPath(path); 363 if (site != null) { 364 // the given path is a root path switch to the determined site 365 getSiteSelector().setValue(site.getSiteRoot()); 366 path = m_currentCms.getRequestContext().removeSiteRoot(path); 367 } else if (OpenCms.getSiteManager().startsWithShared(path)) { 368 getSiteSelector().setValue(OpenCms.getSiteManager().getSharedFolder()); 369 } else if (path.startsWith(CmsWorkplace.VFS_PATH_SYSTEM)) { 370 Container container = getSiteSelector().getContainerDataSource(); 371 String newSiteRoot = null; 372 for (String possibleSiteRoot : Arrays.asList("", "/", "/system", "/system/")) { 373 if (container.containsId(possibleSiteRoot)) { 374 newSiteRoot = possibleSiteRoot; 375 break; 376 } 377 } 378 if (newSiteRoot == null) { 379 LOG.warn( 380 "Couldn't open path in site selector because neither root site nor system folder are in the site selector. path=" 381 + path); 382 return; 383 } 384 getSiteSelector().setValue(newSiteRoot); 385 } 386 } 387 if (!"/".equals(path)) { 388 List<CmsUUID> idsToOpen = Lists.newArrayList(); 389 try { 390 CmsResource currentFolder = m_currentCms.readResource(CmsResource.getParentFolder(path)); 391 if (!m_root.getStructureId().equals(currentFolder.getStructureId())) { 392 idsToOpen.add(currentFolder.getStructureId()); 393 CmsResource parentFolder = null; 394 395 do { 396 try { 397 parentFolder = m_currentCms.readParentFolder(currentFolder.getStructureId()); 398 idsToOpen.add(parentFolder.getStructureId()); 399 currentFolder = parentFolder; 400 } catch (CmsException | NullPointerException e) { 401 LOG.info(e.getLocalizedMessage(), e); 402 break; 403 } 404 } while (!parentFolder.getStructureId().equals(m_root.getStructureId())); 405 // we need to iterate from "top" to "bottom", so we reverse the list of folders 406 Collections.reverse(idsToOpen); 407 408 for (CmsUUID id : idsToOpen) { 409 m_fileTree.expandItem(id); 410 } 411 } 412 } catch (CmsException e) { 413 LOG.debug("Can not read parent folder of current path.", e); 414 } 415 } 416 } 417 418 /** 419 * Switches between the folders and sitemap view of the tree.<p> 420 * 421 * @param showSitemapView <code>true</code> to show the sitemap view 422 */ 423 public void showSitemapView(boolean showSitemapView) { 424 425 if (m_isSitemapView != showSitemapView) { 426 m_isSitemapView = showSitemapView; 427 updateView(); 428 } 429 } 430 431 /** 432 * Displays the start resource by opening all nodes in the tree leading to it.<p> 433 * 434 * @param startResource the resource which should be shown in the tree 435 */ 436 public void showStartResource(CmsResource startResource) { 437 438 openPath(startResource.getRootPath()); 439 } 440 441 /** 442 * Creates the resource tree for the given root.<p> 443 * 444 * @param cms the CMS context 445 * @param root the root resource 446 * @return the resource tree 447 */ 448 protected CmsResourceTreeTable createTree(CmsObject cms, CmsResource root) { 449 450 return new CmsResourceTreeTable(cms, root, m_resourceFilter); 451 } 452 453 /** 454 * Gets the content panel of this dialog.<p> 455 * 456 * @return content panel of this dialog 457 */ 458 protected CmsResourceSelectDialogContents getContents() { 459 460 return ((CmsResourceSelectDialogContents)getCompositionRoot()); 461 } 462 463 /** 464 * Gets the file tree.<p> 465 * 466 * @return the file tree 467 */ 468 protected CmsResourceTreeTable getFileTree() { 469 470 return m_fileTree; 471 } 472 473 /** 474 * Called when the user changes the site.<p> 475 * 476 * @param site the new site root 477 */ 478 protected void onSiteChange(String site) { 479 480 try { 481 removeStringFilter(); 482 m_treeData.removeAllItems(); 483 484 // the tree table caches the open/closed state of items, even when the content of the data source changes, 485 // and then can get confused during a site change because the container items for a site share IDs 486 // with the corresponding container items in the root site. 487 // The following is a hack to clear the open/closed state cache, which depends on the internals of TreeTable. 488 try { 489 Field cStrategy = TreeTable.class.getDeclaredField("cStrategy"); 490 cStrategy.trySetAccessible(); 491 cStrategy.set(m_fileTree, null); 492 } catch (Exception e) { 493 LOG.error(e.getLocalizedMessage(), e); 494 } 495 CmsResourceSelectDialogContents contents = (CmsResourceSelectDialogContents)getCompositionRoot(); 496 contents.getFilterBox().setValue(""); 497 498 CmsObject rootCms = OpenCms.initCmsObject(A_CmsUI.getCmsObject()); 499 rootCms.getRequestContext().setSiteRoot(""); 500 CmsResource siteRootResource = rootCms.readResource(site); 501 m_treeData.initRoot(rootCms, siteRootResource); 502 m_fileTree.expandItem(siteRootResource.getStructureId()); 503 m_siteRoot = site; 504 updateRoot(rootCms, siteRootResource); 505 } catch (CmsException e) { 506 LOG.error(e.getLocalizedMessage(), e); 507 } 508 } 509 510 /** 511 * Updates the current site root resource.<p> 512 * 513 * @param rootCms the CMS context 514 * @param siteRootResource the resource corresponding to a site root 515 */ 516 protected void updateRoot(CmsObject rootCms, CmsResource siteRootResource) { 517 518 m_root = siteRootResource; 519 m_currentCms = rootCms; 520 updateView(); 521 } 522 523 /** 524 * Updates the filtering state.<p> 525 */ 526 protected void updateView() { 527 528 m_fileTree.showSitemapView(m_isSitemapView); 529 } 530 531 private FilterData getFilterData(CmsObject cms, CmsResource root, CmsResourceFilter filter, String filterText) 532 throws CmsException { 533 534 List<CmsResource> allResources = cms.readResources(root, filter, true); 535 List<CmsResource> matching = allResources.stream().filter( 536 res -> res.getName().toLowerCase().contains(filterText.toLowerCase())).collect(Collectors.toList()); 537 Map<String, CmsResource> resourcesByPath = allResources.stream().collect( 538 Collectors.toMap(res -> res.getRootPath(), res -> res, (a, b) -> b)); 539 FilterData result = new FilterData(); 540 for (CmsResource res : matching) { 541 String path = res.getRootPath(); 542 result.getMatchedPaths().add(path); 543 do { 544 path = CmsResource.getParentFolder(path); 545 CmsResource parent = resourcesByPath.get(path); 546 if (parent != null) { 547 result.getOpenFolders().add(parent); 548 } 549 } while ((path != null) && !path.equals(root.getRootPath())); 550 } 551 return result; 552 553 } 554 555 /** 556 * Gets the site selector.<p> 557 * 558 * @return the site selector 559 */ 560 private ComboBox getSiteSelector() { 561 562 return getContents().getSiteSelector(); 563 } 564 565 /** 566 * Removes the current filter. 567 */ 568 private void removeStringFilter() { 569 570 if (m_currentFilter != null) { 571 m_treeData.removeContainerFilter(m_currentFilter); 572 m_currentFilter = null; 573 } 574 } 575 576 /** 577 * Updates the filtering based on the current content of the filter box. 578 */ 579 private void updateFilter() { 580 581 String filterText = getContents().getFilterBox().getValue(); 582 if (CmsStringUtil.isEmptyOrWhitespaceOnly(filterText)) { 583 removeStringFilter(); 584 } else { 585 removeStringFilter(); 586 try { 587 FilterData filterData = getFilterData(m_currentCms, m_root, m_resourceFilter, filterText); 588 List<CmsResource> openFolders = new ArrayList<>(filterData.getOpenFolders()); 589 // sort open folders by path so parents are opened before children 590 openFolders.sort(Comparator.comparing(res -> res.getRootPath())); 591 for (CmsResource resource : openFolders) { 592 m_fileTree.expandItem(resource.getStructureId()); 593 } 594 m_currentFilter = new Container.Filter() { 595 596 private Map<CmsUUID, Boolean> m_cache = new HashMap<>(); 597 598 @Override 599 public boolean appliesToProperty(Object propertyId) { 600 601 return false; 602 } 603 604 @Override 605 public boolean passesFilter(Object itemId, Item item) throws UnsupportedOperationException { 606 607 return m_cache.computeIfAbsent((CmsUUID)itemId, id -> { 608 609 CmsResource resource = (CmsResource)(item.getItemProperty( 610 CmsResourceTreeContainer.PROPERTY_RESOURCE).getValue()); 611 for (String matchPath : filterData.getMatchedPaths()) { 612 if (CmsStringUtil.isPrefixPath(matchPath, resource.getRootPath()) 613 || CmsStringUtil.isPrefixPath(resource.getRootPath(), matchPath)) { 614 return true; 615 } 616 } 617 return false; 618 }); 619 } 620 }; 621 m_treeData.addContainerFilter(m_currentFilter); 622 } catch (Exception e) { 623 CmsErrorDialog.showErrorDialog(e); 624 } 625 } 626 } 627 628}