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}