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