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.tools.content;
029
030import org.opencms.file.CmsObject;
031import org.opencms.i18n.CmsMessageContainer;
032import org.opencms.main.CmsIllegalArgumentException;
033import org.opencms.main.CmsLog;
034import org.opencms.util.CmsStringUtil;
035
036import java.util.Comparator;
037import java.util.Iterator;
038import java.util.List;
039import java.util.Map;
040import java.util.Set;
041import java.util.SortedMap;
042import java.util.TreeMap;
043import java.util.TreeSet;
044import java.util.Vector;
045
046import org.apache.commons.logging.Log;
047
048import org.htmlparser.Attribute;
049import org.htmlparser.NodeFactory;
050import org.htmlparser.PrototypicalNodeFactory;
051import org.htmlparser.Tag;
052import org.htmlparser.util.ParserException;
053
054/**
055 * Bean to hold the settings needed for the operation of replacing HTML Tags of xmlpage resources in
056 * the OpenCms VFS.
057 * <p>
058 *
059 * @since 6.1.7
060 *
061 */
062public final class CmsTagReplaceSettings {
063
064    /**
065     * Property for the tag-replace contentool to know the files that have been processed before in
066     * case of early terminaton in previous runs.
067     */
068    public static final String PROPERTY_CONTENTOOLS_TAGREPLACE = "contentools.tagreplace";
069
070    /** The log object for this class. */
071    private static final Log LOG = CmsLog.getLog(CmsTagReplaceSettings.class);
072
073    /** Needed to verify if a path String denotes a folder in VFS. */
074    private final CmsObject m_cms;
075
076    /** The tags that should be deleted. */
077    private final Set m_deleteTags;
078
079    /** Used to create Tag instances for tags to delete of the proper type in a convenient way. */
080    private NodeFactory m_nodeFactory;
081
082    /**
083     * The value of the shared {@link #PROPERTY_CONTENTOOLS_TAGREPLACE} to set on resources that
084     * have been processed with these settings.
085     */
086    private String m_propertyValueTagReplaceID;
087
088    /**
089     * A map containing lower case tag names of tags to replace as keys and the replacement tag
090     * names as their corresponding values.
091     */
092    private SortedMap m_tags2replacementTags;
093
094    /** The root of all content files to process. */
095    private String m_workPath;
096
097    /**
098     * Bean constructor with cms object for path validation.
099     * <p>
100     *
101     * @param cms used to test the working path for valididty.
102     */
103    public CmsTagReplaceSettings(CmsObject cms) {
104
105        // Treemap guarantees no duplicate keys (ambiguous replacements) and the same default
106        // property ID's for the same replacement strings due to the ordering:
107        m_tags2replacementTags = new TreeMap();
108        m_cms = cms;
109        // all tags are registered for creation
110        m_nodeFactory = new PrototypicalNodeFactory();
111        m_deleteTags = new TreeSet(new Comparator() {
112
113            public int compare(Object o1, Object o2) {
114
115                return o1.getClass().getName().compareTo(o2.getClass().getName());
116            }
117        });
118    }
119
120    /**
121     * Returns the value of the shared {@link #PROPERTY_CONTENTOOLS_TAGREPLACE} to set on resources
122     * that have been processed with these settings.
123     * <p>
124     *
125     * @return the value of the shared {@link #PROPERTY_CONTENTOOLS_TAGREPLACE} to set on resources
126     *         that have been processed with these settings.
127     */
128    public String getPropertyValueTagReplaceID() {
129
130        return m_propertyValueTagReplaceID;
131    }
132
133    /**
134     * Returns the replacements to perform in form of a comma-separated List of "key=value" tokens.
135     * <p>
136     *
137     * @return the replacements to perform in form of a comma-separated List of "key=value" tokens.
138     */
139    public SortedMap getReplacements() {
140
141        return m_tags2replacementTags;
142    }
143
144    /**
145     * Returns the path under which files will be processed recursively.
146     * <p>
147     *
148     * @return the path under which files will be processed recursively.
149     */
150    public String getWorkPath() {
151
152        return m_workPath;
153    }
154
155    /**
156     * Sets the value of the shared {@link #PROPERTY_CONTENTOOLS_TAGREPLACE} to set on resources
157     * that have been processed with these settings.
158     * <p>
159     *
160     * @param propertyValueTagreplaceID the value of the shared
161     *            {@link #PROPERTY_CONTENTOOLS_TAGREPLACE} to set on resources that have been
162     *            processed with these settings.
163     *
164     * @throws CmsIllegalArgumentException if the argument is not valid.
165     */
166    public void setPropertyValueTagReplaceID(String propertyValueTagreplaceID) throws CmsIllegalArgumentException {
167
168        if (CmsStringUtil.isEmptyOrWhitespaceOnly(propertyValueTagreplaceID)) {
169            m_propertyValueTagReplaceID = getDefaultTagReplaceID();
170        } else {
171            m_propertyValueTagReplaceID = propertyValueTagreplaceID;
172        }
173    }
174
175    /**
176     * Sets the replacements to perform in form of a comma-separated List of "key=value" tokens.
177     * <p>
178     *
179     * @param replacements the replacements to perform in form of a comma-separated List of
180     *            "key=value" tokens.
181     *
182     * @throws CmsIllegalArgumentException if the argument is not valid.
183     */
184    public void setReplacements(SortedMap replacements) throws CmsIllegalArgumentException {
185
186        Iterator itMappings = replacements.entrySet().iterator();
187        Map.Entry entry;
188        String key, value;
189        while (itMappings.hasNext()) {
190            entry = (Map.Entry)itMappings.next();
191            key = (String)entry.getKey();
192            value = (String)entry.getValue();
193            if (CmsStringUtil.isEmptyOrWhitespaceOnly(value)) {
194                // removal
195                Tag deleteTag;
196                String tagName = (key).toLowerCase().trim();
197                try {
198                    Vector attributeList = new Vector(1);
199                    Attribute tagNameAttribute = new Attribute();
200                    tagNameAttribute.setName(tagName);
201                    attributeList.add(tagNameAttribute);
202                    deleteTag = m_nodeFactory.createTagNode(null, 0, 0, attributeList);
203                    m_deleteTags.add(deleteTag);
204                    itMappings.remove();
205                } catch (ParserException e) {
206                    CmsMessageContainer container = Messages.get().container(
207                        Messages.GUI_ERR_TAGREPLACE_TAGNAME_INVALID_1,
208                        tagName);
209                    throw new CmsIllegalArgumentException(container, e);
210                }
211            } else {
212                // nop
213            }
214            m_tags2replacementTags = replacements;
215        }
216        // if setPropertyValueTagReplaceID has been invoked earlier with empty value
217        // due to missing user input:
218        if (CmsStringUtil.isEmptyOrWhitespaceOnly(m_propertyValueTagReplaceID)) {
219            // trigger computation of default ID by empty value:
220            setPropertyValueTagReplaceID(null);
221        }
222    }
223
224    /**
225     * Sets the path under which files will be processed recursively.
226     * <p>
227     *
228     * @param workPath the path under which files will be processed recursively.
229     *
230     * @throws CmsIllegalArgumentException if the argument is not valid.
231     */
232    public void setWorkPath(String workPath) throws CmsIllegalArgumentException {
233
234        if (CmsStringUtil.isEmptyOrWhitespaceOnly(workPath)) {
235            throw new CmsIllegalArgumentException(Messages.get().container(Messages.GUI_ERR_WIDGETVALUE_EMPTY_0));
236        }
237        // test if it is a valid path:
238        if (!m_cms.existsResource(workPath)) {
239            throw new CmsIllegalArgumentException(
240                Messages.get().container(Messages.GUI_ERR_TAGREPLACE_WORKPATH_1, workPath));
241        }
242        m_workPath = workPath;
243
244    }
245
246    /**
247     * Returns the Set&lt;{@link org.htmlparser.Tag}&gt; to delete from transformed output.
248     * <p>
249     *
250     * @return the Set&lt;{@link org.htmlparser.Tag}&gt; to delete from transformed output.
251     */
252    protected Set getDeleteTags() {
253
254        return m_deleteTags;
255    }
256
257    /**
258     * Transforms the given Tag into the one it has to become by changing it's name and/or
259     * attributes.
260     * <p>
261     *
262     * @param tag the tag to be transformed.
263     *
264     * @return true if the given tag was modified, false else.
265     *
266     */
267    protected boolean replace(org.htmlparser.Tag tag) {
268
269        boolean result = false;
270        String tagName = tag.getTagName().trim().toLowerCase();
271        String replacementName = (String)m_tags2replacementTags.get(tagName);
272        if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(replacementName)) {
273            // judge this as a bug: getTagName() returns plain name, setter needs leading '/' for
274            // TODO: when updating htmlparser, verify if this has changed / been fixed
275            // closing tags
276            if (tag.isEndTag()) {
277                replacementName = "/" + replacementName;
278            }
279            tag.setTagName(replacementName);
280            result = true;
281            // clear the attributes too:
282            List attributes = tag.getAttributesEx();
283            Iterator itAttribs = attributes.iterator();
284            // skip the "tagname attribute"....
285            itAttribs.next();
286            Attribute attribute;
287            String attName;
288            while (itAttribs.hasNext()) {
289                attribute = (Attribute)itAttribs.next();
290                attName = attribute.getName();
291                if (CmsStringUtil.isEmptyOrWhitespaceOnly(attName)) {
292                    // this is the case for e.g. <h1 >
293                    // -> becomes a tag with an attribute for tag name and a null name attribute
294                    // (for the whitespace!)
295                } else {
296                    if (LOG.isDebugEnabled()) {
297                        LOG.debug(
298                            Messages.get().getBundle().key(
299                                Messages.LOG_DEBUG_TAGREPLACE_TAG_REMOVE_ATTRIB_2,
300                                attName,
301                                tag.getTagName()));
302
303                    }
304                    itAttribs.remove();
305                    if (LOG.isDebugEnabled()) {
306                        LOG.debug(Messages.get().getBundle().key(Messages.LOG_DEBUG_TAGREPLACE_TAG_REMOVE_ATTRIB_OK_0));
307                    }
308                }
309            }
310        }
311        return result;
312    }
313
314    /**
315     * Computes the default property value for resources that have to be marked as "processed by
316     * these replacement settings".
317     * <p>
318     *
319     * The default value will be the alphabetically sorted string for replacments or the empty
320     * String if the replacements have not been set before.
321     * <p>
322     *
323     * @return the default property value for resources that have to be marked as "processed by
324     *         these replacement settings".
325     */
326    private String getDefaultTagReplaceID() {
327
328        if (m_tags2replacementTags.size() == 0) {
329            return ""; // to know that no replacements were set before and the ID will still have
330            // to be computed later
331        } else {
332            StringBuffer result = new StringBuffer();
333            Map.Entry entry;
334            Iterator itEntries = m_tags2replacementTags.entrySet().iterator();
335            while (itEntries.hasNext()) {
336                entry = (Map.Entry)itEntries.next();
337                result.append(entry.getKey()).append('=').append(entry.getValue());
338                if (itEntries.hasNext()) {
339                    result.append(',');
340                }
341            }
342            return result.toString();
343
344        }
345    }
346}