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}