001/*
002 *  Copyright 2015 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.ametys.web.indexing.observation;
017
018import java.util.Collection;
019import java.util.List;
020import java.util.Map;
021
022import org.apache.avalon.framework.context.Context;
023import org.apache.avalon.framework.context.ContextException;
024import org.apache.avalon.framework.context.Contextualizable;
025import org.apache.avalon.framework.service.ServiceException;
026import org.apache.avalon.framework.service.ServiceManager;
027import org.apache.avalon.framework.service.Serviceable;
028import org.apache.cocoon.components.ContextHelper;
029import org.apache.cocoon.environment.Request;
030import org.apache.commons.lang.StringUtils;
031
032import org.ametys.cms.content.indexing.solr.SolrIndexer;
033import org.ametys.cms.content.indexing.solr.observation.ObserverHelper;
034import org.ametys.cms.indexing.IndexingObserver;
035import org.ametys.cms.repository.Content;
036import org.ametys.core.observation.Event;
037import org.ametys.core.observation.Observer;
038import org.ametys.plugins.repository.AmetysObjectResolver;
039import org.ametys.plugins.repository.RepositoryConstants;
040import org.ametys.plugins.repository.provider.RequestAttributeWorkspaceSelector;
041import org.ametys.runtime.plugin.component.AbstractLogEnabled;
042import org.ametys.web.ObservationConstants;
043import org.ametys.web.WebConstants;
044import org.ametys.web.repository.content.WebContent;
045import org.ametys.web.repository.page.Page;
046import org.ametys.web.repository.page.PageDAO;
047import org.ametys.web.repository.page.ZoneItem.ZoneType;
048import org.ametys.web.search.systemprop.OrphanSystemProperty;
049import org.ametys.web.search.systemprop.PagesSystemProperty;
050
051/**
052 * Observes when a content is made orphan or reaffected to a page and update the index accordingly.
053 */
054public class ContentOrphanStatusPart2Observer extends AbstractLogEnabled implements IndexingObserver, Serviceable, Contextualizable
055{
056    /** The Solr indexer. */
057    protected SolrIndexer _solrIndexer;
058    /** The Ametys object resolver */
059    protected AmetysObjectResolver _resolver;
060    /** The page DAO */
061    protected PageDAO _pageDAO;
062    /** The avalon context */
063    protected Context _context;
064    
065    @Override
066    public void service(ServiceManager manager) throws ServiceException
067    {
068        _solrIndexer = (SolrIndexer) manager.lookup(SolrIndexer.ROLE);
069        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
070        _pageDAO = (PageDAO) manager.lookup(PageDAO.ROLE);
071    }
072    
073    @Override
074    public void contextualize(Context context) throws ContextException
075    {
076        _context = context;
077    }
078    
079    @Override
080    public boolean supports(Event event)
081    {
082        return event.getId().equals(ObservationConstants.EVENT_ZONEITEM_DELETED)
083            || event.getId().equals(ObservationConstants.EVENT_ZONEITEM_ADDED)
084            || event.getId().equals(ObservationConstants.EVENT_ZONEITEM_MODIFIED)
085            || event.getId().equals(ObservationConstants.EVENT_PAGE_CHANGED)
086            || event.getId().equals(ObservationConstants.EVENT_PAGE_MOVED)
087            || event.getId().equals(ObservationConstants.EVENT_PAGE_DELETED)
088            || event.getId().equals(org.ametys.cms.ObservationConstants.EVENT_CONTENT_UNTAG_LIVE)
089            || event.getId().equals(org.ametys.cms.ObservationConstants.EVENT_CONTENT_VALIDATED);
090    }
091    
092    @Override
093    public int getPriority()
094    {
095        // Will be processed AFTER live synchronization observers, to have up-to-date pages in live, in order to have correct indexation data
096        return Observer.MAX_PRIORITY + 4000;
097    }
098    
099    @Override
100    public void observe(Event event, Map<String, Object> transientVars) throws Exception
101    {
102        if (ObserverHelper.isNotSuspendedObservationForIndexation())
103        {
104            if (event.getId().equals(ObservationConstants.EVENT_PAGE_CHANGED) || event.getId().equals(ObservationConstants.EVENT_PAGE_MOVED))
105            {
106                Page page = (Page) event.getArguments().get(ObservationConstants.ARGS_PAGE);
107                if (page != null)
108                {
109                    // Update index of contents of page and its sub pages (orphan status may need to be updated)
110                    _updatePageContents(page, transientVars);
111                }
112            }
113            else if (event.getId().equals(ObservationConstants.EVENT_PAGE_DELETED))
114            {
115                @SuppressWarnings("unchecked")
116                List<Content> contents = (List<Content>) event.getArguments().get(ObservationConstants.ARGS_PAGE_CONTENTS);
117                String pageId = (String) event.getArguments().get(ObservationConstants.ARGS_PAGE_ID);
118                if (contents != null)
119                {
120                    for (Content content : contents)
121                    {
122                        _updateContentProperties(content, pageId, transientVars);
123                    }
124                }
125            }
126            else if (event.getId().equals(org.ametys.cms.ObservationConstants.EVENT_CONTENT_UNTAG_LIVE) || event.getId().equals(org.ametys.cms.ObservationConstants.EVENT_CONTENT_VALIDATED))
127            {
128                Content content = (Content) event.getArguments().get(org.ametys.cms.ObservationConstants.ARGS_CONTENT);
129                if (content instanceof WebContent webContent)
130                {
131                    // When a content is unpublished or validated its pages and its subpages may be removed or added to live workspace
132                    // The orphan status of their contents may need to be updated
133                    Collection<Page> referencingPages = webContent.getReferencingPages();
134                    for (Page page : referencingPages)
135                    {
136                        // Get all contents of page and subpages
137                        _updatePageContents(page, transientVars);
138                    }
139                }
140            }
141            else
142            {
143                ZoneType zoneType = (ZoneType) event.getArguments().get(ObservationConstants.ARGS_ZONE_TYPE);
144                if (zoneType == ZoneType.CONTENT)
145                {
146                    Content content = (Content) event.getArguments().get(ObservationConstants.ARGS_ZONE_ITEM_CONTENT);
147                    if (content != null)
148                    {
149                        _updateContentProperties(content, null, transientVars);
150                    }
151                }
152            }
153        }
154    }
155    
156    @SuppressWarnings("unchecked")
157    private boolean _pageWasInLive(String pageId, Map<String, Object> transientVars)
158    {
159        return ((List<String>) transientVars.getOrDefault(ContentOrphanStatusPart1Observer.PAGES_IN_LIVE_BEFORE_SYNCHRONIZATION, List.of())).contains(pageId);
160    }
161
162    private void _updatePageContents (Page page, Map<String, Object> transientVars) throws Exception
163    {
164        for (Content content : _pageDAO.getPageContents(page))
165        {
166            _updateContentProperties(content, page.getId(), transientVars);
167        }
168    }
169    
170    /**
171     * Update content properties
172     * @param content the content
173     * @param pageId the page id
174     * @param transientVars the transvient variables
175     * @throws Exception if an error occurred
176     */
177    protected void _updateContentProperties(Content content, String pageId, Map<String, Object> transientVars) throws Exception
178    {
179        // default workspace
180        _updateProperties(content, RepositoryConstants.DEFAULT_WORKSPACE);
181        
182        // live workspace
183        Request request = ContextHelper.getRequest(_context);
184        String currentWsp = RequestAttributeWorkspaceSelector.getForcedWorkspace(request);
185        try
186        {
187            // Force the workspace.
188            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, WebConstants.LIVE_WORKSPACE);
189            
190            if (_resolver.hasAmetysObjectForId(content.getId()))
191            {
192                Content liveContent = _resolver.resolveById(content.getId(), null);
193                if (StringUtils.isBlank(pageId))
194                {
195                    _updateProperties(liveContent, WebConstants.LIVE_WORKSPACE);
196                }
197                else
198                {
199                    boolean isPageInLive = _resolver.hasAmetysObjectForId(pageId);
200                    boolean pageWasInLive = _pageWasInLive(pageId, transientVars);
201                    if (isPageInLive ^ pageWasInLive) // A XOR B : we update content properties in live workspace if page status has changed
202                    {
203                        _updateProperties(liveContent, WebConstants.LIVE_WORKSPACE);
204                    }
205                }
206            }
207        }
208        finally
209        {
210            // Restore workspace
211            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWsp);
212        }
213    }
214    
215    private void _updateProperties(Content content, String workspace) throws Exception
216    {
217        if (content instanceof WebContent)
218        {
219            // Update orphan status
220            _solrIndexer.updateSystemProperty(content, OrphanSystemProperty.SYSTEM_PROPERTY_ID, workspace);
221            // Update pageIds
222            _solrIndexer.updateSystemProperty(content, PagesSystemProperty.SYSTEM_PROPERTY_ID, workspace);
223        }
224    }
225}