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 GmbH & Co. KG, 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.workplace.editors;
029
030import org.opencms.ade.contenteditor.CmsContentTypeVisitor;
031import org.opencms.cache.CmsVfsMemoryObjectCache;
032import org.opencms.configuration.CmsParameterConfiguration;
033import org.opencms.db.CmsUserSettings;
034import org.opencms.file.CmsFile;
035import org.opencms.file.CmsObject;
036import org.opencms.file.CmsRequestContext;
037import org.opencms.file.CmsResource;
038import org.opencms.file.CmsResourceFilter;
039import org.opencms.file.CmsVfsResourceNotFoundException;
040import org.opencms.file.types.CmsResourceTypeXmlPage;
041import org.opencms.file.types.I_CmsResourceType;
042import org.opencms.main.CmsException;
043import org.opencms.main.CmsLog;
044import org.opencms.main.OpenCms;
045import org.opencms.util.CmsStringUtil;
046import org.opencms.workplace.explorer.CmsExplorerTypeSettings;
047import org.opencms.xml.content.CmsXmlContent;
048import org.opencms.xml.content.CmsXmlContentFactory;
049
050import java.io.ByteArrayInputStream;
051import java.util.ArrayList;
052import java.util.HashMap;
053import java.util.Iterator;
054import java.util.List;
055import java.util.Map;
056import java.util.SortedMap;
057import java.util.TreeMap;
058
059import org.apache.commons.logging.Log;
060
061/**
062 * The editor manager stores information about all available configured editors in OpenCms.<p>
063 *
064 * This class provides methods and constants to select the right editor according to:
065 * <ul>
066 * <li>the user preferences</li>
067 * <li>the users current browser</li>
068 * <li>the resource type</li>
069 * <li>the editor rankings</li>
070 * </ul>
071 * <p>
072 *
073 * @since 6.0.0
074 */
075public class CmsWorkplaceEditorManager {
076
077    /** The filename of the editor configuration XML file. */
078    public static final String EDITOR_CONFIGURATION_FILENAME = "editor_configuration.xml";
079
080    /** The filename of the editor JSP. */
081    public static final String EDITOR_FILENAME = "editor.jsp";
082
083    /** The log object for this class. */
084    private static final Log LOG = CmsLog.getLog(CmsWorkplaceEditorManager.class);
085
086    /** The editor configurations. */
087    private List<CmsWorkplaceEditorConfiguration> m_editorConfigurations;
088
089    /** The preferred editor configurations. */
090    private Map<String, CmsWorkplaceEditorConfiguration> m_preferredEditors;
091
092    /**
093     * Creates a new editor manager.<p>
094     *
095     * @param cms an OpenCms context object that must have been initialized with "Admin" permissions
096     */
097    public CmsWorkplaceEditorManager(CmsObject cms) {
098
099        // get all subfolders of the workplace editor folder
100        List<CmsResource> editorFolders;
101        try {
102            editorFolders = cms.getSubFolders(CmsEditor.PATH_EDITORS);
103        } catch (CmsException e) {
104            LOG.error(Messages.get().getBundle().key(Messages.LOG_READ_EDITIR_FOLDER_FAILED_1, CmsEditor.PATH_EDITORS));
105            // can not throw exception here since then OpenCms would not even start in shell mode (runlevel 2)
106            editorFolders = new ArrayList<CmsResource>();
107        }
108
109        m_editorConfigurations = new ArrayList<CmsWorkplaceEditorConfiguration>(editorFolders.size());
110
111        // try to read the configuration files and create configuration objects for valid configurations
112        Iterator<CmsResource> i = editorFolders.iterator();
113        while (i.hasNext()) {
114            CmsResource currentFolder = i.next();
115            String folderName = CmsEditor.PATH_EDITORS + currentFolder.getName();
116            if (!folderName.endsWith("/")) {
117                folderName += "/";
118            }
119            CmsFile configFile = null;
120            try {
121                configFile = cms.readFile(
122                    folderName + EDITOR_CONFIGURATION_FILENAME,
123                    CmsResourceFilter.IGNORE_EXPIRATION);
124            } catch (CmsException e) {
125                // no configuration file present, ignore this folder
126                if (LOG.isInfoEnabled()) {
127                    LOG.info(e);
128                }
129                continue;
130            }
131            // get the file contents
132            byte[] xmlData = configFile.getContents();
133            CmsWorkplaceEditorConfiguration editorConfig = new CmsWorkplaceEditorConfiguration(
134                xmlData,
135                folderName + EDITOR_FILENAME,
136                currentFolder.getName());
137            if (editorConfig.isValidConfiguration()) {
138                m_editorConfigurations.add(editorConfig);
139            }
140        }
141        m_preferredEditors = new HashMap<String, CmsWorkplaceEditorConfiguration>(m_editorConfigurations.size());
142    }
143
144    /**
145     * Checks whether GWT widgets are available for all fields of a content.<p>
146     *
147     * @param cms the current CMS context
148     * @param resource the resource to check
149     *
150     * @return false if for some fields the new Acacia widgets are not available
151     *
152     * @throws CmsException if something goes wrong
153     */
154    public static boolean checkAcaciaEditorAvailable(CmsObject cms, CmsResource resource) {
155
156        if (resource == null) {
157            try {
158                // we want a stack trace
159                throw new Exception();
160            } catch (Exception e) {
161                LOG.error("Can't check widget availability because resource is null!", e);
162            }
163            return false;
164        }
165        try {
166            CmsFile file = (resource instanceof CmsFile) ? (CmsFile)resource : cms.readFile(resource);
167            CmsXmlContent content = CmsXmlContentFactory.unmarshal(cms, file);
168            if (content.getContentDefinition().getContentHandler().isAcaciaEditorDisabled()) {
169                return false;
170            }
171            CmsContentTypeVisitor visitor = new CmsContentTypeVisitor(cms, file, cms.getRequestContext().getLocale());
172            return visitor.isEditorCompatible(content.getContentDefinition());
173        } catch (CmsException e) {
174            LOG.info("error thrown in checkAcaciaEditorAvailable for " + resource + " : " + e.getLocalizedMessage(), e);
175            return true;
176        }
177    }
178
179    /**
180     * Returns a map of configurable editors for the workplace preferences dialog.<p>
181     *
182     * This map has the resource type name as key, the value is a sorted map with
183     * the ranking as key and a CmsWorkplaceEditorConfiguration object as value.<p>
184     *
185     * @return configurable editors for the workplace preferences dialog
186     */
187    public Map<String, SortedMap<Float, CmsWorkplaceEditorConfiguration>> getConfigurableEditors() {
188
189        Map<String, SortedMap<Float, CmsWorkplaceEditorConfiguration>> configurableEditors = new HashMap<String, SortedMap<Float, CmsWorkplaceEditorConfiguration>>();
190        Iterator<CmsWorkplaceEditorConfiguration> i = m_editorConfigurations.iterator();
191        while (i.hasNext()) {
192            CmsWorkplaceEditorConfiguration currentConfig = i.next();
193            // get all resource types specified for the current editor configuration
194            Iterator<String> k = currentConfig.getResourceTypes().keySet().iterator();
195            while (k.hasNext()) {
196                // key is the current resource type of the configuration
197                String key = k.next();
198
199                // check if the current resource type is only a reference to another resource type
200                CmsExplorerTypeSettings settings = OpenCms.getWorkplaceManager().getExplorerTypeSetting(key);
201                if ((settings == null) || CmsStringUtil.isNotEmpty(settings.getReference())) {
202                    // skip this resource type
203                    continue;
204                }
205
206                if ((currentConfig.getMappingForResourceType(key) == null)
207                    || currentConfig.getMappingForResourceType(key).equals(key)) {
208                    // editor is configurable for specified resource type
209                    SortedMap<Float, CmsWorkplaceEditorConfiguration> editorConfigs = configurableEditors.get(key);
210                    if (editorConfigs == null) {
211                        // no configuration map present for resource type, create one
212                        editorConfigs = new TreeMap<Float, CmsWorkplaceEditorConfiguration>();
213                    }
214                    // put the current editor configuration to the resource map with ranking value as key
215                    editorConfigs.put(new Float(currentConfig.getRankingForResourceType(key)), currentConfig);
216                    // put the resource map to the result map with resource type as key
217                    configurableEditors.put(key, editorConfigs);
218                }
219            }
220        }
221        return configurableEditors;
222    }
223
224    /**
225     * Gets the editor configuration with the given name.<p>
226     *
227     * @param name the name of the editor configuration
228     *
229     * @return the editor configuration
230     */
231    public CmsWorkplaceEditorConfiguration getEditorConfiguration(String name) {
232
233        for (CmsWorkplaceEditorConfiguration config : m_editorConfigurations) {
234            if (name.equals(config.getName())) {
235                return config;
236            }
237        }
238        return null;
239    }
240
241    /**
242     * Gets the value of a global editor configuration parameter.
243     *
244     * @param cms the CMS context
245     * @param editor the editor name
246     * @param param the name of the parameter
247     *
248     * @return the editor parameter value
249     */
250    public String getEditorParameter(CmsObject cms, String editor, String param) {
251
252        String path = OpenCms.getSystemInfo().getConfigFilePath(cms, "editors/" + editor + ".properties");
253        CmsVfsMemoryObjectCache cache = CmsVfsMemoryObjectCache.getVfsMemoryObjectCache();
254        CmsParameterConfiguration config = (CmsParameterConfiguration)cache.getCachedObject(cms, path);
255        if (config == null) {
256            try {
257                CmsFile file = cms.readFile(path);
258                try (ByteArrayInputStream input = new ByteArrayInputStream(file.getContents())) {
259                    config = new CmsParameterConfiguration(input); // Uses ISO-8859-1, should be OK for config parameters
260                    cache.putCachedObject(cms, path, config);
261                }
262            } catch (CmsVfsResourceNotFoundException e) {
263                return null;
264            } catch (Exception e) {
265                LOG.error(e.getLocalizedMessage(), e);
266                return null;
267            }
268        }
269        return config.getString(param, null);
270    }
271
272    /**
273     * Returns the editor URI for the current resource type.<p>
274     *
275     * @param context the request context
276     * @param userAgent the user agent String that identifies the browser
277     * @return a valid editor URI for the resource type or null, if no editor matches
278     */
279    public String getWidgetEditor(CmsRequestContext context, String userAgent) {
280
281        // step 1: check if the user specified a preferred editor for the resource type xmlpage
282        CmsUserSettings settings = new CmsUserSettings(context.getCurrentUser());
283        String resourceType = CmsResourceTypeXmlPage.getStaticTypeName();
284        String preferredEditorSetting = settings.getPreferredEditor(resourceType);
285        if (preferredEditorSetting == null) {
286            // no preferred editor setting found for this resource type, look for mapped resource type preferred editor
287            Iterator<CmsWorkplaceEditorConfiguration> i = m_editorConfigurations.iterator();
288            while (i.hasNext()) {
289                CmsWorkplaceEditorConfiguration currentConfig = i.next();
290                String mapping = currentConfig.getMappingForResourceType(resourceType);
291                if (mapping != null) {
292                    preferredEditorSetting = settings.getPreferredEditor(mapping);
293                }
294                if (preferredEditorSetting != null) {
295                    break;
296                }
297            }
298        }
299        if (preferredEditorSetting != null) {
300            CmsWorkplaceEditorConfiguration preferredConf = filterPreferredEditor(preferredEditorSetting);
301            if ((preferredConf != null) && preferredConf.isWidgetEditor() && preferredConf.matchesBrowser(userAgent)) {
302                // return preferred editor only if it matches the current users browser
303                return preferredConf.getWidgetEditor();
304            }
305        }
306
307        // step 2: filter editors for the given resoure type
308        SortedMap<Float, CmsWorkplaceEditorConfiguration> filteredEditors = filterEditorsForResourceType(resourceType);
309
310        // step 3: check if one of the editors matches the current users browser
311        while (filteredEditors.size() > 0) {
312            // check editor configuration with highest ranking
313            Float key = filteredEditors.lastKey();
314            CmsWorkplaceEditorConfiguration conf = filteredEditors.get(key);
315            if (conf.isWidgetEditor() && conf.matchesBrowser(userAgent)) {
316                return conf.getWidgetEditor();
317            }
318            filteredEditors.remove(key);
319        }
320
321        // no valid editor found
322        return null;
323    }
324
325    /**
326     * Checks if there is an editor which can process the given resource.<p>
327     *
328     * @param res the resource
329     *
330     * @return true if the given resource can be edited with one of the configured editors
331     */
332    public boolean isEditorAvailableForResource(CmsResource res) {
333
334        I_CmsResourceType type = OpenCms.getResourceManager().getResourceType(res);
335        String typeName = type.getTypeName();
336        for (CmsWorkplaceEditorConfiguration editorConfig : m_editorConfigurations) {
337            if (editorConfig.matchesResourceType(typeName)) {
338                return true;
339            }
340        }
341        return false;
342    }
343
344    /**
345     * Returns the default editor URI for the current resource type.<p>
346     *
347     * @param context the request context
348     * @param resourceType the current resource type
349     * @param userAgent the user agent String that identifies the browser
350     * @return a valid default editor URI for the resource type or null, if no editor matches
351     */
352    protected String getDefaultEditorUri(CmsRequestContext context, String resourceType, String userAgent) {
353
354        SortedMap<Float, CmsWorkplaceEditorConfiguration> filteredEditors = filterEditorsForResourceType(resourceType);
355        while (filteredEditors.size() > 0) {
356            // get the configuration with the lowest key value from the map
357            Float key = filteredEditors.firstKey();
358            CmsWorkplaceEditorConfiguration conf = filteredEditors.get(key);
359            // match the found configuration with the current users browser
360            if (conf.matchesBrowser(userAgent)) {
361                return conf.getEditorUri();
362            }
363            filteredEditors.remove(key);
364        }
365        if (context == null) {
366            // this is just so that all parameters are used, signature should be identical to getEditorUri(...)
367            return null;
368        }
369        // no valid default editor found
370        return null;
371    }
372
373    /**
374     * Returns the editor configuration objects.<p>
375     *
376     * @return the editor configuration objects
377     */
378    protected List<CmsWorkplaceEditorConfiguration> getEditorConfigurations() {
379
380        return m_editorConfigurations;
381    }
382
383    /**
384     * Returns the editor URI for the current resource type.<p>
385     *
386     * @param context the request context
387     * @param resourceType the current resource type
388     * @param userAgent the user agent String that identifies the browser
389     * @return a valid editor URI for the resource type or null, if no editor matches
390     */
391    protected String getEditorUri(CmsRequestContext context, String resourceType, String userAgent) {
392
393        // step 1: check if the user specified a preferred editor for the given resource type
394        CmsUserSettings settings = new CmsUserSettings(context.getCurrentUser());
395        String preferredEditorSetting = settings.getPreferredEditor(resourceType);
396        if (preferredEditorSetting == null) {
397            // no preferred editor setting found for this resource type, look for mapped resource type preferred editor
398            Iterator<CmsWorkplaceEditorConfiguration> i = m_editorConfigurations.iterator();
399            while (i.hasNext()) {
400                CmsWorkplaceEditorConfiguration currentConfig = i.next();
401                String mapping = currentConfig.getMappingForResourceType(resourceType);
402                if (mapping != null) {
403                    preferredEditorSetting = settings.getPreferredEditor(mapping);
404                }
405                if (preferredEditorSetting != null) {
406                    break;
407                }
408            }
409        }
410        if (preferredEditorSetting != null) {
411            CmsWorkplaceEditorConfiguration preferredConf = filterPreferredEditor(preferredEditorSetting);
412            if ((preferredConf != null) && preferredConf.matchesBrowser(userAgent)) {
413                // return preferred editor only if it matches the current users browser
414                return preferredConf.getEditorUri();
415            }
416        }
417
418        // step 2: filter editors for the given resoure type
419        SortedMap<Float, CmsWorkplaceEditorConfiguration> filteredEditors = filterEditorsForResourceType(resourceType);
420
421        // step 3: check if one of the editors matches the current users browser
422        while (filteredEditors.size() > 0) {
423            // check editor configuration with highest ranking
424            Float key = filteredEditors.lastKey();
425            CmsWorkplaceEditorConfiguration conf = filteredEditors.get(key);
426            if (conf.matchesBrowser(userAgent)) {
427                return conf.getEditorUri();
428            }
429            filteredEditors.remove(key);
430        }
431
432        // no valid editor found
433        return null;
434    }
435
436    /**
437     * Filters the matching editors for the given resource type from the list of all available editors.<p>
438     *
439     * @param resourceType the resource type to filter
440     * @return a map of filtered editor configurations sorted asceding by the ranking for the current resource type, with the (Float) ranking as key
441     */
442    private SortedMap<Float, CmsWorkplaceEditorConfiguration> filterEditorsForResourceType(String resourceType) {
443
444        SortedMap<Float, CmsWorkplaceEditorConfiguration> filteredEditors = new TreeMap<Float, CmsWorkplaceEditorConfiguration>();
445        Iterator<CmsWorkplaceEditorConfiguration> i = m_editorConfigurations.iterator();
446        while (i.hasNext()) {
447            CmsWorkplaceEditorConfiguration currentConfig = i.next();
448            if (currentConfig.matchesResourceType(resourceType)) {
449                float key = currentConfig.getRankingForResourceType(resourceType);
450                if (key >= 0) {
451                    filteredEditors.put(new Float(key), currentConfig);
452                }
453            }
454        }
455        return filteredEditors;
456    }
457
458    /**
459     * Filters the preferred editor from the list of all available editors.<p>
460     *
461     * @param preferredEditor the preferred editor identification String
462     * @return the preferred editor configuration object or null, if none is found
463     */
464    private CmsWorkplaceEditorConfiguration filterPreferredEditor(String preferredEditor) {
465
466        if (m_preferredEditors.size() == 0) {
467            Iterator<CmsWorkplaceEditorConfiguration> i = m_editorConfigurations.iterator();
468            while (i.hasNext()) {
469                CmsWorkplaceEditorConfiguration currentConfig = i.next();
470                m_preferredEditors.put(currentConfig.getEditorUri(), currentConfig);
471            }
472        }
473        return m_preferredEditors.get(preferredEditor);
474    }
475
476}