001    /**
002     * Copyright (c) 2000-2012 Liferay, Inc. All rights reserved.
003     *
004     * The contents of this file are subject to the terms of the Liferay Enterprise
005     * Subscription License ("License"). You may not use this file except in
006     * compliance with the License. You can obtain a copy of the License by
007     * contacting Liferay, Inc. See the License for the specific language governing
008     * permissions and limitations under the License, including but not limited to
009     * distribution rights of the Software.
010     *
011     *
012     *
013     */
014    
015    package com.liferay.portlet.journal.util;
016    
017    import com.liferay.portal.kernel.dao.orm.DynamicQuery;
018    import com.liferay.portal.kernel.dao.orm.DynamicQueryFactoryUtil;
019    import com.liferay.portal.kernel.dao.orm.Projection;
020    import com.liferay.portal.kernel.dao.orm.ProjectionFactoryUtil;
021    import com.liferay.portal.kernel.dao.orm.ProjectionList;
022    import com.liferay.portal.kernel.dao.orm.Property;
023    import com.liferay.portal.kernel.dao.orm.PropertyFactoryUtil;
024    import com.liferay.portal.kernel.log.Log;
025    import com.liferay.portal.kernel.log.LogFactoryUtil;
026    import com.liferay.portal.kernel.search.BaseIndexer;
027    import com.liferay.portal.kernel.search.BooleanQuery;
028    import com.liferay.portal.kernel.search.Document;
029    import com.liferay.portal.kernel.search.DocumentImpl;
030    import com.liferay.portal.kernel.search.Field;
031    import com.liferay.portal.kernel.search.SearchContext;
032    import com.liferay.portal.kernel.search.SearchEngineUtil;
033    import com.liferay.portal.kernel.search.Summary;
034    import com.liferay.portal.kernel.util.GetterUtil;
035    import com.liferay.portal.kernel.util.HtmlUtil;
036    import com.liferay.portal.kernel.util.LocaleUtil;
037    import com.liferay.portal.kernel.util.LocalizationUtil;
038    import com.liferay.portal.kernel.util.StringBundler;
039    import com.liferay.portal.kernel.util.StringPool;
040    import com.liferay.portal.kernel.util.StringUtil;
041    import com.liferay.portal.kernel.util.Validator;
042    import com.liferay.portal.kernel.workflow.WorkflowConstants;
043    import com.liferay.portal.kernel.xml.DocumentException;
044    import com.liferay.portal.kernel.xml.Element;
045    import com.liferay.portal.kernel.xml.Node;
046    import com.liferay.portal.kernel.xml.SAXReaderUtil;
047    import com.liferay.portal.security.pacl.PACLClassLoaderUtil;
048    import com.liferay.portal.util.PortletKeys;
049    import com.liferay.portlet.journal.NoSuchStructureException;
050    import com.liferay.portlet.journal.model.JournalArticle;
051    import com.liferay.portlet.journal.model.JournalArticleConstants;
052    import com.liferay.portlet.journal.model.JournalStructure;
053    import com.liferay.portlet.journal.service.JournalArticleLocalServiceUtil;
054    import com.liferay.portlet.journal.service.JournalStructureLocalServiceUtil;
055    
056    import java.util.ArrayList;
057    import java.util.Collection;
058    import java.util.LinkedHashMap;
059    import java.util.LinkedList;
060    import java.util.List;
061    import java.util.Locale;
062    
063    import javax.portlet.PortletURL;
064    
065    /**
066     * @author Brian Wing Shun Chan
067     * @author Harry Mark
068     * @author Bruno Farache
069     * @author Raymond Augé
070     * @author Hugo Huijser
071     */
072    public class JournalIndexer extends BaseIndexer {
073    
074            public static final String[] CLASS_NAMES = {JournalArticle.class.getName()};
075    
076            public static final String PORTLET_ID = PortletKeys.JOURNAL;
077    
078            public String[] getClassNames() {
079                    return CLASS_NAMES;
080            }
081    
082            public String getPortletId() {
083                    return PORTLET_ID;
084            }
085    
086            @Override
087            public boolean isPermissionAware() {
088                    return _PERMISSION_AWARE;
089            }
090    
091            @Override
092            public void postProcessContextQuery(
093                            BooleanQuery contextQuery, SearchContext searchContext)
094                    throws Exception {
095    
096                    Long classNameId = (Long)searchContext.getAttribute(
097                            Field.CLASS_NAME_ID);
098    
099                    if (classNameId != null) {
100                            contextQuery.addRequiredTerm("classNameId", classNameId.toString());
101                    }
102    
103                    int status = GetterUtil.getInteger(
104                            searchContext.getAttribute(Field.STATUS),
105                            WorkflowConstants.STATUS_APPROVED);
106    
107                    if (status != WorkflowConstants.STATUS_ANY) {
108                            contextQuery.addRequiredTerm(Field.STATUS, status);
109                    }
110    
111                    String articleType = (String)searchContext.getAttribute("articleType");
112    
113                    if (Validator.isNotNull(articleType)) {
114                            contextQuery.addRequiredTerm(Field.TYPE, articleType);
115                    }
116    
117                    String structureId = (String)searchContext.getAttribute("structureId");
118    
119                    if (Validator.isNotNull(structureId)) {
120                            contextQuery.addRequiredTerm("structureId", structureId);
121                    }
122    
123                    String templateId = (String)searchContext.getAttribute("templateId");
124    
125                    if (Validator.isNotNull(templateId)) {
126                            contextQuery.addRequiredTerm("templateId", templateId);
127                    }
128            }
129    
130            @Override
131            public void postProcessSearchQuery(
132                            BooleanQuery searchQuery, SearchContext searchContext)
133                    throws Exception {
134    
135                    addSearchTerm(searchQuery, searchContext, Field.CLASS_PK, false);
136                    addSearchLocalizedTerm(
137                            searchQuery, searchContext, Field.CONTENT, false);
138                    addSearchLocalizedTerm(
139                            searchQuery, searchContext, Field.DESCRIPTION, false);
140                    addSearchTerm(searchQuery, searchContext, Field.ENTRY_CLASS_PK, false);
141                    addSearchLocalizedTerm(searchQuery, searchContext, Field.TITLE, false);
142                    addSearchTerm(searchQuery, searchContext, Field.TYPE, false);
143                    addSearchTerm(searchQuery, searchContext, Field.USER_NAME, false);
144    
145                    LinkedHashMap<String, Object> params =
146                            (LinkedHashMap<String, Object>)searchContext.getAttribute("params");
147    
148                    if (params != null) {
149                            String expandoAttributes = (String)params.get("expandoAttributes");
150    
151                            if (Validator.isNotNull(expandoAttributes)) {
152                                    addSearchExpando(searchQuery, searchContext, expandoAttributes);
153                            }
154                    }
155            }
156    
157            protected void addReindexCriteria(
158                    DynamicQuery dynamicQuery, long companyId, double version, int status) {
159    
160                    addReindexCriteria(dynamicQuery, companyId, status);
161    
162                    Property property = PropertyFactoryUtil.forName("version");
163    
164                    dynamicQuery.add(property.eq(version));
165            }
166    
167            protected void addReindexCriteria(
168                    DynamicQuery dynamicQuery, long companyId, int status) {
169    
170                    Property companyIdProperty = PropertyFactoryUtil.forName("companyId");
171    
172                    dynamicQuery.add(companyIdProperty.eq(companyId));
173    
174                    Property statusProperty = PropertyFactoryUtil.forName("status");
175    
176                    dynamicQuery.add(statusProperty.eq(status));
177            }
178    
179            @Override
180            protected void addSearchLocalizedTerm(
181                            BooleanQuery searchQuery, SearchContext searchContext, String field,
182                            boolean like)
183                    throws Exception {
184    
185                    if (Validator.isNull(field)) {
186                            return;
187                    }
188    
189                    String value = String.valueOf(searchContext.getAttribute(field));
190    
191                    if (Validator.isNull(value)) {
192                            value = searchContext.getKeywords();
193                    }
194    
195                    if (Validator.isNull(value)) {
196                            return;
197                    }
198    
199                    field = DocumentImpl.getLocalizedName(searchContext.getLocale(), field);
200    
201                    if (searchContext.isAndSearch()) {
202                            searchQuery.addRequiredTerm(field, value, like);
203                    }
204                    else {
205                            searchQuery.addTerm(field, value, like);
206                    }
207            }
208    
209            @Override
210            protected void doDelete(Object obj) throws Exception {
211                    JournalArticle article = (JournalArticle)obj;
212    
213                    deleteDocument(
214                            article.getCompanyId(), article.getGroupId(),
215                            article.getArticleId());
216            }
217    
218            @Override
219            protected Document doGetDocument(Object obj) throws Exception {
220                    JournalArticle article = (JournalArticle)obj;
221    
222                    Document document = getBaseModelDocument(PORTLET_ID, article);
223    
224                    document.addUID(
225                            PORTLET_ID, article.getGroupId(), article.getArticleId());
226    
227                    Locale defaultLocale = LocaleUtil.getDefault();
228    
229                    String defaultLanguageId = LocaleUtil.toLanguageId(defaultLocale);
230    
231                    String[] languageIds = getLanguageIds(
232                            defaultLanguageId, article.getContent());
233    
234                    for (String languageId : languageIds) {
235                            String content = extractContent(article, languageId);
236    
237                            if (languageId.equals(defaultLanguageId)) {
238                                    document.addText(Field.CONTENT, content);
239                            }
240    
241                            document.addText(
242                                    Field.CONTENT.concat(StringPool.UNDERLINE).concat(languageId),
243                                    content);
244                    }
245    
246                    document.addLocalizedText(
247                            Field.DESCRIPTION, article.getDescriptionMap());
248                    document.addLocalizedText(Field.TITLE, article.getTitleMap());
249                    document.addKeyword(Field.TYPE, article.getType());
250                    document.addKeyword(Field.VERSION, article.getVersion());
251    
252                    document.addKeyword("articleId", article.getArticleId());
253                    document.addDate("displayDate", article.getDisplayDate());
254                    document.addKeyword("layoutUuid", article.getLayoutUuid());
255                    document.addKeyword("structureId", article.getStructureId());
256                    document.addKeyword("templateId", article.getTemplateId());
257    
258                    JournalStructure structure = null;
259    
260                    if (Validator.isNotNull(article.getStructureId())) {
261                            try {
262                                    structure = JournalStructureLocalServiceUtil.getStructure(
263                                            article.getGroupId(), article.getStructureId(), true);
264                            }
265                            catch (NoSuchStructureException nsse) {
266                            }
267                    }
268    
269                    processStructure(structure, document, article.getContent());
270    
271                    return document;
272            }
273    
274            @Override
275            protected String doGetSortField(String orderByCol) {
276                    if (orderByCol.equals("display-date")) {
277                            return "displayDate";
278                    }
279                    else if (orderByCol.equals("id")) {
280                            return Field.ENTRY_CLASS_PK;
281                    }
282                    else if (orderByCol.equals("modified-date")) {
283                            return Field.MODIFIED_DATE;
284                    }
285                    else if (orderByCol.equals("title")) {
286                            return Field.TITLE;
287                    }
288                    else {
289                            return orderByCol;
290                    }
291            }
292    
293            @Override
294            protected Summary doGetSummary(
295                    Document document, Locale locale, String snippet,
296                    PortletURL portletURL) {
297    
298                    String title = document.get(locale, Field.TITLE);
299    
300                    String content = snippet;
301    
302                    if (Validator.isNull(snippet)) {
303                            content = StringUtil.shorten(
304                                    document.get(locale, Field.CONTENT), 200);
305                    }
306    
307                    String groupId = document.get(Field.GROUP_ID);
308                    String articleId = document.get("articleId");
309                    String version = document.get(Field.VERSION);
310    
311                    portletURL.setParameter("struts_action", "/journal/edit_article");
312                    portletURL.setParameter("groupId", groupId);
313                    portletURL.setParameter("articleId", articleId);
314                    portletURL.setParameter("version", version);
315    
316                    return new Summary(title, content, portletURL);
317            }
318    
319            @Override
320            protected void doReindex(Object obj) throws Exception {
321                    JournalArticle article = (JournalArticle)obj;
322    
323                    Document document = getDocument(article);
324    
325                    if (!article.isIndexable() ||
326                            (!article.isApproved() &&
327                             (article.getVersion() !=
328                                      JournalArticleConstants.VERSION_DEFAULT))) {
329    
330                            SearchEngineUtil.deleteDocument(
331                                    getSearchEngineId(), article.getCompanyId(),
332                                    document.get(Field.UID));
333    
334                            return;
335                    }
336    
337                    SearchEngineUtil.updateDocument(
338                            getSearchEngineId(), article.getCompanyId(), document);
339            }
340    
341            @Override
342            protected void doReindex(String className, long classPK) throws Exception {
343                    JournalArticle article =
344                            JournalArticleLocalServiceUtil.getLatestArticle(
345                                    classPK, WorkflowConstants.STATUS_APPROVED);
346    
347                    doReindex(article);
348            }
349    
350            @Override
351            protected void doReindex(String[] ids) throws Exception {
352                    long companyId = GetterUtil.getLong(ids[0]);
353    
354                    reindexArticles(companyId);
355            }
356    
357            protected String encodeFieldName(String name) {
358                    return _FIELD_NAMESPACE.concat(StringPool.FORWARD_SLASH).concat(name);
359            }
360    
361            protected String extractContent(JournalArticle article, String languageId) {
362                    String content = article.getContentByLocale(languageId);
363    
364                    if (Validator.isNotNull(article.getStructureId())) {
365                            content = extractDynamicContent(content);
366                    }
367                    else {
368                            content = extractStaticContent(content);
369                    }
370    
371                    content = HtmlUtil.extractText(content);
372    
373                    return content;
374            }
375    
376            protected String extractDynamicContent(Element rootElement) {
377                    StringBundler sb = new StringBundler();
378    
379                    List<Element> dynamicElementElements = rootElement.elements(
380                            "dynamic-element");
381    
382                    for (Element dynamicElementElement : dynamicElementElements) {
383                            String type = dynamicElementElement.attributeValue(
384                                    "type", StringPool.BLANK);
385    
386                            if (!type.equals("boolean") && !type.equals("document_library") &&
387                                    !type.equals("image") && !type.equals("list") &&
388                                    !type.equals("link_to_layout") && !type.equals("multi-list") &&
389                                    !type.equals("selection_break")) {
390    
391                                    Element dynamicContentElement = dynamicElementElement.element(
392                                            "dynamic-content");
393    
394                                    if (dynamicContentElement != null) {
395                                            String dynamicContent = dynamicContentElement.getText();
396    
397                                            sb.append(dynamicContent);
398                                            sb.append(StringPool.SPACE);
399                                    }
400                            }
401    
402                            sb.append(extractDynamicContent(dynamicElementElement));
403                    }
404    
405                    return sb.toString();
406            }
407    
408            protected String extractDynamicContent(String content) {
409                    try {
410                            com.liferay.portal.kernel.xml.Document document =
411                                    SAXReaderUtil.read(content);
412    
413                            Element rootElement = document.getRootElement();
414    
415                            return extractDynamicContent(rootElement);
416                    }
417                    catch (DocumentException de) {
418                            _log.error(de);
419                    }
420    
421                    return StringPool.BLANK;
422            }
423    
424            protected String extractStaticContent(String content) {
425                    content = StringUtil.replace(content, "<![CDATA[", StringPool.BLANK);
426                    content = StringUtil.replace(content, "]]>", StringPool.BLANK);
427                    content = StringUtil.replace(content, "&amp;", "&");
428                    content = StringUtil.replace(content, "&lt;", "<");
429                    content = StringUtil.replace(content, "&gt;", ">");
430    
431                    return content;
432            }
433    
434            protected String[] getLanguageIds(
435                    String defaultLanguageId, String content) {
436    
437                    String[] languageIds = LocalizationUtil.getAvailableLocales(content);
438    
439                    if (languageIds.length == 0) {
440                            languageIds = new String[] {defaultLanguageId};
441                    }
442    
443                    return languageIds;
444            }
445    
446            @Override
447            protected String getPortletId(SearchContext searchContext) {
448                    return PORTLET_ID;
449            }
450    
451            protected List<JournalArticle> getReindexApprovedArticles(
452                            long companyId, long startId, long endId)
453                    throws Exception {
454    
455                    DynamicQuery dynamicQuery = DynamicQueryFactoryUtil.forClass(
456                            JournalArticle.class, PACLClassLoaderUtil.getPortalClassLoader());
457    
458                    Property property = PropertyFactoryUtil.forName("id");
459    
460                    dynamicQuery.add(property.ge(startId));
461                    dynamicQuery.add(property.lt(endId));
462    
463                    addReindexCriteria(
464                            dynamicQuery, companyId, WorkflowConstants.STATUS_APPROVED);
465    
466                    return JournalArticleLocalServiceUtil.dynamicQuery(dynamicQuery);
467            }
468    
469            protected List<JournalArticle> getReindexDraftArticles(
470                            long companyId, long startId, long endId)
471                    throws Exception {
472    
473                    DynamicQuery dynamicQuery = DynamicQueryFactoryUtil.forClass(
474                            JournalArticle.class, PACLClassLoaderUtil.getPortalClassLoader());
475    
476                    Property property = PropertyFactoryUtil.forName("id");
477    
478                    dynamicQuery.add(property.ge(startId));
479                    dynamicQuery.add(property.lt(endId));
480    
481                    addReindexCriteria(
482                            dynamicQuery, companyId, JournalArticleConstants.VERSION_DEFAULT,
483                            WorkflowConstants.STATUS_APPROVED);
484    
485                    return JournalArticleLocalServiceUtil.dynamicQuery(dynamicQuery);
486            }
487    
488            protected void indexField(
489                    Document document, Element element, String elType, String elIndexType) {
490    
491                    if (Validator.isNull(elIndexType)) {
492                            return;
493                    }
494    
495                    com.liferay.portal.kernel.xml.Document structureDocument =
496                            element.getDocument();
497    
498                    Element rootElement = structureDocument.getRootElement();
499    
500                    String defaultLocale = GetterUtil.getString(
501                            rootElement.attributeValue("default-locale"));
502    
503                    String name = encodeFieldName(element.attributeValue("name"));
504    
505                    List<Element> dynamicContentElements = element.elements(
506                            "dynamic-content");
507    
508                    for (Element dynamicContentElement : dynamicContentElements) {
509                            String contentLocale = GetterUtil.getString(
510                                    dynamicContentElement.attributeValue("language-id"));
511    
512                            String[] value = new String[] {dynamicContentElement.getText()};
513    
514                            if (elType.equals("multi-list")) {
515                                    List<Element> optionElements = dynamicContentElement.elements(
516                                            "option");
517    
518                                    value = new String[optionElements.size()];
519    
520                                    for (int i = 0; i < optionElements.size(); i++) {
521                                            value[i] = optionElements.get(i).getText();
522                                    }
523                            }
524    
525                            if (elIndexType.equals("keyword")) {
526                                    if (Validator.isNull(contentLocale)) {
527                                            document.addKeyword(name, value);
528                                    }
529                                    else {
530                                            if (defaultLocale.equals(contentLocale)) {
531                                                    document.addKeyword(name, value);
532                                            }
533    
534                                            document.addKeyword(
535                                                    name.concat(StringPool.UNDERLINE).concat(contentLocale),
536                                                    value);
537                                    }
538                            }
539                            else if (elIndexType.equals("text")) {
540                                    if (Validator.isNull(contentLocale)) {
541                                            document.addText(
542                                                    name, StringUtil.merge(value, StringPool.SPACE));
543                                    }
544                                    else {
545                                            if (defaultLocale.equals(contentLocale)) {
546                                                    document.addText(
547                                                            name, StringUtil.merge(value, StringPool.SPACE));
548                                            }
549    
550                                            document.addText(
551                                                    name.concat(StringPool.UNDERLINE).concat(contentLocale),
552                                                    StringUtil.merge(value, StringPool.SPACE));
553                                    }
554                            }
555                    }
556            }
557    
558            protected void processStructure(
559                            com.liferay.portal.kernel.xml.Document structureDocument,
560                            Document document, Element rootElement)
561                    throws Exception {
562    
563                    LinkedList<Element> queue = new LinkedList<Element>(
564                            rootElement.elements());
565    
566                    Element element = null;
567    
568                    while ((element = queue.poll()) != null) {
569                            String elName = element.attributeValue("name", StringPool.BLANK);
570                            String elType = element.attributeValue("type", StringPool.BLANK);
571                            String elIndexType = element.attributeValue(
572                                    "index-type", StringPool.BLANK);
573    
574                            if (structureDocument != null) {
575                                    String path = element.getPath().concat(
576                                            "[@name='").concat(elName).concat("']");
577    
578                                    Node structureNode = structureDocument.selectSingleNode(path);
579    
580                                    if (structureNode != null) {
581                                            Element structureElement = (Element)structureNode;
582    
583                                            elType = structureElement.attributeValue(
584                                                    "type", StringPool.BLANK);
585                                            elIndexType = structureElement.attributeValue(
586                                                    "index-type", StringPool.BLANK);
587                                    }
588                            }
589    
590                            if (Validator.isNotNull(elType)) {
591                                    indexField(document, element, elType, elIndexType);
592                            }
593    
594                            queue.addAll(element.elements());
595                    }
596            }
597    
598            protected void processStructure(
599                    JournalStructure structure, Document document, String content) {
600    
601                    try {
602                            com.liferay.portal.kernel.xml.Document structureDocument = null;
603    
604                            if (structure != null) {
605                                    structureDocument = SAXReaderUtil.read(structure.getXsd());
606                            }
607    
608                            com.liferay.portal.kernel.xml.Document contentDocument =
609                                    SAXReaderUtil.read(content);
610    
611                            Element rootElement = contentDocument.getRootElement();
612    
613                            processStructure(structureDocument, document, rootElement);
614                    }
615                    catch (Exception e) {
616                            _log.error(e, e);
617                    }
618            }
619    
620            protected void reindexArticles(long companyId) throws Exception {
621                    DynamicQuery dynamicQuery = DynamicQueryFactoryUtil.forClass(
622                            JournalArticle.class, PACLClassLoaderUtil.getPortalClassLoader());
623    
624                    Projection minIdProjection = ProjectionFactoryUtil.min("id");
625                    Projection maxIdProjection = ProjectionFactoryUtil.max("id");
626    
627                    ProjectionList projectionList = ProjectionFactoryUtil.projectionList();
628    
629                    projectionList.add(minIdProjection);
630                    projectionList.add(maxIdProjection);
631    
632                    dynamicQuery.setProjection(projectionList);
633    
634                    addReindexCriteria(
635                            dynamicQuery, companyId, WorkflowConstants.STATUS_APPROVED);
636    
637                    List<Object[]> results = JournalArticleLocalServiceUtil.dynamicQuery(
638                            dynamicQuery);
639    
640                    Object[] minAndMaxIds = results.get(0);
641    
642                    if ((minAndMaxIds[0] == null) || (minAndMaxIds[1] == null)) {
643                            return;
644                    }
645    
646                    long minId = (Long)minAndMaxIds[0];
647                    long maxId = (Long)minAndMaxIds[1];
648    
649                    long startId = minId;
650                    long endId = startId + DEFAULT_INTERVAL;
651    
652                    while (startId <= maxId) {
653                            reindexArticles(companyId, startId, endId);
654    
655                            startId = endId;
656                            endId += DEFAULT_INTERVAL;
657                    }
658            }
659    
660            protected void reindexArticles(long companyId, long startId, long endId)
661                    throws Exception {
662    
663                    List<JournalArticle> articles = new ArrayList<JournalArticle>();
664    
665                    articles.addAll(getReindexApprovedArticles(companyId, startId, endId));
666                    articles.addAll(getReindexDraftArticles(companyId, startId, endId));
667    
668                    if (articles.isEmpty()) {
669                            return;
670                    }
671    
672                    Collection<Document> documents = new ArrayList<Document>(
673                            articles.size());
674    
675                    for (JournalArticle article : articles) {
676                            if (!article.isIndexable()) {
677                                    continue;
678                            }
679    
680                            if (article.isApproved()) {
681                                    JournalArticle latestArticle =
682                                            JournalArticleLocalServiceUtil.getLatestArticle(
683                                                    article.getResourcePrimKey(),
684                                                    WorkflowConstants.STATUS_APPROVED);
685    
686                                    if (!latestArticle.isIndexable()) {
687                                            continue;
688                                    }
689                            }
690    
691                            Document document = getDocument(article);
692    
693                            documents.add(document);
694                    }
695    
696                    SearchEngineUtil.updateDocuments(
697                            getSearchEngineId(), companyId, documents);
698            }
699    
700            private static final String _FIELD_NAMESPACE = "web_content";
701    
702            private static final boolean _PERMISSION_AWARE = true;
703    
704            private static Log _log = LogFactoryUtil.getLog(JournalIndexer.class);
705    
706    }