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}