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