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 propNames.add(propDef.getName()); 243 } 244 CmsTemplateFinder templateFinder = new CmsTemplateFinder(cms); 245 result.setTemplates(templateFinder.getTemplates()); 246 result.setAllProperties(propNames); 247 result.setStructureId(id); 248 result.setSitePath(sitePath); 249 return result; 250 } finally { 251 cms.getRequestContext().setSiteRoot(originalSiteRoot); 252 } 253 } 254 255 /** 256 * Sets a structure id that overrides the one stored in a property change set.<p> 257 * 258 * @param structureId the new structure id 259 */ 260 public void overrideStructureId(CmsUUID structureId) { 261 262 m_overrideStructureId = structureId; 263 } 264 265 /** 266 * Saves a set of property changes.<p> 267 * 268 * @param changes the set of property changes 269 * @throws CmsException if something goes wrong 270 */ 271 public void saveProperties(CmsPropertyChangeSet changes) throws CmsException { 272 273 CmsObject cms = m_cms; 274 CmsUUID structureId = changes.getTargetStructureId(); 275 if (m_overrideStructureId != null) { 276 structureId = m_overrideStructureId; 277 } 278 CmsResource resource = cms.readResource(structureId, CmsResourceFilter.IGNORE_EXPIRATION); 279 boolean shallow = true; 280 for (CmsPropertyModification propMode : changes.getChanges()) { 281 if (propMode.isFileNameProperty()) { 282 shallow = false; 283 } 284 } 285 CmsLockActionRecord actionRecord = CmsLockUtil.ensureLock(cms, resource, shallow); 286 try { 287 Map<String, CmsProperty> ownProps = getPropertiesByName(cms.readPropertyObjects(resource, false)); 288 // determine if the title property should be changed in case of a 'NavText' change 289 boolean changeOwnTitle = shouldChangeTitle(ownProps); 290 291 String hasNavTextChange = null; 292 List<CmsProperty> ownPropertyChanges = new ArrayList<CmsProperty>(); 293 for (CmsPropertyModification propMod : changes.getChanges()) { 294 if (propMod.isFileNameProperty()) { 295 // in case of the file name property, the resource needs to be renamed 296 if ((m_overrideStructureId == null) && !resource.getStructureId().equals(propMod.getId())) { 297 if (propMod.getId() != null) { 298 throw new IllegalStateException("Invalid structure id in property changes."); 299 } 300 } 301 CmsResource.checkResourceName(propMod.getValue()); 302 String oldSitePath = CmsFileUtil.removeTrailingSeparator(cms.getSitePath(resource)); 303 String parentPath = CmsResource.getParentFolder(oldSitePath); 304 String newSitePath = CmsFileUtil.removeTrailingSeparator( 305 CmsStringUtil.joinPaths(parentPath, propMod.getValue())); 306 if (!oldSitePath.equals(newSitePath)) { 307 cms.moveResource(oldSitePath, newSitePath); 308 } 309 // read the resource again to update name and path 310 resource = cms.readResource(resource.getStructureId(), CmsResourceFilter.IGNORE_EXPIRATION); 311 } else { 312 CmsProperty propToModify = null; 313 if ((m_overrideStructureId != null) || resource.getStructureId().equals(propMod.getId())) { 314 315 if (CmsPropertyDefinition.PROPERTY_NAVTEXT.equals(propMod.getName())) { 316 hasNavTextChange = propMod.getValue(); 317 } else if (CmsPropertyDefinition.PROPERTY_TITLE.equals(propMod.getName())) { 318 changeOwnTitle = false; 319 } 320 propToModify = ownProps.get(propMod.getName()); 321 if (propToModify == null) { 322 propToModify = new CmsProperty(propMod.getName(), null, null); 323 } 324 ownPropertyChanges.add(propToModify); 325 } else { 326 throw new IllegalStateException("Invalid structure id in property changes!"); 327 } 328 String newValue = propMod.getValue(); 329 if (newValue == null) { 330 newValue = ""; 331 } 332 if (propMod.isStructureValue()) { 333 propToModify.setStructureValue(newValue); 334 } else { 335 propToModify.setResourceValue(newValue); 336 } 337 } 338 } 339 if (hasNavTextChange != null) { 340 if (changeOwnTitle) { 341 CmsProperty titleProp = ownProps.get(CmsPropertyDefinition.PROPERTY_TITLE); 342 if (titleProp == null) { 343 titleProp = new CmsProperty(CmsPropertyDefinition.PROPERTY_TITLE, null, null); 344 } 345 titleProp.setStructureValue(hasNavTextChange); 346 ownPropertyChanges.add(titleProp); 347 } 348 } 349 if (!ownPropertyChanges.isEmpty()) { 350 cms.writePropertyObjects(resource, ownPropertyChanges); 351 } 352 } finally { 353 if (actionRecord.getChange() == LockChange.locked) { 354 cms.unlockResource(resource); 355 } 356 } 357 if (m_updateIndex) { 358 OpenCms.getSearchManager().updateOfflineIndexes(); 359 } 360 361 } 362 363 /** 364 * Sets the 'update index' flag to control whether the index should be updated after saving. 365 * 366 * @param updateIndex true if the index should be updated after saving 367 */ 368 public void setUpdateIndex(boolean updateIndex) { 369 370 m_updateIndex = updateIndex; 371 } 372 373 /** 374 * Converts CmsProperty objects to CmsClientProperty objects.<p> 375 * 376 * @param properties a list of server-side properties 377 * 378 * @return a map of client-side properties 379 */ 380 protected Map<String, CmsClientProperty> convertProperties(List<CmsProperty> properties) { 381 382 Map<String, CmsClientProperty> result = new HashMap<String, CmsClientProperty>(); 383 for (CmsProperty prop : properties) { 384 CmsClientProperty clientProp = new CmsClientProperty( 385 prop.getName(), 386 prop.getStructureValue(), 387 prop.getResourceValue()); 388 clientProp.setOrigin(prop.getOrigin()); 389 result.put(clientProp.getName(), clientProp); 390 } 391 return result; 392 } 393 394 /** 395 * Helper method to get the default property configuration for the given resource type.<p> 396 * 397 * @param typeName the name of the resource type 398 * 399 * @return the default property configuration for the given type 400 */ 401 protected Map<String, CmsXmlContentProperty> getDefaultPropertiesForType(String typeName) { 402 403 Map<String, CmsXmlContentProperty> propertyConfig = new LinkedHashMap<String, CmsXmlContentProperty>(); 404 CmsExplorerTypeSettings explorerType = OpenCms.getWorkplaceManager().getExplorerTypeSetting(typeName); 405 if (explorerType != null) { 406 List<String> defaultProps = explorerType.getProperties(); 407 for (String propName : defaultProps) { 408 CmsXmlContentProperty property = new CmsXmlContentProperty( 409 propName, 410 "string", 411 "string", 412 "", 413 "", 414 "", 415 "", 416 null, 417 "", 418 "", 419 "false"); 420 propertyConfig.put(propName, property); 421 } 422 } 423 return propertyConfig; 424 } 425 426 /** 427 * Converts a list of properties to a map.<p> 428 * 429 * @param properties the list of properties 430 * 431 * @return a map from property names to properties 432 */ 433 protected Map<String, CmsProperty> getPropertiesByName(List<CmsProperty> properties) { 434 435 Map<String, CmsProperty> result = new HashMap<String, CmsProperty>(); 436 for (CmsProperty property : properties) { 437 String key = property.getName(); 438 result.put(key, property.clone()); 439 } 440 return result; 441 } 442 443 /** 444 * 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> 445 * 446 * @param cms the cms context 447 * @param resource the resource 448 * 449 * @return <code>true</code> if the resource is writable 450 * 451 * @throws CmsException in case checking the permissions fails 452 */ 453 protected boolean isWritable(CmsObject cms, CmsResource resource) throws CmsException { 454 455 boolean writable = cms.hasPermissions( 456 resource, 457 CmsPermissionSet.ACCESS_WRITE, 458 false, 459 CmsResourceFilter.IGNORE_EXPIRATION); 460 if (writable) { 461 CmsLock lock = cms.getLock(resource); 462 writable = lock.isUnlocked() || lock.isOwnedBy(cms.getRequestContext().getCurrentUser()); 463 if (writable) { 464 CmsResourceUtil resUtil = new CmsResourceUtil(cms, resource); 465 writable = resUtil.isInsideProject() && !resUtil.getProjectState().isLockedForPublishing(); 466 } 467 } 468 return writable; 469 } 470 471 /** 472 * Determines if the title property should be changed in case of a 'NavText' change.<p> 473 * 474 * @param properties the current resource properties 475 * 476 * @return <code>true</code> if the title property should be changed in case of a 'NavText' change 477 */ 478 private boolean shouldChangeTitle(Map<String, CmsProperty> properties) { 479 480 return (properties == null) 481 || (properties.get(CmsPropertyDefinition.PROPERTY_TITLE) == null) 482 || (properties.get(CmsPropertyDefinition.PROPERTY_TITLE).getValue() == null) 483 || ((properties.get(CmsPropertyDefinition.PROPERTY_NAVTEXT) != null) 484 && properties.get(CmsPropertyDefinition.PROPERTY_TITLE).getValue().equals( 485 properties.get(CmsPropertyDefinition.PROPERTY_NAVTEXT).getValue())); 486 } 487 488}