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 GmbH & Co. KG, 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.widgets; 029 030import org.opencms.acacia.shared.CmsWidgetUtil; 031import org.opencms.file.CmsFile; 032import org.opencms.file.CmsObject; 033import org.opencms.file.CmsResource; 034import org.opencms.file.CmsResourceFilter; 035import org.opencms.file.CmsVfsResourceNotFoundException; 036import org.opencms.i18n.CmsMessages; 037import org.opencms.main.CmsException; 038import org.opencms.main.CmsLog; 039import org.opencms.main.OpenCms; 040import org.opencms.relations.CmsCategory; 041import org.opencms.relations.CmsCategoryService; 042import org.opencms.util.CmsStringUtil; 043import org.opencms.util.CmsUUID; 044import org.opencms.workplace.CmsWorkplace; 045import org.opencms.xml.content.I_CmsXmlContentHandler.DisplayType; 046import org.opencms.xml.types.A_CmsXmlContentValue; 047import org.opencms.xml.types.CmsXmlCategoryValue; 048import org.opencms.xml.types.CmsXmlDynamicCategoryValue; 049import org.opencms.xml.types.I_CmsXmlContentValue; 050 051import java.util.ArrayList; 052import java.util.HashMap; 053import java.util.HashSet; 054import java.util.Iterator; 055import java.util.List; 056import java.util.Locale; 057import java.util.Map; 058import java.util.Set; 059 060import org.apache.commons.logging.Log; 061import org.apache.commons.text.StringEscapeUtils; 062 063/** 064 * Provides a widget for a category based dependent select boxes.<p> 065 * 066 * @since 7.0.3 067 */ 068public class CmsCategoryWidget extends A_CmsWidget implements I_CmsADEWidget { 069 070 /** Configuration parameter to set the category to display. */ 071 public static final String CONFIGURATION_CATEGORY = "category"; 072 073 /** Configuration parameter to set the 'only leaf' flag parameter. */ 074 public static final String CONFIGURATION_ONLYLEAFS = "onlyleafs"; 075 076 /** Configuration parameter to set the 'property' parameter. */ 077 public static final String CONFIGURATION_PROPERTY = "property"; 078 079 /** Configuration parameter to set the 'selection type' parameter. */ 080 private static final String CONFIGURATION_PARENTSELECTION = "parentselection"; 081 082 /** Configuration parameter to set the 'selection type' parameter. */ 083 private static final String CONFIGURATION_SELECTIONTYPE = "selectiontype"; 084 085 /** Configuration parameter to set the collapsing state when opening the selection. */ 086 private static final String CONFIGURATION_COLLAPSED = "collapsed"; 087 088 /** Configuration parameter to set flag, indicating if categories should be shown separated by repository. */ 089 private static final String CONFIGURATION_SHOW_WITH_REPOSITORY = "showwithrepository"; 090 091 /** Configuration parameter to set flag, indicating if categories should be shown separated by repository. */ 092 private static final String CONFIGURATION_REFPATH = "refpath"; 093 094 /** The log object for this class. */ 095 private static final Log LOG = CmsLog.getLog(CmsCategoryWidget.class); 096 097 /** The displayed category. */ 098 private String m_category; 099 100 /** The 'only leaf' flag. */ 101 private boolean m_onlyLeafs; 102 103 /** The value if the parents should be selected with the children. */ 104 private boolean m_parentSelection; 105 106 /** The property to read the starting category from. */ 107 private String m_property; 108 109 /** The selection type parsed from configuration string. */ 110 private String m_selectiontype = "multi"; 111 112 /** 113 * Creates a new category widget.<p> 114 */ 115 public CmsCategoryWidget() { 116 117 // empty constructor is required for class registration 118 super(); 119 } 120 121 /** 122 * Creates a category widget with the specified options.<p> 123 * 124 * @param configuration the configuration for the widget 125 */ 126 public CmsCategoryWidget(String configuration) { 127 128 super(configuration); 129 } 130 131 /** 132 * @see org.opencms.widgets.A_CmsWidget#getConfiguration() 133 */ 134 @Override 135 public String getConfiguration() { 136 137 Map<String, String> result = generateCommonConfigPart(); 138 139 return CmsWidgetUtil.generatePipeSeparatedConfigString(result); 140 } 141 142 /** 143 * @see org.opencms.widgets.I_CmsADEWidget#getConfiguration(org.opencms.file.CmsObject, org.opencms.xml.types.A_CmsXmlContentValue, org.opencms.i18n.CmsMessages, org.opencms.file.CmsResource, java.util.Locale) 144 */ 145 public String getConfiguration( 146 CmsObject cms, 147 A_CmsXmlContentValue schemaType, 148 CmsMessages messages, 149 CmsResource resource, 150 Locale contentLocale) { 151 152 // adjust 'selection type' according to schema type. 153 if (!m_selectiontype.equals("single") 154 && (schemaType.getTypeName().equals(CmsXmlCategoryValue.TYPE_NAME) 155 || schemaType.getTypeName().equals(CmsXmlDynamicCategoryValue.TYPE_NAME))) { 156 m_selectiontype = "multi"; 157 } else { 158 m_selectiontype = "single"; 159 } 160 161 // NOTE: set starting category as "category=" - independently if it is set via "property" or "category" config option. 162 m_category = this.getStartingCategory(cms, cms.getSitePath(resource)); 163 Map<String, String> result = generateCommonConfigPart(); 164 // append 'collapsed' flag, if necessary 165 if (OpenCms.getWorkplaceManager().isDisplayCategorySelectionCollapsed()) { 166 result.put(CONFIGURATION_COLLAPSED, null); 167 } 168 // append 'showWithCategory' flag, if necessary 169 if (OpenCms.getWorkplaceManager().isDisplayCategorySelectionCollapsed()) { 170 result.put(CONFIGURATION_SHOW_WITH_REPOSITORY, null); 171 } 172 if (m_parentSelection) { 173 result.put(CONFIGURATION_PARENTSELECTION, null); 174 } 175 result.put(CONFIGURATION_REFPATH, cms.getSitePath(resource)); 176 177 return CmsWidgetUtil.generatePipeSeparatedConfigString(result); 178 } 179 180 /** 181 * @see org.opencms.widgets.I_CmsADEWidget#getCssResourceLinks(org.opencms.file.CmsObject) 182 */ 183 public List<String> getCssResourceLinks(CmsObject cms) { 184 185 // nothing to do 186 return null; 187 } 188 189 /** 190 * @see org.opencms.widgets.I_CmsADEWidget#getDefaultDisplayType() 191 */ 192 public DisplayType getDefaultDisplayType() { 193 194 return DisplayType.wide; 195 } 196 197 /** 198 * @see org.opencms.widgets.I_CmsWidget#getDialogIncludes(org.opencms.file.CmsObject, org.opencms.widgets.I_CmsWidgetDialog) 199 */ 200 @Override 201 public String getDialogIncludes(CmsObject cms, I_CmsWidgetDialog widgetDialog) { 202 203 StringBuffer result = new StringBuffer(16); 204 result.append("<script src=\""); 205 result.append(CmsWorkplace.getSkinUri()); 206 result.append("components/widgets/category.js\"></script>\n"); 207 return result.toString(); 208 } 209 210 /** 211 * @see org.opencms.widgets.I_CmsWidget#getDialogWidget(org.opencms.file.CmsObject, org.opencms.widgets.I_CmsWidgetDialog, org.opencms.widgets.I_CmsWidgetParameter) 212 */ 213 public String getDialogWidget(CmsObject cms, I_CmsWidgetDialog widgetDialog, I_CmsWidgetParameter param) { 214 215 // get select box options from default value String 216 CmsCategory selected = null; 217 try { 218 String name = param.getStringValue(cms); 219 selected = CmsCategoryService.getInstance().getCategory(cms, name); 220 } catch (CmsException e) { 221 // ignore 222 } 223 224 StringBuffer result = new StringBuffer(16); 225 List<List<CmsSelectWidgetOption>> levels = new ArrayList<List<CmsSelectWidgetOption>>(); 226 try { 227 // write arrays of categories 228 result.append("<script >\n"); 229 String referencePath = null; 230 try { 231 referencePath = cms.getSitePath(getResource(cms, param)); 232 } catch (Exception e) { 233 // ignore, this can happen if a new resource is edited using direct edit 234 } 235 String startingCat = getStartingCategory(cms, referencePath); 236 List<CmsCategory> cats = CmsCategoryService.getInstance().readCategories( 237 cms, 238 startingCat, 239 true, 240 referencePath); 241 cats = CmsCategoryService.getInstance().localizeCategories( 242 cms, 243 cats, 244 OpenCms.getWorkplaceManager().getWorkplaceLocale(cms)); 245 int baseLevel; 246 if (CmsStringUtil.isEmptyOrWhitespaceOnly(startingCat)) { 247 baseLevel = 0; 248 } else { 249 baseLevel = CmsResource.getPathLevel(startingCat); 250 if (!(startingCat.startsWith("/") && startingCat.endsWith("/"))) { 251 baseLevel++; 252 } 253 } 254 int level; 255 Set<String> done = new HashSet<String>(); 256 List<CmsSelectWidgetOption> options = new ArrayList<CmsSelectWidgetOption>(); 257 String jsId = CmsStringUtil.substitute(param.getId(), ".", ""); 258 for (level = baseLevel + 1; !cats.isEmpty(); level++) { 259 if (level != (baseLevel + 1)) { 260 result.append("var cat" + (level - baseLevel) + jsId + " = new Array(\n"); 261 } 262 Iterator<CmsCategory> itSubs = cats.iterator(); 263 while (itSubs.hasNext()) { 264 CmsCategory cat = itSubs.next(); 265 String title = cat.getTitle(); 266 String titleJs = StringEscapeUtils.escapeEcmaScript(title); 267 String titleHtml = StringEscapeUtils.escapeHtml4(title); 268 if ((CmsResource.getPathLevel(cat.getPath()) + 1) == level) { 269 itSubs.remove(); 270 if (done.contains(cat.getPath())) { 271 continue; 272 } 273 if (level != (baseLevel + 1)) { 274 result.append( 275 "new Array('" 276 + cat.getId() 277 + "', '" 278 + CmsCategoryService.getInstance().readCategory( 279 cms, 280 CmsResource.getParentFolder(cat.getPath()), 281 referencePath).getId() 282 + "', '" 283 + titleJs 284 + "'),\n"); 285 } 286 if ((level == (baseLevel + 1)) 287 || ((selected != null) 288 && selected.getPath().startsWith(CmsResource.getParentFolder(cat.getPath())))) { 289 if (levels.size() < (level - baseLevel)) { 290 options = new ArrayList<CmsSelectWidgetOption>(); 291 levels.add(options); 292 options.add( 293 new CmsSelectWidgetOption( 294 "", 295 true, 296 Messages.get().getBundle(widgetDialog.getLocale()).key( 297 Messages.GUI_CATEGORY_SELECT_0))); 298 } 299 options.add(new CmsSelectWidgetOption(cat.getId().toString(), false, titleHtml)); 300 } 301 done.add(cat.getPath()); 302 } 303 } 304 if (level != (baseLevel + 1)) { 305 result.deleteCharAt(result.length() - 1); 306 result.deleteCharAt(result.length() - 1); 307 result.append(");\n"); 308 } 309 } 310 result.append("</script>\n"); 311 312 result.append("<td class=\"xmlTd\" >"); 313 result.append( 314 "<input id='" 315 + param.getId() 316 + "' name='" 317 + param.getId() 318 + "' type='hidden' value='" 319 + (selected != null ? selected.getId().toString() : "") 320 + "'>\n"); 321 322 for (int i = 1; i < (level - baseLevel); i++) { 323 result.append("<span id='" + param.getId() + "cat" + i + "IdDisplay'"); 324 if (levels.size() >= i) { 325 options = levels.get(i - 1); 326 } else { 327 result.append(" style='display:none'"); 328 options = new ArrayList<CmsSelectWidgetOption>(); 329 options.add( 330 new CmsSelectWidgetOption( 331 "", 332 true, 333 Messages.get().getBundle(widgetDialog.getLocale()).key(Messages.GUI_CATEGORY_SELECT_0))); 334 } 335 result.append(">"); 336 result.append( 337 buildSelectBox( 338 param.getId(), 339 i, 340 options, 341 (selected != null 342 ? CmsCategoryService.getInstance().readCategory( 343 cms, 344 CmsResource.getPathPart(selected.getPath(), i + baseLevel), 345 referencePath).getId().toString() 346 : ""), 347 param.hasError(), 348 (i == (level - baseLevel - 1)))); 349 result.append("</span> "); 350 } 351 result.append("</td>"); 352 } catch (CmsException e) { 353 result.append(e.getLocalizedMessage()); 354 } 355 return result.toString(); 356 } 357 358 /** 359 * @see org.opencms.widgets.I_CmsADEWidget#getInitCall() 360 */ 361 public String getInitCall() { 362 363 // nothing to do 364 return null; 365 } 366 367 /** 368 * @see org.opencms.widgets.I_CmsADEWidget#getJavaScriptResourceLinks(org.opencms.file.CmsObject) 369 */ 370 public List<String> getJavaScriptResourceLinks(CmsObject cms) { 371 372 // nothing to do 373 return null; 374 } 375 376 /** 377 * Returns the starting category depending on the configuration options.<p> 378 * 379 * @param cms the cms context 380 * @param referencePath the right resource path 381 * 382 * @return the starting category 383 */ 384 public String getStartingCategory(CmsObject cms, String referencePath) { 385 386 String ret = ""; 387 if (CmsStringUtil.isEmptyOrWhitespaceOnly(m_category) && CmsStringUtil.isEmptyOrWhitespaceOnly(m_property)) { 388 ret = "/"; 389 } else if (CmsStringUtil.isEmptyOrWhitespaceOnly(m_property)) { 390 ret = m_category; 391 } else { 392 // use the given property from the right file 393 if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(referencePath)) { 394 try { 395 ret = cms.readPropertyObject(referencePath, m_property, true).getValue("/"); 396 } catch (CmsException ex) { 397 // should never happen 398 if (LOG.isErrorEnabled()) { 399 LOG.error(ex.getLocalizedMessage(), ex); 400 } 401 } 402 } 403 } 404 if (!ret.endsWith("/")) { 405 ret += "/"; 406 } 407 if (ret.startsWith("/")) { 408 ret = ret.substring(1); 409 } 410 return ret; 411 } 412 413 /** 414 * @see org.opencms.widgets.I_CmsADEWidget#getWidgetName() 415 */ 416 public String getWidgetName() { 417 418 return CmsCategoryWidget.class.getName(); 419 } 420 421 /** 422 * @see org.opencms.widgets.A_CmsWidget#isCompactViewEnabled() 423 */ 424 @Override 425 public boolean isCompactViewEnabled() { 426 427 return false; 428 } 429 430 /** 431 * @see org.opencms.widgets.I_CmsADEWidget#isInternal() 432 */ 433 public boolean isInternal() { 434 435 return false; 436 } 437 438 /** 439 * Check if only leaf selection is allowed.<p> 440 * 441 * @return <code>true</code>, if only leaf selection is allowed 442 */ 443 public boolean isOnlyLeafs() { 444 445 return Boolean.valueOf(m_onlyLeafs).booleanValue(); 446 } 447 448 /** 449 * @see org.opencms.widgets.I_CmsWidget#newInstance() 450 */ 451 public I_CmsWidget newInstance() { 452 453 return new CmsCategoryWidget(getConfiguration()); 454 } 455 456 /** 457 * @see org.opencms.widgets.A_CmsWidget#setConfiguration(java.lang.String) 458 */ 459 @Override 460 public void setConfiguration(String configuration) { 461 462 Map<String, String> configOptions = CmsWidgetUtil.parsePipeSeparatedConfigString(configuration); 463 // we have to validate later, since we do not have any cms object here 464 m_category = ""; 465 if (!configOptions.isEmpty()) { 466 m_category = CmsWidgetUtil.getStringOption(configOptions, CONFIGURATION_CATEGORY, m_category); 467 m_onlyLeafs = CmsWidgetUtil.getBooleanOption(configOptions, CONFIGURATION_ONLYLEAFS); 468 m_parentSelection = CmsWidgetUtil.getBooleanOption(configOptions, CONFIGURATION_PARENTSELECTION); 469 m_property = CmsWidgetUtil.getStringOption(configOptions, CONFIGURATION_PROPERTY, m_property); 470 m_selectiontype = CmsWidgetUtil.getStringOption( 471 configOptions, 472 CONFIGURATION_SELECTIONTYPE, 473 m_selectiontype); 474 if (null != m_selectiontype) { 475 m_selectiontype = m_selectiontype.toLowerCase(); 476 } 477 } 478 super.setConfiguration(configuration); 479 } 480 481 /** 482 * @see org.opencms.widgets.A_CmsWidget#setEditorValue(org.opencms.file.CmsObject, java.util.Map, org.opencms.widgets.I_CmsWidgetDialog, org.opencms.widgets.I_CmsWidgetParameter) 483 */ 484 @Override 485 public void setEditorValue( 486 CmsObject cms, 487 Map<String, String[]> formParameters, 488 I_CmsWidgetDialog widgetDialog, 489 I_CmsWidgetParameter param) { 490 491 super.setEditorValue(cms, formParameters, widgetDialog, param); 492 String id = param.getStringValue(cms); 493 if (CmsStringUtil.isEmptyOrWhitespaceOnly(id)) { 494 return; 495 } 496 try { 497 CmsCategory cat = CmsCategoryService.getInstance().getCategory(cms, cms.readResource(new CmsUUID(id))); 498 String referencePath = null; 499 try { 500 referencePath = cms.getSitePath(getResource(cms, param)); 501 } catch (Exception e) { 502 // ignore, this can happen if a new resource is edited using direct edit 503 } 504 if (cat.getPath().startsWith(getStartingCategory(cms, referencePath))) { 505 param.setStringValue(cms, cat.getRootPath()); 506 } else { 507 param.setStringValue(cms, ""); 508 } 509 } catch (CmsException e) { 510 // invalid value 511 param.setStringValue(cms, ""); 512 } 513 } 514 515 /** 516 * Generates html code for the category selection.<p> 517 * 518 * @param baseId the widget id 519 * @param level the category deep level 520 * @param options the list of {@link CmsSelectWidgetOption} objects 521 * @param selected the selected option 522 * @param hasError if to display error message 523 * @param last if it is the last level 524 * 525 * @return html code 526 */ 527 protected String buildSelectBox( 528 String baseId, 529 int level, 530 List<CmsSelectWidgetOption> options, 531 String selected, 532 boolean hasError, 533 boolean last) { 534 535 StringBuffer result = new StringBuffer(16); 536 String id = baseId + "cat" + level + "Id"; 537 String childId = baseId + "cat" + (level + 1) + "Id"; 538 result.append("<select class=\"xmlInput"); 539 if (hasError) { 540 result.append(" xmlInputError"); 541 } 542 result.append("\" name=\""); 543 result.append(id); 544 result.append("\" id=\""); 545 result.append(id); 546 result.append("\" onchange=\""); 547 if (last) { 548 result.append("setWidgetValue('" + baseId + "');"); 549 } else { 550 String jsId = CmsStringUtil.substitute(baseId, ".", ""); 551 result.append("setChildListBox(this, getElemById('" + childId + "'), cat" + (level + 1) + jsId + ");"); 552 } 553 result.append("\">"); 554 555 Iterator<CmsSelectWidgetOption> i = options.iterator(); 556 while (i.hasNext()) { 557 CmsSelectWidgetOption option = i.next(); 558 // create the option 559 result.append("<option value=\""); 560 result.append(option.getValue()); 561 result.append("\""); 562 if ((selected != null) && selected.equals(option.getValue())) { 563 result.append(" selected=\"selected\""); 564 } 565 result.append(">"); 566 result.append(option.getOption()); 567 result.append("</option>"); 568 } 569 result.append("</select>"); 570 return result.toString(); 571 } 572 573 /** 574 * Returns the default locale in the content of the given resource.<p> 575 * 576 * @param cms the cms context 577 * @param resource the resource path to get the default locale for 578 * 579 * @return the default locale of the resource 580 */ 581 protected Locale getDefaultLocale(CmsObject cms, String resource) { 582 583 Locale locale = OpenCms.getLocaleManager().getDefaultLocale(cms, resource); 584 if (locale == null) { 585 List<Locale> locales = OpenCms.getLocaleManager().getAvailableLocales(); 586 if (locales.size() > 0) { 587 locale = locales.get(0); 588 } else { 589 locale = Locale.ENGLISH; 590 } 591 } 592 return locale; 593 } 594 595 /** 596 * Returns the right resource, depending on the locale.<p> 597 * 598 * @param cms the cms context 599 * @param param the widget parameter 600 * 601 * @return the resource to get/set the categories for 602 */ 603 protected CmsResource getResource(CmsObject cms, I_CmsWidgetParameter param) { 604 605 I_CmsXmlContentValue value = (I_CmsXmlContentValue)param; 606 CmsFile file = value.getDocument().getFile(); 607 String resourceName = cms.getSitePath(file); 608 if (CmsWorkplace.isTemporaryFile(file)) { 609 StringBuffer result = new StringBuffer(resourceName.length() + 2); 610 result.append(CmsResource.getFolderPath(resourceName)); 611 result.append(CmsResource.getName(resourceName).substring(1)); 612 resourceName = result.toString(); 613 } 614 try { 615 List<CmsResource> listsib = cms.readSiblings(resourceName, CmsResourceFilter.ALL); 616 for (int i = 0; i < listsib.size(); i++) { 617 CmsResource resource = listsib.get(i); 618 // get the default locale of the resource 619 Locale locale = getDefaultLocale(cms, cms.getSitePath(resource)); 620 if (locale.equals(value.getLocale())) { 621 // get the property for the right locale 622 return resource; 623 } 624 } 625 } catch (CmsVfsResourceNotFoundException e) { 626 // may hapen if editing a new resource 627 if (LOG.isDebugEnabled()) { 628 LOG.debug(e.getLocalizedMessage(), e); 629 } 630 } catch (CmsException e) { 631 if (LOG.isErrorEnabled()) { 632 LOG.error(e.getLocalizedMessage(), e); 633 } 634 } 635 return file; 636 } 637 638 /** 639 * Helper to generate the common configuration part for client-side and server-side widget. 640 * @return the common configuration options as map 641 */ 642 private Map<String, String> generateCommonConfigPart() { 643 644 Map<String, String> result = new HashMap<>(); 645 if (m_category != null) { 646 result.put(CONFIGURATION_CATEGORY, m_category); 647 } 648 // append 'only leafs' to configuration 649 if (m_onlyLeafs) { 650 result.put(CONFIGURATION_ONLYLEAFS, null); 651 } 652 // append 'property' to configuration 653 if (m_property != null) { 654 result.put(CONFIGURATION_PROPERTY, m_property); 655 } 656 // append 'selectionType' to configuration 657 if (m_selectiontype != null) { 658 result.put(CONFIGURATION_SELECTIONTYPE, m_selectiontype); 659 } 660 return result; 661 } 662}