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.workplace.editors.directedit; 029 030import org.opencms.acacia.shared.I_CmsSerialDateValue.PatternType; 031import org.opencms.ade.configuration.CmsResourceTypeConfig; 032import org.opencms.ade.containerpage.shared.CmsDialogOptions; 033import org.opencms.ade.containerpage.shared.CmsDialogOptions.Option; 034import org.opencms.file.CmsFile; 035import org.opencms.file.CmsObject; 036import org.opencms.file.CmsResource; 037import org.opencms.i18n.CmsMessageContainer; 038import org.opencms.i18n.CmsMessages; 039import org.opencms.main.CmsException; 040import org.opencms.main.CmsLog; 041import org.opencms.main.OpenCms; 042import org.opencms.search.galleries.CmsGallerySearch; 043import org.opencms.search.galleries.CmsGallerySearchResult; 044import org.opencms.util.CmsUUID; 045import org.opencms.widgets.serialdate.CmsSerialDateBeanFactory; 046import org.opencms.widgets.serialdate.CmsSerialDateValue; 047import org.opencms.widgets.serialdate.I_CmsSerialDateBean; 048import org.opencms.xml.containerpage.CmsContainerElementBean; 049import org.opencms.xml.content.CmsXmlContent; 050import org.opencms.xml.content.CmsXmlContentFactory; 051import org.opencms.xml.types.CmsXmlSerialDateValue; 052import org.opencms.xml.types.I_CmsXmlContentValue; 053 054import java.text.DateFormat; 055import java.util.ArrayList; 056import java.util.Date; 057import java.util.List; 058import java.util.Locale; 059import java.util.Map; 060import java.util.Objects; 061 062import org.apache.commons.logging.Log; 063 064/** Special edit handler for contents that define multiple instances in a date series. */ 065public class CmsDateSeriesEditHandler implements I_CmsEditHandler { 066 067 /** Handler, that does the real work. We use an internal handler to allow for state. */ 068 private static class InternalHandler { 069 070 /** Logger for the class. */ 071 private static final Log LOG = CmsLog.getLog(InternalHandler.class); 072 073 /** Option: Edit / delete the only a single item instance of the series. */ 074 private static final String OPTION_INSTANCE = "instance"; 075 076 /** Edit option: Edit the whole series. */ 077 private static final String OPTION_SERIES = "series"; 078 079 /** The cms object with the current context. */ 080 private CmsObject m_cms; 081 082 /** The content that should be edited/deleted. */ 083 private CmsXmlContent m_content; 084 085 /** The content value that holds the definition of the date series. */ 086 private I_CmsXmlContentValue m_contentValue; 087 088 /** The edited container page element. */ 089 private CmsContainerElementBean m_elementBean; 090 091 /** The file of the content that should be edited/deleted. */ 092 private CmsFile m_file; 093 094 /** The date of the current instance of the series. */ 095 private Date m_instanceDate; 096 097 /** UUID of the container page we currently act on. */ 098 private CmsUUID m_pageContextId; 099 100 /** The current request parameters. */ 101 private Map<String, String[]> m_requestParameters; 102 103 /** The date series as defined in the content. */ 104 private I_CmsSerialDateBean m_series; 105 106 /** The definition of the date series from the content. */ 107 private CmsSerialDateValue m_value; 108 109 /** 110 * Constructor for the internal handler, basically taking the information that is provided in all methods of {@link I_CmsEditHandler} to do the common initialization. 111 * @param cms the current cms object. 112 * @param elementBean the currently edited container page element 113 * @param requestParams the current request parameters 114 * @param pageContextId the structure id of the container page where editing takes place 115 */ 116 public InternalHandler( 117 CmsObject cms, 118 CmsContainerElementBean elementBean, 119 Map<String, String[]> requestParams, 120 CmsUUID pageContextId) { 121 122 try { 123 m_cms = cms; 124 m_elementBean = elementBean; 125 m_requestParameters = requestParams; 126 m_pageContextId = pageContextId; 127 elementBean.initResource(cms); 128 CmsResource res = elementBean.getResource(); 129 m_file = cms.readFile(res); 130 m_content = CmsXmlContentFactory.unmarshal(cms, m_file); 131 m_contentValue = getSerialDateContentValue(m_content, null); 132 m_value = m_contentValue != null ? new CmsSerialDateValue(m_contentValue.getStringValue(cms)) : null; 133 m_series = CmsSerialDateBeanFactory.createSerialDateBean(m_value); 134 setInstanceDate(); 135 136 } catch (Exception e) { 137 LOG.error("Failed to determine all information to edit the instance of the date series.", e); 138 } 139 } 140 141 /** 142 * Returns the options for the delete dialog or <code>null</code> if no special options are available. 143 * @return the options for the delete dialog or <code>null</code> if no special options are available. 144 */ 145 public CmsDialogOptions getDeleteOptions() { 146 147 if (null != m_instanceDate) { 148 Locale wpl = OpenCms.getWorkplaceManager().getWorkplaceLocale(m_cms); 149 CmsMessages messages = Messages.get().getBundle(wpl); 150 if (!m_value.getPatternType().equals(PatternType.NONE)) { 151 List<Option> options = new ArrayList<>(2); 152 String instanceDate = DateFormat.getDateInstance(DateFormat.LONG, wpl).format(m_instanceDate); 153 Option oInstance = new Option( 154 OPTION_INSTANCE, 155 messages.key(Messages.GUI_DATE_SERIES_HANDLER_DELETE_OPTION_INSTANCE_1, instanceDate), 156 messages.key( 157 Messages.GUI_DATE_SERIES_HANDLER_DELETE_OPTION_INSTANCE_HELP_ACTIVE_1, 158 instanceDate), 159 false); 160 options.add(oInstance); 161 Option oSeries = new Option( 162 CmsDialogOptions.REGULAR_DELETE, 163 messages.key(Messages.GUI_DATE_SERIES_HANDLER_DELETE_OPTION_SERIES_0), 164 messages.key(Messages.GUI_DATE_SERIES_HANDLER_DELETE_OPTION_SERIES_HELP_ACTIVE_0), 165 false); 166 options.add(oSeries); 167 return new CmsDialogOptions( 168 messages.key(Messages.GUI_DATE_SERIES_HANDLER_DELETE_DIALOG_HEADING_0), 169 messages.key(Messages.GUI_DATE_SERIES_HANDLER_DELETE_DIALOG_INFO_1, getTitle(wpl)), 170 options); 171 } 172 } 173 return null; 174 175 } 176 177 /** 178 * Returns the options for the edit dialog or <code>null</code> if no special options are available. 179 * @param isListElement flag, indicating if the edited element is in a list. 180 * @return the options for the edit dialog or <code>null</code> if no special options are available. 181 */ 182 public CmsDialogOptions getEditOptions(boolean isListElement) { 183 184 if (null != m_instanceDate) { 185 Locale wpl = OpenCms.getWorkplaceManager().getWorkplaceLocale(m_cms); 186 CmsMessages messages = Messages.get().getBundle(wpl); 187 if (!m_value.getPatternType().equals(PatternType.NONE)) { 188 List<Option> options = new ArrayList<>(2); 189 String instanceDate = DateFormat.getDateInstance(DateFormat.LONG, wpl).format(m_instanceDate); 190 Option oInstance; 191 if (!isListElement && !isContainerPageLockable()) { 192 oInstance = new Option( 193 OPTION_INSTANCE, 194 messages.key(Messages.GUI_DATE_SERIES_HANDLER_EDIT_OPTION_INSTANCE_1, instanceDate), 195 messages.key( 196 Messages.GUI_DATE_SERIES_HANDLER_EDIT_OPTION_INSTANCE_HELP_INACTIVE_1, 197 instanceDate), 198 true); 199 } else { 200 oInstance = new Option( 201 OPTION_INSTANCE, 202 messages.key(Messages.GUI_DATE_SERIES_HANDLER_EDIT_OPTION_INSTANCE_1, instanceDate), 203 messages.key( 204 Messages.GUI_DATE_SERIES_HANDLER_EDIT_OPTION_INSTANCE_HELP_ACTIVE_1, 205 instanceDate), 206 false); 207 } 208 options.add(oInstance); 209 Option oSeries = new Option( 210 OPTION_SERIES, 211 messages.key(Messages.GUI_DATE_SERIES_HANDLER_EDIT_OPTION_SERIES_0), 212 messages.key(Messages.GUI_DATE_SERIES_HANDLER_EDIT_OPTION_SERIES_HELP_ACTIVE_0), 213 false); 214 options.add(oSeries); 215 return new CmsDialogOptions( 216 messages.key(Messages.GUI_DATE_SERIES_HANDLER_EDIT_DIALOG_HEADING_0), 217 messages.key(Messages.GUI_DATE_SERIES_HANDLER_EDIT_DIALOG_INFO_1, getTitle(wpl)), 218 options); 219 } 220 } 221 return null; 222 223 } 224 225 /** 226 * Handles a delete operation. 227 * @param deleteOption the delete option. 228 * @throws CmsException thrown if deletion fails. 229 */ 230 public void handleDelete(String deleteOption) throws CmsException { 231 232 if (Objects.equals(deleteOption, OPTION_INSTANCE)) { 233 addExceptionForInstance(); 234 } else { 235 throw new CmsException( 236 new CmsMessageContainer( 237 Messages.get(), 238 Messages.ERR_DATE_SERIES_HANDLER_INVALID_DELETE_OPTION_1, 239 deleteOption)); 240 } 241 242 } 243 244 /** 245 * Handles the preparation of the edit action, except for the series case, since in that case no specific handling is necessary. 246 * @param editOption the edit option. 247 * @return the structure id of the content that should be edited. 248 * @throws CmsException thrown if preparing the edit operation fails. 249 */ 250 public CmsUUID prepareForEdit(String editOption) throws CmsException { 251 252 if (Objects.equals(OPTION_INSTANCE, editOption)) { 253 return extractDate(); 254 } else { 255 throw new CmsException( 256 new CmsMessageContainer( 257 Messages.get(), 258 Messages.ERR_DATE_SERIES_HANDLER_INVALID_EDIT_OPTION_1, 259 editOption)); 260 } 261 262 } 263 264 /** 265 * Adds an exception in the series content and writes the changed content back to the file. 266 * @throws CmsException thrown if something goes wrong. 267 */ 268 private void addExceptionForInstance() throws CmsException { 269 270 if (null != m_instanceDate) { 271 try { 272 m_cms.lockResource(m_file); 273 m_value.addException(m_instanceDate); 274 String stringValue = m_value.toString(); 275 for (Locale l : m_content.getLocales()) { 276 I_CmsXmlContentValue contentValue = getSerialDateContentValue(m_content, l); 277 contentValue.setStringValue(m_cms, stringValue); 278 } 279 m_file.setContents(m_content.marshal()); 280 m_cms.writeFile(m_file); 281 m_cms.unlockResource(m_file); 282 } catch (Exception e) { 283 throw new CmsException( 284 new CmsMessageContainer( 285 Messages.get(), 286 Messages.ERR_DATE_SERIES_HANDLER_ADD_EXCEPTION_FAILED_0), 287 e); 288 } 289 } else { 290 throw new CmsException( 291 new CmsMessageContainer( 292 Messages.get(), 293 Messages.ERR_DATE_SERIES_HANDLER_ADD_EXCEPTION_FAILED_MISSING_DATE_0)); 294 } 295 } 296 297 /** 298 * Creates a copy of the series content and adjusts the dates in the copy. As well, adds an exception to the original series content. 299 * @return the structure id of the newly created content. 300 * @throws CmsException thrown if something goes wrong. 301 */ 302 private CmsUUID extractDate() throws CmsException { 303 304 if (null != m_instanceDate) { 305 try { 306 CmsResource page = m_cms.readResource(m_pageContextId); 307 CmsResourceTypeConfig typeConfig = OpenCms.getADEManager().lookupConfiguration( 308 m_cms, 309 page.getRootPath()).getResourceType( 310 OpenCms.getResourceManager().getResourceType(m_file).getTypeName()); 311 String pattern = typeConfig.getNamePattern(true); 312 String newSitePath = OpenCms.getResourceManager().getNameGenerator().getNewFileName( 313 m_cms, 314 CmsResource.getFolderPath(m_file.getRootPath()) + pattern, 315 5); 316 String oldSitePath = m_cms.getSitePath(m_file); 317 m_cms.copyResource(oldSitePath, newSitePath); 318 CmsFile newFile = m_cms.readFile(newSitePath); 319 CmsXmlContent newContent = CmsXmlContentFactory.unmarshal(m_cms, newFile); 320 CmsSerialDateValue newValue = new CmsSerialDateValue(); 321 newValue.setStart(m_instanceDate); 322 if ((m_value.getEnd() != null) && (m_series.getEventDuration() != null)) { 323 newValue.setEnd(new Date(m_instanceDate.getTime() + m_series.getEventDuration().longValue())); 324 } 325 newValue.setParentSeriesId(m_file.getStructureId()); 326 newValue.setWholeDay(Boolean.valueOf(m_value.isWholeDay())); 327 newValue.setPatternType(PatternType.NONE); 328 String newValueString = newValue.toString(); 329 for (Locale l : newContent.getLocales()) { 330 I_CmsXmlContentValue newContentValue = getSerialDateContentValue(newContent, l); 331 newContentValue.setStringValue(m_cms, newValueString); 332 } 333 newFile.setContents(newContent.marshal()); 334 m_cms.writeFile(newFile); 335 m_cms.unlockResource(newFile); 336 337 addExceptionForInstance(); 338 339 return newFile.getStructureId(); 340 } catch (Exception e) { 341 throw new CmsException( 342 new CmsMessageContainer( 343 Messages.get(), 344 Messages.ERR_DATE_SERIES_HANDLER_EXTRACT_CONTENT_FAILED_0), 345 e); 346 } 347 } else { 348 throw new CmsException( 349 new CmsMessageContainer( 350 Messages.get(), 351 Messages.ERR_DATE_SERIES_HANDLER_EXTRACT_CONTENT_FAILED_MISSING_DATE_0)); 352 } 353 } 354 355 /** 356 * Returns the content value that contains the date series definition. 357 * @param content the XML content to read the value from. 358 * @param locale the locale to get the value in. 359 * @return the content value that contains the date series definition. 360 */ 361 private I_CmsXmlContentValue getSerialDateContentValue(CmsXmlContent content, Locale locale) { 362 363 if ((null == locale) && !content.getLocales().isEmpty()) { 364 locale = content.getLocales().get(0); 365 } 366 for (I_CmsXmlContentValue value : content.getValues(locale)) { 367 if (value.getTypeName().equals(CmsXmlSerialDateValue.TYPE_NAME)) { 368 return value; 369 } 370 } 371 return null; 372 } 373 374 /** 375 * Returns the gallery title of the series content. 376 * @param l the locale to show the title in. 377 * @return the gallery title of the series content. 378 */ 379 private String getTitle(Locale l) { 380 381 CmsGallerySearchResult result; 382 try { 383 result = CmsGallerySearch.searchById(m_cms, m_contentValue.getDocument().getFile().getStructureId(), l); 384 return result.getTitle(); 385 } catch (CmsException e) { 386 LOG.error("Could not retrieve title of series content.", e); 387 return ""; 388 } 389 390 } 391 392 /** 393 * Checks, if the container page the edit operation takes place on can be locked by the current user. 394 * @return a flag, indicating if the page can be locked by the current user. 395 */ 396 private boolean isContainerPageLockable() { 397 398 try { 399 return m_cms.getLock(m_cms.readResource(m_pageContextId)).isLockableBy( 400 m_cms.getRequestContext().getCurrentUser()); 401 } catch (Exception e) { 402 LOG.error("Failed to check if the container page is lockable by the current user.", e); 403 return false; 404 } 405 } 406 407 /** 408 * Sets the date of the currently edited instance of the series. 409 */ 410 private void setInstanceDate() { 411 412 String sl = null; 413 Map<String, String> settings = m_elementBean.getSettings(); 414 if (settings.containsKey(PARAM_INSTANCEDATE)) { 415 sl = settings.get(PARAM_INSTANCEDATE); 416 } else if (m_requestParameters.containsKey(PARAM_INSTANCEDATE)) { 417 String[] sls = m_requestParameters.get(PARAM_INSTANCEDATE); 418 if ((sls != null) && (sls.length > 0)) { 419 sl = sls[0]; 420 } 421 } 422 if (sl != null) { 423 try { 424 long l = Long.parseLong(sl); 425 Date d = new Date(l); 426 if (m_series.getDates().contains(d)) { 427 m_instanceDate = d; 428 } else { 429 throw new Exception("Instance date is not a date of the series."); 430 } 431 } catch (Exception e) { 432 LOG.error( 433 "Could not read valid date from setting or request parameter \"" + PARAM_INSTANCEDATE + "\".", 434 e); 435 } 436 } else { 437 // TODO: Handle possible other element settings that could determine the actual instance date. 438 } 439 } 440 441 } 442 443 /** The key of the parameter/setting the instance date of the instance that should be edited is read from. */ 444 public static final String PARAM_INSTANCEDATE = "instancedate"; 445 446 /** 447 * @see org.opencms.workplace.editors.directedit.I_CmsEditHandler#getDeleteOptions(org.opencms.file.CmsObject, org.opencms.xml.containerpage.CmsContainerElementBean, org.opencms.util.CmsUUID, java.util.Map) 448 */ 449 @Override 450 public CmsDialogOptions getDeleteOptions( 451 CmsObject cms, 452 CmsContainerElementBean elementBean, 453 CmsUUID pageContextId, 454 Map<String, String[]> requestParams) { 455 456 InternalHandler internalHandler = new InternalHandler(cms, elementBean, requestParams, pageContextId); 457 return internalHandler.getDeleteOptions(); 458 } 459 460 /** 461 * @see org.opencms.workplace.editors.directedit.I_CmsEditHandler#getEditOptions(org.opencms.file.CmsObject, org.opencms.xml.containerpage.CmsContainerElementBean, org.opencms.util.CmsUUID, java.util.Map, boolean) 462 */ 463 @Override 464 public CmsDialogOptions getEditOptions( 465 CmsObject cms, 466 CmsContainerElementBean elementBean, 467 CmsUUID pageContextId, 468 Map<String, String[]> requestParams, 469 boolean isListElement) { 470 471 InternalHandler internalHandler = new InternalHandler(cms, elementBean, requestParams, pageContextId); 472 return internalHandler.getEditOptions(isListElement); 473 } 474 475 /** 476 * @see org.opencms.workplace.editors.directedit.I_CmsEditHandler#getNewOptions(org.opencms.file.CmsObject, org.opencms.xml.containerpage.CmsContainerElementBean, org.opencms.util.CmsUUID, java.util.Map) 477 */ 478 public CmsDialogOptions getNewOptions( 479 CmsObject cms, 480 CmsContainerElementBean elementBean, 481 CmsUUID pageContextId, 482 Map<String, String[]> requestParam) { 483 484 return null; 485 } 486 487 /** 488 * @see org.opencms.workplace.editors.directedit.I_CmsEditHandler#handleDelete(org.opencms.file.CmsObject, org.opencms.xml.containerpage.CmsContainerElementBean, java.lang.String, org.opencms.util.CmsUUID, java.util.Map) 489 */ 490 @Override 491 public void handleDelete( 492 CmsObject cms, 493 CmsContainerElementBean elementBean, 494 String deleteOption, 495 CmsUUID pageContextId, 496 Map<String, String[]> requestParams) 497 throws CmsException { 498 499 InternalHandler internalHandler = new InternalHandler(cms, elementBean, requestParams, pageContextId); 500 501 internalHandler.handleDelete(deleteOption); 502 503 } 504 505 /** 506 * @see org.opencms.workplace.editors.directedit.I_CmsEditHandler#handleNew(org.opencms.file.CmsObject, java.lang.String, java.util.Locale, java.lang.String, java.lang.String, java.lang.String, org.opencms.xml.containerpage.CmsContainerElementBean, org.opencms.util.CmsUUID, java.util.Map, java.lang.String) 507 */ 508 public String handleNew( 509 CmsObject cms, 510 String newLink, 511 Locale locale, 512 String referenceSitePath, 513 String modelFileSitePath, 514 String postCreateHandler, 515 CmsContainerElementBean element, 516 CmsUUID pageId, 517 Map<String, String[]> requestParams, 518 String choice) { 519 520 return null; 521 } 522 523 /** 524 * @see org.opencms.workplace.editors.directedit.I_CmsEditHandler#prepareForEdit(org.opencms.file.CmsObject, org.opencms.xml.containerpage.CmsContainerElementBean, java.lang.String, org.opencms.util.CmsUUID, java.util.Map) 525 */ 526 @Override 527 public CmsUUID prepareForEdit( 528 CmsObject cms, 529 CmsContainerElementBean elementBean, 530 String editOption, 531 CmsUUID pageContextId, 532 Map<String, String[]> requestParams) 533 throws CmsException { 534 535 if (Objects.equals(InternalHandler.OPTION_SERIES, editOption)) { 536 return elementBean.getId(); 537 } 538 InternalHandler internalHandler = new InternalHandler(cms, elementBean, requestParams, pageContextId); 539 return internalHandler.prepareForEdit(editOption); 540 } 541 542 /** 543 * @see org.opencms.workplace.editors.directedit.I_CmsEditHandler#setParameters(java.util.Map) 544 */ 545 public void setParameters(Map<String, String> params) { 546 // this handler doesn't need parameters 547 } 548}