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.gwt; 029 030import org.opencms.ade.configuration.CmsADEConfigData; 031import org.opencms.cache.CmsVfsMemoryObjectCache; 032import org.opencms.file.CmsFile; 033import org.opencms.file.CmsObject; 034import org.opencms.file.CmsProperty; 035import org.opencms.file.CmsPropertyDefinition; 036import org.opencms.file.CmsResource; 037import org.opencms.file.CmsResourceFilter; 038import org.opencms.file.types.CmsResourceTypeXmlContainerPage; 039import org.opencms.gwt.shared.property.CmsClientProperty; 040import org.opencms.gwt.shared.property.CmsPropertiesBean; 041import org.opencms.gwt.shared.property.CmsPropertyChangeSet; 042import org.opencms.gwt.shared.property.CmsPropertyModification; 043import org.opencms.lock.CmsLock; 044import org.opencms.lock.CmsLockActionRecord; 045import org.opencms.lock.CmsLockActionRecord.LockChange; 046import org.opencms.lock.CmsLockUtil; 047import org.opencms.main.CmsException; 048import org.opencms.main.CmsLog; 049import org.opencms.main.OpenCms; 050import org.opencms.security.CmsPermissionSet; 051import org.opencms.util.CmsFileUtil; 052import org.opencms.util.CmsMacroResolver; 053import org.opencms.util.CmsStringUtil; 054import org.opencms.util.CmsUUID; 055import org.opencms.widgets.CmsHtmlWidget; 056import org.opencms.widgets.CmsHtmlWidgetOption; 057import org.opencms.workplace.explorer.CmsExplorerTypeSettings; 058import org.opencms.workplace.explorer.CmsResourceUtil; 059import org.opencms.xml.content.CmsXmlContentProperty; 060import org.opencms.xml.content.CmsXmlContentPropertyHelper; 061 062import java.io.UnsupportedEncodingException; 063import java.util.ArrayList; 064import java.util.Collections; 065import java.util.HashMap; 066import java.util.LinkedHashMap; 067import java.util.List; 068import java.util.Locale; 069import java.util.Map; 070 071import org.apache.commons.collections.Transformer; 072import org.apache.commons.logging.Log; 073 074import com.google.common.collect.Lists; 075import com.google.common.collect.Maps; 076 077/** 078 * Helper class responsible for loading / saving properties when using the property dialog.<p> 079 */ 080public class CmsPropertyEditorHelper { 081 082 /** The log instance for this class. */ 083 private static final Log LOG = CmsLog.getLog(CmsPropertyEditorHelper.class); 084 085 /** The CMS context. */ 086 private CmsObject m_cms; 087 088 /** Flag that controls whether the index should be updated after saving. */ 089 private boolean m_updateIndex; 090 091 /** Structure id which should be used instead of the structure id in a property change set (can be null). */ 092 private CmsUUID m_overrideStructureId; 093 094 /** 095 * Creates a new instance.<p> 096 * 097 * @param cms the CMS context 098 */ 099 public CmsPropertyEditorHelper(CmsObject cms) { 100 101 m_cms = cms; 102 103 } 104 105 /** 106 * Updates the property configuration for properties using WYSIWYG widgets.<p> 107 * 108 * @param propertyConfig the property configuration 109 * @param cms the CMS context 110 * @param resource the current resource (may be null) 111 */ 112 public static void updateWysiwygConfig( 113 Map<String, CmsXmlContentProperty> propertyConfig, 114 CmsObject cms, 115 CmsResource resource) { 116 117 Map<String, CmsXmlContentProperty> wysiwygUpdates = Maps.newHashMap(); 118 String wysiwygConfig = null; 119 for (Map.Entry<String, CmsXmlContentProperty> entry : propertyConfig.entrySet()) { 120 CmsXmlContentProperty prop = entry.getValue(); 121 if (prop.getWidget().equals("wysiwyg")) { 122 if (wysiwygConfig == null) { 123 String configStr = ""; 124 try { 125 String filePath = OpenCms.getSystemInfo().getConfigFilePath(cms, "wysiwyg/property-widget"); 126 String configFromVfs = (String)CmsVfsMemoryObjectCache.getVfsMemoryObjectCache().loadVfsObject( 127 cms, 128 filePath, 129 new Transformer() { 130 131 public Object transform(Object rootPath) { 132 133 try { 134 CmsFile file = cms.readFile( 135 (String)rootPath, 136 CmsResourceFilter.IGNORE_EXPIRATION); 137 return new String(file.getContents(), "UTF-8"); 138 } catch (Exception e) { 139 return ""; 140 } 141 } 142 }); 143 configStr = configFromVfs; 144 } catch (Exception e) { 145 LOG.error(e.getLocalizedMessage(), e); 146 } 147 148 CmsHtmlWidgetOption opt = new CmsHtmlWidgetOption(configStr); 149 Locale locale = resource != null 150 ? OpenCms.getLocaleManager().getDefaultLocale(cms, resource) 151 : Locale.ENGLISH; 152 String json = CmsHtmlWidget.getJSONConfiguration(opt, cms, resource, locale).toString(); 153 List<String> nums = Lists.newArrayList(); 154 try { 155 for (byte b : json.getBytes("UTF-8")) { 156 nums.add("" + b); 157 } 158 } catch (UnsupportedEncodingException e) { 159 // TODO Auto-generated catch block 160 e.printStackTrace(); 161 } 162 wysiwygConfig = "v:" + CmsStringUtil.listAsString(nums, ","); 163 } 164 CmsXmlContentProperty prop2 = prop.withConfig(wysiwygConfig); 165 wysiwygUpdates.put(entry.getKey(), prop2); 166 } 167 } 168 propertyConfig.putAll(wysiwygUpdates); 169 } 170 171 /** 172 * Internal method for computing the default property configurations for a list of structure ids.<p> 173 * 174 * @param structureIds the structure ids for which we want the default property configurations 175 * @return a map from the given structure ids to their default property configurations 176 * 177 * @throws CmsException if something goes wrong 178 */ 179 public Map<CmsUUID, Map<String, CmsXmlContentProperty>> getDefaultProperties( 180 181 List<CmsUUID> structureIds) 182 throws CmsException { 183 184 CmsObject cms = m_cms; 185 186 Map<CmsUUID, Map<String, CmsXmlContentProperty>> result = Maps.newHashMap(); 187 for (CmsUUID structureId : structureIds) { 188 CmsResource resource = cms.readResource(structureId, CmsResourceFilter.ALL); 189 String typeName = OpenCms.getResourceManager().getResourceType(resource).getTypeName(); 190 Map<String, CmsXmlContentProperty> propertyConfig = getDefaultPropertiesForType(typeName); 191 result.put(structureId, propertyConfig); 192 } 193 return result; 194 } 195 196 /** 197 * Loads the data needed for editing the properties of a resource.<p> 198 * 199 * @param id the structure id of the resource 200 * @return the data needed for editing the properties 201 * 202 * @throws CmsException if something goes wrong 203 */ 204 public CmsPropertiesBean loadPropertyData(CmsUUID id) throws CmsException { 205 206 CmsObject cms = m_cms; 207 String originalSiteRoot = cms.getRequestContext().getSiteRoot(); 208 CmsPropertiesBean result = new CmsPropertiesBean(); 209 CmsResource resource = cms.readResource(id, CmsResourceFilter.IGNORE_EXPIRATION); 210 result.setReadOnly(!isWritable(cms, resource)); 211 result.setFolder(resource.isFolder()); 212 result.setContainerPage(CmsResourceTypeXmlContainerPage.isContainerPage(resource)); 213 String sitePath = cms.getSitePath(resource); 214 CmsADEConfigData config = OpenCms.getADEManager().lookupConfiguration(cms, resource.getRootPath()); 215 216 Map<String, CmsXmlContentProperty> defaultProperties = getDefaultProperties( 217 218 Collections.singletonList(resource.getStructureId())).get(resource.getStructureId()); 219 220 Map<String, CmsXmlContentProperty> mergedConfig = config.getPropertyConfiguration(defaultProperties); 221 Map<String, CmsXmlContentProperty> propertyConfig = mergedConfig; 222 223 // Resolve macros in the property configuration 224 propertyConfig = CmsXmlContentPropertyHelper.resolveMacrosInProperties( 225 propertyConfig, 226 CmsMacroResolver.newWorkplaceLocaleResolver(cms)); 227 updateWysiwygConfig(propertyConfig, cms, resource); 228 229 result.setPropertyDefinitions(new LinkedHashMap<String, CmsXmlContentProperty>(propertyConfig)); 230 try { 231 cms.getRequestContext().setSiteRoot(""); 232 String parentPath = CmsResource.getParentFolder(resource.getRootPath()); 233 CmsResource parent = cms.readResource(parentPath, CmsResourceFilter.IGNORE_EXPIRATION); 234 List<CmsProperty> parentProperties = cms.readPropertyObjects(parent, true); 235 List<CmsProperty> ownProperties = cms.readPropertyObjects(resource, false); 236 result.setOwnProperties(convertProperties(ownProperties)); 237 result.setInheritedProperties(convertProperties(parentProperties)); 238 result.setPageInfo(CmsVfsService.getPageInfo(cms, resource)); 239 List<CmsPropertyDefinition> propDefs = cms.readAllPropertyDefinitions(); 240 List<String> propNames = new ArrayList<String>(); 241 for (CmsPropertyDefinition propDef : propDefs) { 242 if (CmsStringUtil.isEmpty(propDef.getName())) { 243 LOG.warn("Empty property definition name: " + propDef); 244 continue; 245 } 246 propNames.add(propDef.getName()); 247 } 248 CmsTemplateFinder templateFinder = new CmsTemplateFinder(cms); 249 result.setTemplates(templateFinder.getTemplates()); 250 result.setAllProperties(propNames); 251 result.setStructureId(id); 252 result.setSitePath(sitePath); 253 return result; 254 } finally { 255 cms.getRequestContext().setSiteRoot(originalSiteRoot); 256 } 257 } 258 259 /** 260 * Sets a structure id that overrides the one stored in a property change set.<p> 261 * 262 * @param structureId the new structure id 263 */ 264 public void overrideStructureId(CmsUUID structureId) { 265 266 m_overrideStructureId = structureId; 267 } 268 269 /** 270 * Saves a set of property changes.<p> 271 * 272 * @param changes the set of property changes 273 * @throws CmsException if something goes wrong 274 */ 275 public void saveProperties(CmsPropertyChangeSet changes) throws CmsException { 276 277 CmsObject cms = m_cms; 278 CmsUUID structureId = changes.getTargetStructureId(); 279 if (m_overrideStructureId != null) { 280 structureId = m_overrideStructureId; 281 } 282 CmsResource resource = cms.readResource(structureId, CmsResourceFilter.IGNORE_EXPIRATION); 283 boolean shallow = true; 284 for (CmsPropertyModification propMode : changes.getChanges()) { 285 if (propMode.isFileNameProperty()) { 286 shallow = false; 287 } 288 } 289 CmsLockActionRecord actionRecord = CmsLockUtil.ensureLock(cms, resource, shallow); 290 try { 291 Map<String, CmsProperty> ownProps = getPropertiesByName(cms.readPropertyObjects(resource, false)); 292 // determine if the title property should be changed in case of a 'NavText' change 293 boolean changeOwnTitle = shouldChangeTitle(ownProps); 294 295 String hasNavTextChange = null; 296 List<CmsProperty> ownPropertyChanges = new ArrayList<CmsProperty>(); 297 for (CmsPropertyModification propMod : changes.getChanges()) { 298 if (propMod.isFileNameProperty()) { 299 // in case of the file name property, the resource needs to be renamed 300 if ((m_overrideStructureId == null) && !resource.getStructureId().equals(propMod.getId())) { 301 if (propMod.getId() != null) { 302 throw new IllegalStateException("Invalid structure id in property changes."); 303 } 304 } 305 CmsResource.checkResourceName(propMod.getValue()); 306 String oldSitePath = CmsFileUtil.removeTrailingSeparator(cms.getSitePath(resource)); 307 String parentPath = CmsResource.getParentFolder(oldSitePath); 308 String newSitePath = CmsFileUtil.removeTrailingSeparator( 309 CmsStringUtil.joinPaths(parentPath, propMod.getValue())); 310 if (!oldSitePath.equals(newSitePath)) { 311 cms.moveResource(oldSitePath, newSitePath); 312 } 313 // read the resource again to update name and path 314 resource = cms.readResource(resource.getStructureId(), CmsResourceFilter.IGNORE_EXPIRATION); 315 } else { 316 CmsProperty propToModify = null; 317 if ((m_overrideStructureId != null) || resource.getStructureId().equals(propMod.getId())) { 318 319 if (CmsPropertyDefinition.PROPERTY_NAVTEXT.equals(propMod.getName())) { 320 hasNavTextChange = propMod.getValue(); 321 } else if (CmsPropertyDefinition.PROPERTY_TITLE.equals(propMod.getName())) { 322 changeOwnTitle = false; 323 } 324 propToModify = ownProps.get(propMod.getName()); 325 if (propToModify == null) { 326 propToModify = new CmsProperty(propMod.getName(), null, null); 327 } 328 ownPropertyChanges.add(propToModify); 329 } else { 330 throw new IllegalStateException("Invalid structure id in property changes!"); 331 } 332 String newValue = propMod.getValue(); 333 if (newValue == null) { 334 newValue = ""; 335 } 336 if (propMod.isStructureValue()) { 337 propToModify.setStructureValue(newValue); 338 } else { 339 propToModify.setResourceValue(newValue); 340 } 341 } 342 } 343 if (hasNavTextChange != null) { 344 if (changeOwnTitle) { 345 CmsProperty titleProp = ownProps.get(CmsPropertyDefinition.PROPERTY_TITLE); 346 if (titleProp == null) { 347 titleProp = new CmsProperty(CmsPropertyDefinition.PROPERTY_TITLE, null, null); 348 } 349 titleProp.setStructureValue(hasNavTextChange); 350 ownPropertyChanges.add(titleProp); 351 } 352 } 353 if (!ownPropertyChanges.isEmpty()) { 354 cms.writePropertyObjects(resource, ownPropertyChanges); 355 } 356 } finally { 357 if (actionRecord.getChange() == LockChange.locked) { 358 cms.unlockResource(resource); 359 } 360 } 361 if (m_updateIndex) { 362 OpenCms.getSearchManager().updateOfflineIndexes(); 363 } 364 365 } 366 367 /** 368 * Sets the 'update index' flag to control whether the index should be updated after saving. 369 * 370 * @param updateIndex true if the index should be updated after saving 371 */ 372 public void setUpdateIndex(boolean updateIndex) { 373 374 m_updateIndex = updateIndex; 375 } 376 377 /** 378 * Converts CmsProperty objects to CmsClientProperty objects.<p> 379 * 380 * @param properties a list of server-side properties 381 * 382 * @return a map of client-side properties 383 */ 384 protected Map<String, CmsClientProperty> convertProperties(List<CmsProperty> properties) { 385 386 Map<String, CmsClientProperty> result = new HashMap<String, CmsClientProperty>(); 387 for (CmsProperty prop : properties) { 388 CmsClientProperty clientProp = new CmsClientProperty( 389 prop.getName(), 390 prop.getStructureValue(), 391 prop.getResourceValue()); 392 clientProp.setOrigin(prop.getOrigin()); 393 result.put(clientProp.getName(), clientProp); 394 } 395 return result; 396 } 397 398 /** 399 * Helper method to get the default property configuration for the given resource type.<p> 400 * 401 * @param typeName the name of the resource type 402 * 403 * @return the default property configuration for the given type 404 */ 405 protected Map<String, CmsXmlContentProperty> getDefaultPropertiesForType(String typeName) { 406 407 Map<String, CmsXmlContentProperty> propertyConfig = new LinkedHashMap<String, CmsXmlContentProperty>(); 408 CmsExplorerTypeSettings explorerType = OpenCms.getWorkplaceManager().getExplorerTypeSetting(typeName); 409 if (explorerType != null) { 410 List<String> defaultProps = explorerType.getProperties(); 411 for (String propName : defaultProps) { 412 CmsXmlContentProperty property = new CmsXmlContentProperty( 413 propName, 414 "string", 415 "string", 416 "", 417 "", 418 "", 419 "", 420 null, 421 "", 422 "", 423 "false"); 424 propertyConfig.put(propName, property); 425 } 426 } 427 return propertyConfig; 428 } 429 430 /** 431 * Converts a list of properties to a map.<p> 432 * 433 * @param properties the list of properties 434 * 435 * @return a map from property names to properties 436 */ 437 protected Map<String, CmsProperty> getPropertiesByName(List<CmsProperty> properties) { 438 439 Map<String, CmsProperty> result = new HashMap<String, CmsProperty>(); 440 for (CmsProperty property : properties) { 441 String key = property.getName(); 442 result.put(key, property.clone()); 443 } 444 return result; 445 } 446 447 /** 448 * Returns whether the current user has write permissions, the resource is lockable or already locked by the current user and is in the current project.<p> 449 * 450 * @param cms the cms context 451 * @param resource the resource 452 * 453 * @return <code>true</code> if the resource is writable 454 * 455 * @throws CmsException in case checking the permissions fails 456 */ 457 protected boolean isWritable(CmsObject cms, CmsResource resource) throws CmsException { 458 459 boolean writable = cms.hasPermissions( 460 resource, 461 CmsPermissionSet.ACCESS_WRITE, 462 false, 463 CmsResourceFilter.IGNORE_EXPIRATION); 464 if (writable) { 465 CmsLock lock = cms.getLock(resource); 466 writable = lock.isUnlocked() || lock.isOwnedBy(cms.getRequestContext().getCurrentUser()); 467 if (writable) { 468 CmsResourceUtil resUtil = new CmsResourceUtil(cms, resource); 469 writable = resUtil.isInsideProject() && !resUtil.getProjectState().isLockedForPublishing(); 470 } 471 } 472 return writable; 473 } 474 475 /** 476 * Determines if the title property should be changed in case of a 'NavText' change.<p> 477 * 478 * @param properties the current resource properties 479 * 480 * @return <code>true</code> if the title property should be changed in case of a 'NavText' change 481 */ 482 private boolean shouldChangeTitle(Map<String, CmsProperty> properties) { 483 484 return (properties == null) 485 || (properties.get(CmsPropertyDefinition.PROPERTY_TITLE) == null) 486 || (properties.get(CmsPropertyDefinition.PROPERTY_TITLE).getValue() == null) 487 || ((properties.get(CmsPropertyDefinition.PROPERTY_NAVTEXT) != null) 488 && properties.get(CmsPropertyDefinition.PROPERTY_TITLE).getValue().equals( 489 properties.get(CmsPropertyDefinition.PROPERTY_NAVTEXT).getValue())); 490 } 491 492}