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.search.fields;
029
030import org.opencms.file.CmsObject;
031import org.opencms.file.CmsProject;
032import org.opencms.file.CmsProperty;
033import org.opencms.file.CmsResource;
034import org.opencms.file.CmsUser;
035import org.opencms.file.I_CmsResource;
036import org.opencms.i18n.CmsMessageContainer;
037import org.opencms.main.CmsLog;
038import org.opencms.main.CmsRuntimeException;
039import org.opencms.search.CmsSearchUtil;
040import org.opencms.search.Messages;
041import org.opencms.search.extractors.I_CmsExtractionResult;
042import org.opencms.util.CmsStringUtil;
043import org.opencms.xml.CmsXmlUtils;
044
045import java.text.ParseException;
046import java.util.ArrayList;
047import java.util.Arrays;
048import java.util.Comparator;
049import java.util.Date;
050import java.util.List;
051import java.util.Locale;
052import java.util.Map;
053import java.util.SortedMap;
054import java.util.TreeMap;
055
056import org.apache.commons.logging.Log;
057import org.apache.lucene.document.DateTools;
058
059/**
060 * Describes a mapping of a piece of content from an OpenCms VFS resource to a field of a search index.<p>
061 *
062 * @since 7.0.0
063 */
064public class CmsSearchFieldMapping implements I_CmsSearchFieldMapping {
065
066    /** The log object for this class. */
067    private static final Log LOG = CmsLog.getLog(CmsSearchFieldMapping.class);
068
069    /** Default for expiration date since Long.MAX_VALUE is to big. */
070    private static final String DATE_EXPIRED_DEFAULT_STR = "21000101";
071
072    /** The default expiration date. */
073    private static Date m_defaultDateExpired;
074
075    /** Serial version UID. */
076    private static final long serialVersionUID = 3016384419639743033L;
077
078    /** The configured default value. */
079    private String m_defaultValue;
080
081    /** Pre-calculated hash value. */
082    private int m_hashCode;
083
084    /** The locale to extract content items in. */
085    protected Locale m_locale;
086
087    /** The parameter for the mapping type. */
088    private String m_param;
089
090    /** The mapping type. */
091    private CmsSearchFieldMappingType m_type;
092
093    /** Flag, indicating if the mapping applies to a lucene index. */
094    private boolean m_isLucene;
095
096    /**
097     * Public constructor for a new search field mapping.<p>
098     */
099    public CmsSearchFieldMapping() {
100
101        // no initialization required
102    }
103
104    /**
105     * Public constructor for a new search field mapping.<p>
106     *
107     * @param isLucene flag, indicating if the mapping is done for a lucene index
108     */
109    public CmsSearchFieldMapping(boolean isLucene) {
110
111        this();
112        m_isLucene = isLucene;
113    }
114
115    /**
116     * Public constructor for a new search field mapping.<p>
117     *
118     * @param type the type to use, see {@link #setType(CmsSearchFieldMappingType)}
119     * @param param the mapping parameter, see {@link #setParam(String)}
120     */
121    public CmsSearchFieldMapping(CmsSearchFieldMappingType type, String param) {
122
123        this();
124        setType(type);
125        setParam(param);
126    }
127
128    /**
129     * Public constructor for a new search field mapping.<p>
130     *
131     * @param type the type to use, see {@link #setType(CmsSearchFieldMappingType)}
132     * @param param the mapping parameter, see {@link #setParam(String)}
133     * @param isLucene flag, indicating if the mapping is done for a lucene index
134     */
135    public CmsSearchFieldMapping(CmsSearchFieldMappingType type, String param, boolean isLucene) {
136
137        this(type, param);
138        m_isLucene = isLucene;
139    }
140
141    /**
142     * Returns the default expiration date, meaning the resource never expires.<p>
143     *
144     * @return the default expiration date
145     *
146     * @throws ParseException if something goes wrong parsing the default date string
147     */
148    public static Date getDefaultDateExpired() throws ParseException {
149
150        if (m_defaultDateExpired == null) {
151            m_defaultDateExpired = DateTools.stringToDate("21000101");
152        }
153        return m_defaultDateExpired;
154    }
155
156    /**
157     * Two mappings are equal if the type and the parameter is equal.<p>
158     *
159     * @see java.lang.Object#equals(java.lang.Object)
160     */
161    @Override
162    public boolean equals(Object obj) {
163
164        if (obj == this) {
165            return true;
166        }
167        if ((obj instanceof I_CmsSearchFieldMapping)) {
168            I_CmsSearchFieldMapping other = (I_CmsSearchFieldMapping)obj;
169            return (CmsStringUtil.isEqual(m_type, other.getType()))
170                && (CmsStringUtil.isEqual(m_param, other.getParam()));
171        }
172        return false;
173    }
174
175    /**
176     * @see org.opencms.search.fields.I_CmsSearchFieldMapping#getDefaultValue()
177     */
178    public String getDefaultValue() {
179
180        return m_defaultValue;
181    }
182
183    /**
184     * @see org.opencms.search.fields.I_CmsSearchFieldMapping#getParam()
185     */
186    public String getParam() {
187
188        return m_param;
189    }
190
191    /**
192     * @see org.opencms.search.fields.I_CmsSearchFieldMapping#getStringValue(org.opencms.file.CmsObject, org.opencms.file.CmsResource, org.opencms.search.extractors.I_CmsExtractionResult, java.util.List, java.util.List)
193     */
194    public String getStringValue(
195        CmsObject cms,
196        CmsResource res,
197        I_CmsExtractionResult extractionResult,
198        List<CmsProperty> properties,
199        List<CmsProperty> propertiesSearched) {
200
201        String content = null;
202        switch (getType().getMode()) {
203            case 0: // content
204                if (extractionResult != null) {
205                    content = extractionResult.getContent();
206                }
207                break;
208            case 1: // property
209                if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(getParam())) {
210                    content = CmsProperty.get(getParam(), properties).getValue();
211                    CmsSearchUtil.stripHtmlFromPropertyIfNecessary(getParam(), content);
212                }
213                break;
214            case 2: // property-search
215                if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(getParam())) {
216                    content = CmsProperty.get(getParam(), propertiesSearched).getValue();
217                    CmsSearchUtil.stripHtmlFromPropertyIfNecessary(getParam(), content);
218                }
219                break;
220            case 3: // item (retrieve value for the given XPath from the content items)
221                if ((extractionResult != null) && CmsStringUtil.isNotEmptyOrWhitespaceOnly(getParam())) {
222                    Map<String, String> localizedContentItems = m_locale == null
223                    ? extractionResult.getContentItems()
224                    : extractionResult.getContentItems(m_locale);
225                    content = getContentItemForXPath(localizedContentItems, getParam().trim());
226                }
227                break;
228            case 5: // attribute
229                if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(getParam())) {
230                    I_CmsResource.CmsResourceAttribute attribute = null;
231                    try {
232                        attribute = I_CmsResource.CmsResourceAttribute.valueOf(getParam());
233                    } catch (Exception e) {
234                        // invalid attribute name specified, attribute will be null
235                    }
236                    if (attribute != null) {
237                        // map all attributes for a resource
238                        switch (attribute) {
239                            case dateContent:
240                                content = m_isLucene
241                                ? DateTools.timeToString(res.getDateContent(), DateTools.Resolution.MILLISECOND)
242                                : Long.toString(res.getDateContent());
243                                break;
244                            case dateCreated:
245                                content = m_isLucene
246                                ? DateTools.timeToString(res.getDateCreated(), DateTools.Resolution.MILLISECOND)
247                                : Long.toString(res.getDateCreated());
248                                break;
249                            case dateExpired:
250                                if (m_isLucene) {
251                                    long expirationDate = res.getDateExpired();
252                                    if (expirationDate == CmsResource.DATE_EXPIRED_DEFAULT) {
253                                        // default of Long.MAX_VALUE is to big, use January 1, 2100 instead
254                                        content = DATE_EXPIRED_DEFAULT_STR;
255                                    } else {
256                                        content = DateTools.timeToString(
257                                            expirationDate,
258                                            DateTools.Resolution.MILLISECOND);
259                                    }
260                                } else {
261                                    content = Long.toString(res.getDateExpired());
262                                }
263                                break;
264                            case dateLastModified:
265                                content = m_isLucene
266                                ? DateTools.timeToString(res.getDateLastModified(), DateTools.Resolution.MILLISECOND)
267                                : Long.toString(res.getDateLastModified());
268                                break;
269                            case dateReleased:
270                                content = m_isLucene
271                                ? DateTools.timeToString(res.getDateReleased(), DateTools.Resolution.MILLISECOND)
272                                : Long.toString(res.getDateReleased());
273                                break;
274                            case flags:
275                                content = String.valueOf(res.getFlags());
276                                break;
277                            case length:
278                                content = String.valueOf(res.getLength());
279                                break;
280                            case name:
281                                content = res.getName();
282                                break;
283                            case projectLastModified:
284                                try {
285                                    CmsProject project = cms.readProject(res.getProjectLastModified());
286                                    content = project.getName();
287                                } catch (Exception e) {
288                                    // NOOP, content is already null
289                                }
290                                break;
291                            case resourceId:
292                                content = res.getResourceId().toString();
293                                break;
294                            case rootPath:
295                                content = res.getRootPath();
296                                break;
297                            case siblingCount:
298                                content = String.valueOf(res.getSiblingCount());
299                                break;
300                            case state:
301                                content = res.getState().toString();
302                                break;
303                            case structureId:
304                                content = res.getStructureId().toString();
305                                break;
306                            case typeId:
307                                content = String.valueOf(res.getTypeId());
308                                break;
309                            case userCreated:
310                                try {
311                                    CmsUser user = cms.readUser(res.getUserCreated());
312                                    content = user.getName();
313                                } catch (Exception e) {
314                                    // NOOP, content is already null
315                                }
316                                break;
317                            case userLastModified:
318                                try {
319                                    CmsUser user = cms.readUser(res.getUserLastModified());
320                                    content = user.getName();
321                                } catch (Exception e) {
322                                    // NOOP, content is already null
323                                }
324                                break;
325                            case version:
326                                content = String.valueOf(res.getVersion());
327                                break;
328                            default:
329                                // NOOP, content is already null
330                        }
331                    }
332                }
333                break;
334            default:
335                // NOOP, content is already null
336        }
337        if (content == null) {
338            // in case the content is not available, use the default value for this mapping
339            content = getDefaultValue();
340        }
341        return content;
342    }
343
344    /**
345     * @see org.opencms.search.fields.I_CmsSearchFieldMapping#getType()
346     */
347    public CmsSearchFieldMappingType getType() {
348
349        return m_type;
350    }
351
352    /**
353     * The hash code depends on the type and the parameter.<p>
354     *
355     * @see java.lang.Object#hashCode()
356     */
357    @Override
358    public int hashCode() {
359
360        if (m_hashCode == 0) {
361            int hashCode = 73 * (m_type == null ? 29 : m_type.hashCode());
362            if (m_param != null) {
363                hashCode += m_param.hashCode();
364            }
365            m_hashCode = hashCode;
366        }
367        return m_hashCode;
368    }
369
370    /**
371     * @see org.opencms.search.fields.I_CmsSearchFieldMapping#setDefaultValue(java.lang.String)
372     */
373    public void setDefaultValue(String defaultValue) {
374
375        if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(defaultValue)) {
376            m_defaultValue = defaultValue.trim();
377        } else {
378            m_defaultValue = null;
379        }
380    }
381
382    /**
383     * @see org.opencms.search.fields.I_CmsSearchFieldMapping#setLocale(java.util.Locale)
384     */
385    public void setLocale(Locale locale) {
386
387        m_locale = locale;
388    }
389
390    /**
391     * @see org.opencms.search.fields.I_CmsSearchFieldMapping#setParam(java.lang.String)
392     */
393    public void setParam(String param) {
394
395        if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(param)) {
396            m_param = param.trim();
397        } else {
398            m_param = null;
399        }
400    }
401
402    /**
403     * @see org.opencms.search.fields.I_CmsSearchFieldMapping#setType(org.opencms.search.fields.CmsSearchFieldMappingType)
404     */
405    public void setType(CmsSearchFieldMappingType type) {
406
407        m_type = type;
408    }
409
410    /**
411     * @see org.opencms.search.fields.I_CmsSearchFieldMapping#setType(java.lang.String)
412     */
413    public void setType(String type) {
414
415        CmsSearchFieldMappingType mappingType = CmsSearchFieldMappingType.valueOf(type);
416        if (mappingType == null) {
417            // invalid mapping type has been used, throw an exception
418            throw new CmsRuntimeException(
419                new CmsMessageContainer(Messages.get(), Messages.ERR_FIELD_TYPE_UNKNOWN_1, new Object[] {type}));
420        }
421        setType(mappingType);
422    }
423
424    /**
425     * Returns a "\n" separated String of values for the given XPath if according content items can be found.<p>
426     *
427     * @param contentItems the content items to get the value from
428     * @param xpath the short XPath parameter to get the value for
429     *
430     * @return a "\n" separated String of element values found in the content items for the given XPath
431     */
432    private String getContentItemForXPath(Map<String, String> contentItems, String xpath) {
433
434        if (contentItems.get(xpath) != null) { // content item found for XPath
435            return contentItems.get(xpath);
436        } else { // try a multiple value mapping and ensure that the values are in correct order.
437            SortedMap<List<Integer>, String> valueMap = new TreeMap<>(new Comparator<List<Integer>>() {
438
439                // expects lists of the same length that contain only non-null values. This is given for the use case.
440                @SuppressWarnings("boxing")
441                public int compare(List<Integer> l1, List<Integer> l2) {
442
443                    for (int i = 0; i < l1.size(); i++) {
444                        int numCompare = Integer.compare(l1.get(i), l2.get(i));
445                        if (0 != numCompare) {
446                            return numCompare;
447                        }
448                    }
449                    return 0;
450                }
451            });
452            for (Map.Entry<String, String> entry : contentItems.entrySet()) {
453                if (CmsXmlUtils.removeXpath(entry.getKey()).equals(xpath)) { // the removed path refers an item
454
455                    String[] xPathParts = entry.getKey().split("/");
456                    List<Integer> indexes = new ArrayList<>(xPathParts.length);
457                    for (String xPathPart : Arrays.asList(xPathParts)) {
458                        if (!xPathPart.isEmpty()) {
459                            indexes.add(Integer.valueOf(CmsXmlUtils.getXpathIndexInt(xPathPart)));
460                        }
461                    }
462                    valueMap.put(indexes, entry.getValue());
463                }
464            }
465            StringBuffer result = new StringBuffer();
466            for (String value : valueMap.values()) {
467                result.append(value);
468                result.append("\n");
469            }
470            return result.length() > 1 ? result.toString().substring(0, result.length() - 1) : null;
471        }
472    }
473}