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.xml.containerpage; 029 030import org.opencms.ade.containerpage.shared.CmsFormatterConfig; 031import org.opencms.file.CmsObject; 032import org.opencms.file.CmsResource; 033import org.opencms.file.types.CmsResourceTypeJsp; 034import org.opencms.main.CmsException; 035import org.opencms.main.CmsLog; 036import org.opencms.main.OpenCms; 037import org.opencms.security.CmsRole; 038import org.opencms.util.CmsStringUtil; 039import org.opencms.util.CmsUUID; 040 041import java.util.ArrayList; 042import java.util.Arrays; 043import java.util.Collection; 044import java.util.Collections; 045import java.util.Comparator; 046import java.util.LinkedHashMap; 047import java.util.List; 048import java.util.Map; 049import java.util.Set; 050 051import org.apache.commons.logging.Log; 052 053import com.google.common.base.Optional; 054import com.google.common.base.Predicate; 055import com.google.common.base.Predicates; 056import com.google.common.collect.Collections2; 057import com.google.common.collect.ComparisonChain; 058import com.google.common.collect.Iterables; 059import com.google.common.collect.Maps; 060import com.google.common.collect.Sets; 061 062/** 063 * Represents a formatter configuration.<p> 064 * 065 * A formatter configuration can be either defined in the XML schema XSD of a XML content, 066 * or in a special sitemap configuration file.<p> 067 * 068 * @since 8.0.0 069 */ 070public final class CmsFormatterConfiguration { 071 072 /** 073 * This class is used to sort lists of formatter beans in order of importance.<p> 074 */ 075 public static class FormatterComparator implements Comparator<I_CmsFormatterBean> { 076 077 /** 078 * @see java.util.Comparator#compare(java.lang.Object, java.lang.Object) 079 */ 080 public int compare(I_CmsFormatterBean first, I_CmsFormatterBean second) { 081 082 return ComparisonChain.start().compare(second.getRank(), first.getRank()).compare( 083 second.isTypeFormatter() ? 1 : 0, 084 first.isTypeFormatter() ? 1 : 0).compare(second.getMinWidth(), first.getMinWidth()).result(); 085 } 086 } 087 088 /** 089 * Predicate which checks whether the given formatter is a detail formatter.<p> 090 */ 091 public static class IsDetail implements Predicate<I_CmsFormatterBean> { 092 093 /** 094 * @see com.google.common.base.Predicate#apply(java.lang.Object) 095 */ 096 public boolean apply(I_CmsFormatterBean formatter) { 097 098 return formatter.isDetailFormatter(); 099 } 100 } 101 102 /** 103 * Predicate which checks whether the given formatter is a display formatter.<p> 104 */ 105 public static class IsDisplay implements Predicate<I_CmsFormatterBean> { 106 107 /** 108 * @see com.google.common.base.Predicate#apply(java.lang.Object) 109 */ 110 public boolean apply(I_CmsFormatterBean formatter) { 111 112 return formatter.isDisplayFormatter(); 113 } 114 } 115 116 /** 117 * Predicate to check whether the formatter is from a schema.<p> 118 */ 119 public static class IsSchemaFormatter implements Predicate<I_CmsFormatterBean> { 120 121 /** 122 * @see com.google.common.base.Predicate#apply(java.lang.Object) 123 */ 124 public boolean apply(I_CmsFormatterBean formatter) { 125 126 return !formatter.isFromFormatterConfigFile(); 127 128 } 129 } 130 131 /** 132 * Predicate which checks whether a formatter matches the given container type or width.<p> 133 */ 134 private class MatchesTypeOrWidth implements Predicate<I_CmsFormatterBean> { 135 136 /** The set of container types to match. */ 137 private Set<String> m_types = Sets.newHashSet(); 138 139 /** The container width. */ 140 private int m_width; 141 142 /** 143 * Creates a new matcher instance.<p> 144 * 145 * @param type the container type 146 * @param width the container width 147 */ 148 public MatchesTypeOrWidth(String type, int width) { 149 150 if (!CmsStringUtil.isEmptyOrWhitespaceOnly(type)) { 151 // split with comma and optionally spaces to the left/right of the comma as separator 152 m_types.addAll(Arrays.asList(type.trim().split(" *, *"))); 153 } 154 m_width = width; 155 } 156 157 /** 158 * @see com.google.common.base.Predicate#apply(java.lang.Object) 159 */ 160 public boolean apply(I_CmsFormatterBean formatter) { 161 162 return matchFormatter(formatter, m_types, m_width); 163 } 164 } 165 166 /** The empty formatter configuration. */ 167 public static final CmsFormatterConfiguration EMPTY_CONFIGURATION = new CmsFormatterConfiguration(null, null); 168 169 /** The log instance for this class. */ 170 public static final Log LOG = CmsLog.getLog(CmsFormatterConfiguration.class); 171 172 /** The container width to match all width configured formatters. */ 173 public static final int MATCH_ALL_CONTAINER_WIDTH = -2; 174 175 /** CmsObject used to read the JSP resources configured in the XSD schema. */ 176 private static CmsObject m_adminCms; 177 178 /** All formatters that have been added to this configuration. */ 179 private List<I_CmsFormatterBean> m_allFormatters; 180 181 /** The available display formatters. */ 182 private List<I_CmsFormatterBean> m_displayFormatters; 183 184 /** Cache for the searchContent option. */ 185 private Map<CmsUUID, Boolean> m_searchContent = Maps.newHashMap(); 186 187 /** 188 * Creates a new formatter configuration based on the given list of formatters.<p> 189 * 190 * @param cms the current users OpenCms context 191 * @param formatters the list of configured formatters 192 */ 193 private CmsFormatterConfiguration(CmsObject cms, List<I_CmsFormatterBean> formatters) { 194 195 if (formatters == null) { 196 // this is needed for the empty configuration 197 m_allFormatters = Collections.emptyList(); 198 } else { 199 m_allFormatters = new ArrayList<I_CmsFormatterBean>(formatters); 200 } 201 init(cms, m_adminCms); 202 } 203 204 /** 205 * Returns the formatter configuration for the current project based on the given list of formatters.<p> 206 * 207 * @param cms the current users OpenCms context, required to know which project to read the JSP from 208 * @param formatters the list of configured formatters 209 * 210 * @return the formatter configuration for the current project based on the given list of formatters 211 */ 212 public static CmsFormatterConfiguration create(CmsObject cms, List<I_CmsFormatterBean> formatters) { 213 214 if ((formatters != null) && (formatters.size() > 0) && (cms != null)) { 215 return new CmsFormatterConfiguration(cms, formatters); 216 } else { 217 return EMPTY_CONFIGURATION; 218 } 219 } 220 221 /** 222 * Initialize the formatter configuration.<p> 223 * 224 * @param cms an initialized admin OpenCms user context 225 * 226 * @throws CmsException in case the initialization fails 227 */ 228 public static void initialize(CmsObject cms) throws CmsException { 229 230 OpenCms.getRoleManager().checkRole(cms, CmsRole.ADMINISTRATOR); 231 try { 232 // store the Admin cms to index Cms resources 233 m_adminCms = OpenCms.initCmsObject(cms); 234 m_adminCms.getRequestContext().setSiteRoot(""); 235 } catch (CmsException e) { 236 // this should never happen 237 } 238 } 239 240 /** 241 * Checks whether the given formatter bean matches the container types, width and nested flag.<p> 242 * 243 * @param formatter the formatter bean 244 * @param types the container types 245 * @param width the container width 246 * 247 * @return <code>true</code> in case the formatter matches 248 */ 249 public static boolean matchFormatter(I_CmsFormatterBean formatter, Set<String> types, int width) { 250 251 if (formatter.isMatchAll()) { 252 return true; 253 } 254 if (formatter.isTypeFormatter()) { 255 return !Sets.intersection(types, formatter.getContainerTypes()).isEmpty(); 256 } else { 257 return (width == MATCH_ALL_CONTAINER_WIDTH) 258 || ((formatter.getMinWidth() <= width) && (width <= formatter.getMaxWidth())); 259 } 260 } 261 262 /** 263 * Gets a list of all defined formatters.<p> 264 * 265 * @return the list of all formatters 266 */ 267 public List<I_CmsFormatterBean> getAllFormatters() { 268 269 return new ArrayList<I_CmsFormatterBean>(m_allFormatters); 270 } 271 272 /** 273 * Gets the formatters which are available for the given container type and width.<p> 274 * 275 * @param containerTypes the container types (comma separated) 276 * @param containerWidth the container width 277 * 278 * @return the list of available formatters 279 */ 280 public List<I_CmsFormatterBean> getAllMatchingFormatters(String containerTypes, int containerWidth) { 281 282 return new ArrayList<I_CmsFormatterBean>( 283 Collections2.filter(m_allFormatters, new MatchesTypeOrWidth(containerTypes, containerWidth))); 284 285 } 286 287 /** 288 * Selects the best matching formatter for the provided type and width from this configuration.<p> 289 * 290 * This method first tries to find the formatter for the provided container type. 291 * If this fails, it returns the width based formatter that matched the container width.<p> 292 * 293 * @param containerTypes the container types (comma separated) 294 * @param containerWidth the container width 295 * 296 * @return the matching formatter, or <code>null</code> if none was found 297 */ 298 public I_CmsFormatterBean getDefaultFormatter(final String containerTypes, final int containerWidth) { 299 300 Optional<I_CmsFormatterBean> result = Iterables.tryFind( 301 m_allFormatters, 302 new MatchesTypeOrWidth(containerTypes, containerWidth)); 303 return result.orNull(); 304 } 305 306 /** 307 * Selects the best matching schema formatter for the provided type and width from this configuration.<p> 308 * 309 * @param containerTypes the container types (comma separated) 310 * @param containerWidth the container width 311 * 312 * @return the matching formatter, or <code>null</code> if none was found 313 */ 314 public I_CmsFormatterBean getDefaultSchemaFormatter(final String containerTypes, final int containerWidth) { 315 316 Optional<I_CmsFormatterBean> result = Iterables.tryFind( 317 m_allFormatters, 318 Predicates.and(new IsSchemaFormatter(), new MatchesTypeOrWidth(containerTypes, containerWidth))); 319 return result.orNull(); 320 } 321 322 /** 323 * Gets the detail formatter to use for the given type and container width.<p> 324 * 325 * @param types the container types (comma separated) 326 * @param containerWidth the container width 327 * 328 * @return the detail formatter to use 329 */ 330 public I_CmsFormatterBean getDetailFormatter(String types, int containerWidth) { 331 332 // detail formatters must still match the type or width 333 Predicate<I_CmsFormatterBean> checkValidDetailFormatter = Predicates.and( 334 new MatchesTypeOrWidth(types, containerWidth), 335 new IsDetail()); 336 Optional<I_CmsFormatterBean> result = Iterables.tryFind(m_allFormatters, checkValidDetailFormatter); 337 return result.orNull(); 338 } 339 340 /** 341 * Gets all detail formatters.<p> 342 * 343 * @return the detail formatters 344 */ 345 public Collection<I_CmsFormatterBean> getDetailFormatters() { 346 347 return Collections.<I_CmsFormatterBean> unmodifiableCollection( 348 Collections2.filter(m_allFormatters, new IsDetail())); 349 } 350 351 /** 352 * Returns the display formatter for this type.<p> 353 * 354 * @return the display formatter 355 */ 356 public I_CmsFormatterBean getDisplayFormatter() { 357 358 if (!getDisplayFormatters().isEmpty()) { 359 return getDisplayFormatters().get(0); 360 } 361 return null; 362 } 363 364 /** 365 * Returns the available display formatters.<p> 366 * 367 * @return the display formatters 368 */ 369 public List<I_CmsFormatterBean> getDisplayFormatters() { 370 371 if (m_displayFormatters == null) { 372 List<I_CmsFormatterBean> formatters = new ArrayList<I_CmsFormatterBean>( 373 Collections2.filter(m_allFormatters, new IsDisplay())); 374 if (formatters.size() > 1) { 375 Collections.sort(formatters, new Comparator<I_CmsFormatterBean>() { 376 377 public int compare(I_CmsFormatterBean o1, I_CmsFormatterBean o2) { 378 379 return o1.getRank() == o2.getRank() ? 0 : (o1.getRank() < o2.getRank() ? -1 : 1); 380 } 381 }); 382 } 383 m_displayFormatters = Collections.unmodifiableList(formatters); 384 } 385 return m_displayFormatters; 386 } 387 388 /** 389 * Returns the formatters available for selection for the given container type and width.<p> 390 * 391 * @param containerTypes the container types (comma separated) 392 * @param containerWidth the container width 393 * 394 * @return the list of available formatters 395 */ 396 public Map<String, I_CmsFormatterBean> getFormatterSelection(String containerTypes, int containerWidth) { 397 398 Map<String, I_CmsFormatterBean> result = new LinkedHashMap<String, I_CmsFormatterBean>(); 399 for (I_CmsFormatterBean formatter : Collections2.filter( 400 m_allFormatters, 401 new MatchesTypeOrWidth(containerTypes, containerWidth))) { 402 if (formatter.isFromFormatterConfigFile()) { 403 result.put(formatter.getId(), formatter); 404 } else { 405 result.put( 406 CmsFormatterConfig.SCHEMA_FORMATTER_ID + formatter.getJspStructureId().toString(), 407 formatter); 408 } 409 } 410 return result; 411 } 412 413 /** 414 * Returns the formatter from this configuration that is to be used for the preview in the ADE gallery GUI, 415 * or <code>null</code> if there is no preview formatter configured.<p> 416 * 417 * @return the formatter from this configuration that is to be used for the preview in the ADE gallery GUI, 418 * or <code>null</code> if there is no preview formatter configured 419 */ 420 public I_CmsFormatterBean getPreviewFormatter() { 421 422 Optional<I_CmsFormatterBean> result; 423 result = Iterables.tryFind(m_allFormatters, new Predicate<I_CmsFormatterBean>() { 424 425 public boolean apply(I_CmsFormatterBean formatter) { 426 427 return formatter.isPreviewFormatter(); 428 } 429 }); 430 if (!result.isPresent()) { 431 result = Iterables.tryFind(m_allFormatters, new Predicate<I_CmsFormatterBean>() { 432 433 public boolean apply(I_CmsFormatterBean formatter) { 434 435 if (formatter.isTypeFormatter()) { 436 return formatter.getContainerTypes().contains(CmsFormatterBean.PREVIEW_TYPE); 437 } else { 438 return (formatter.getMinWidth() <= CmsFormatterBean.PREVIEW_WIDTH) 439 && (CmsFormatterBean.PREVIEW_WIDTH <= formatter.getMaxWidth()); 440 } 441 } 442 }); 443 } 444 if (!result.isPresent()) { 445 result = Iterables.tryFind(m_allFormatters, new Predicate<I_CmsFormatterBean>() { 446 447 public boolean apply(I_CmsFormatterBean formatter) { 448 449 return !formatter.isTypeFormatter() && (formatter.getMaxWidth() >= CmsFormatterBean.PREVIEW_WIDTH); 450 451 } 452 }); 453 } 454 if (!result.isPresent() && !m_allFormatters.isEmpty()) { 455 result = Optional.fromNullable(m_allFormatters.iterator().next()); 456 } 457 return result.orNull(); 458 } 459 460 /** 461 * Returns the provided <code>true</code> in case this configuration has a formatter 462 * for the given type / width parameters.<p> 463 * 464 * @param containerTypes the container types (comma separated) 465 * @param containerWidth the container width 466 * 467 * @return the provided <code>true</code> in case this configuration has a formatter 468 * for the given type / width parameters. 469 */ 470 public boolean hasFormatter(String containerTypes, int containerWidth) { 471 472 return getDefaultFormatter(containerTypes, containerWidth) != null; 473 } 474 475 /** 476 * Returns <code>true</code> in case there is at least one usable formatter configured in this configuration.<p> 477 * 478 * @return <code>true</code> in case there is at least one usable formatter configured in this configuration 479 */ 480 public boolean hasFormatters() { 481 482 return !m_allFormatters.isEmpty(); 483 } 484 485 /** 486 * Returns <code>true</code> in case this configuration contains a formatter with the 487 * provided structure id that has been configured for including the formatted content in the online search.<p> 488 * 489 * @param formatterStructureId the formatter structure id 490 * 491 * @return <code>true</code> in case this configuration contains a formatter with the 492 * provided structure id that has been configured for including the formatted content in the online search 493 */ 494 public boolean isSearchContent(CmsUUID formatterStructureId) { 495 496 if (EMPTY_CONFIGURATION == this) { 497 // don't search if this is just the empty configuration 498 return false; 499 } 500 // lookup the cache 501 Boolean result = m_searchContent.get(formatterStructureId); 502 if (result == null) { 503 // result so far unknown 504 for (I_CmsFormatterBean formatter : m_allFormatters) { 505 if (formatter.getJspStructureId().equals(formatterStructureId)) { 506 // found the match 507 result = Boolean.valueOf(formatter.isSearchContent()); 508 // first match rules 509 break; 510 } 511 } 512 if (result == null) { 513 // no match found, in this case dont search the content 514 result = Boolean.FALSE; 515 } 516 // store result in the cache 517 m_searchContent.put(formatterStructureId, result); 518 } 519 520 return result.booleanValue(); 521 } 522 523 /** 524 * Initializes all formatters of this configuration.<p> 525 * 526 * It is also checked if the configured JSP root path exists, if not the formatter is removed 527 * as it is unusable.<p> 528 * 529 * @param userCms the current users OpenCms context, used for selecting the right project 530 * @param adminCms the Admin user context to use for reading the JSP resources 531 */ 532 private void init(CmsObject userCms, CmsObject adminCms) { 533 534 List<I_CmsFormatterBean> filteredFormatters = new ArrayList<I_CmsFormatterBean>(); 535 for (I_CmsFormatterBean formatter : m_allFormatters) { 536 537 if (formatter.getJspStructureId() == null) { 538 // a formatter may have been re-used so the structure id is already available 539 CmsResource res = null; 540 // first we make sure that the JSP exists at all (and also we read the UUID that way) 541 try { 542 // first get a cms copy so we can mess up the context without modifying the original 543 CmsObject cmsCopy = OpenCms.initCmsObject(adminCms); 544 cmsCopy.getRequestContext().setCurrentProject(userCms.getRequestContext().getCurrentProject()); 545 // switch to the root site 546 cmsCopy.getRequestContext().setSiteRoot(""); 547 // now read the JSP 548 res = cmsCopy.readResource(formatter.getJspRootPath()); 549 } catch (CmsException e) { 550 //if this happens the result is null and we write a LOG error 551 } 552 if ((res == null) || !CmsResourceTypeJsp.isJsp(res)) { 553 // the formatter must exist and it must be a JSP 554 LOG.error( 555 Messages.get().getBundle().key( 556 Messages.ERR_FORMATTER_JSP_DONT_EXIST_1, 557 formatter.getJspRootPath())); 558 } else { 559 formatter.setJspStructureId(res.getStructureId()); 560 // res may still be null in case of failure 561 } 562 } 563 564 if (formatter.getJspStructureId() != null) { 565 filteredFormatters.add(formatter); 566 } else { 567 LOG.warn("Invalid formatter: " + formatter.getJspRootPath()); 568 } 569 } 570 Collections.sort(filteredFormatters, new FormatterComparator()); 571 m_allFormatters = Collections.unmodifiableList(filteredFormatters); 572 } 573 574}