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