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.repository.Content;
035import org.ametys.core.observation.Event;
036import org.ametys.core.observation.Observer;
037import org.ametys.plugins.repository.AmetysObjectResolver;
038import org.ametys.plugins.repository.RepositoryConstants;
039import org.ametys.plugins.repository.provider.RequestAttributeWorkspaceSelector;
040import org.ametys.runtime.plugin.component.AbstractLogEnabled;
041import org.ametys.web.ObservationConstants;
042import org.ametys.web.WebConstants;
043import org.ametys.web.repository.content.WebContent;
044import org.ametys.web.repository.page.Page;
045import org.ametys.web.repository.page.PageDAO;
046import org.ametys.web.repository.page.ZoneItem.ZoneType;
047import org.ametys.web.search.solr.field.OrphanSearchField;
048import org.ametys.web.search.solr.field.PagesSearchField;
049
050/**
051 * Observes when a content is made orphan or reaffected to a page and update the index accordingly.
052 */
053public class ContentOrphanStatusPart2Observer extends AbstractLogEnabled implements Observer, Serviceable, Contextualizable
054{
055    /** The Solr indexer. */
056    protected SolrIndexer _solrIndexer;
057    /** The Ametys object resolver */
058    protected AmetysObjectResolver _resolver;
059    /** The page DAO */
060    protected PageDAO _pageDAO;
061    /** The avalon context */
062    protected Context _context;
063    
064    @Override
065    public void service(ServiceManager manager) throws ServiceException
066    {
067        _solrIndexer = (SolrIndexer) manager.lookup(SolrIndexer.ROLE);
068        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
069        _pageDAO = (PageDAO) manager.lookup(PageDAO.ROLE);
070    }
071    
072    @Override
073    public void contextualize(Context context) throws ContextException
074    {
075        _context = context;
076    }
077    
078    @Override
079    public boolean supports(Event event)
080    {
081        return event.getId().equals(ObservationConstants.EVENT_ZONEITEM_DELETED)
082            || event.getId().equals(ObservationConstants.EVENT_ZONEITEM_ADDED)
083            || event.getId().equals(ObservationConstants.EVENT_ZONEITEM_MODIFIED)
084            || event.getId().equals(ObservationConstants.EVENT_PAGE_CHANGED)
085            || event.getId().equals(ObservationConstants.EVENT_PAGE_MOVED)
086            || event.getId().equals(ObservationConstants.EVENT_PAGE_DELETED)
087            || event.getId().equals(org.ametys.cms.ObservationConstants.EVENT_CONTENT_UNTAG_LIVE)
088            || event.getId().equals(org.ametys.cms.ObservationConstants.EVENT_CONTENT_VALIDATED);
089    }
090    
091    @Override
092    public int getPriority(Event event)
093    {
094        // Will be processed AFTER live synchronization observers, to have up-to-date pages in live, in order to have correct indexation data
095        return Observer.MAX_PRIORITY + 4000;
096    }
097    
098    @Override
099    public void observe(Event event, Map<String, Object> transientVars) throws Exception
100    {
101        if (ObserverHelper.isNotSuspendedObservationForIndexation())
102        {
103            if (event.getId().equals(ObservationConstants.EVENT_PAGE_CHANGED) || event.getId().equals(ObservationConstants.EVENT_PAGE_MOVED))
104            {
105                Page page = (Page) event.getArguments().get(ObservationConstants.ARGS_PAGE);
106                if (page != null)
107                {
108                    // Update index of contents of page and its sub pages (orphan status may need to be updated)
109                    _updatePageContents(page, transientVars);
110                }
111            }
112            else if (event.getId().equals(ObservationConstants.EVENT_PAGE_DELETED))
113            {
114                @SuppressWarnings("unchecked")
115                List<Content> contents = (List<Content>) event.getArguments().get(ObservationConstants.ARGS_PAGE_CONTENTS);
116                String pageId = (String) event.getArguments().get(ObservationConstants.ARGS_PAGE_ID);
117                if (contents != null)
118                {
119                    for (Content content : contents)
120                    {
121                        _updateContentProperties(content, pageId, transientVars);   
122                    }
123                }
124            }
125            else if (event.getId().equals(org.ametys.cms.ObservationConstants.EVENT_CONTENT_UNTAG_LIVE) || event.getId().equals(org.ametys.cms.ObservationConstants.EVENT_CONTENT_VALIDATED))
126            {
127                Content content = (Content) event.getArguments().get(org.ametys.cms.ObservationConstants.ARGS_CONTENT);
128                if (content instanceof WebContent webContent)
129                {
130                    // When a content is unpublished or validated its pages and its subpages may be removed or added to live workspace 
131                    // The orphan status of their contents may need to be updated 
132                    Collection<Page> referencingPages = ((WebContent) content).getReferencingPages();
133                    for (Page page : referencingPages)
134                    {
135                        // Get all contents of page and subpages
136                        _updatePageContents(page, transientVars);
137                    }
138                }
139            }
140            else
141            {
142                ZoneType zoneType = (ZoneType) event.getArguments().get(ObservationConstants.ARGS_ZONE_TYPE);
143                if (zoneType == ZoneType.CONTENT)
144                {
145                    Content content = (Content) event.getArguments().get(ObservationConstants.ARGS_ZONE_ITEM_CONTENT);
146                    if (content != null)
147                    {
148                        _updateContentProperties(content, null, transientVars);
149                    }
150                }
151            }
152        }
153    }
154    
155    @SuppressWarnings("unchecked")
156    private boolean _pageWasInLive(String pageId, Map<String, Object> transientVars)
157    {
158        return ((List<String>) transientVars.getOrDefault(ContentOrphanStatusPart1Observer.PAGES_IN_LIVE_BEFORE_SYNCHRONIZATION, List.of())).contains(pageId);
159    }
160
161    private void _updatePageContents (Page page, Map<String, Object> transientVars) throws Exception
162    {
163        for (Content content : _pageDAO.getPageContents(page))
164        {
165            _updateContentProperties(content, page.getId(), transientVars);
166        }
167    }
168    
169    /**
170     * Update content properties
171     * @param content the content 
172     * @param pageId the page id
173     * @param transientVars the transvient variables
174     * @throws Exception if an error occurred
175     */
176    protected void _updateContentProperties(Content content, String pageId, Map<String, Object> transientVars) throws Exception
177    {
178        // default workspace
179        _updateProperties(content, RepositoryConstants.DEFAULT_WORKSPACE);
180        
181        // live workspace
182        Request request = ContextHelper.getRequest(_context);
183        String currentWsp = RequestAttributeWorkspaceSelector.getForcedWorkspace(request);
184        try
185        {
186            // Force the workspace.
187            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, WebConstants.LIVE_WORKSPACE);
188            
189            if (_resolver.hasAmetysObjectForId(content.getId()))
190            {
191                Content liveContent = _resolver.resolveById(content.getId(), null);
192                if (StringUtils.isBlank(pageId))
193                {
194                    _updateProperties(liveContent, WebConstants.LIVE_WORKSPACE);
195                }
196                else
197                {
198                    boolean isPageInLive = _resolver.hasAmetysObjectForId(pageId);
199                    boolean pageWasInLive = _pageWasInLive(pageId, transientVars);
200                    if (isPageInLive ^ pageWasInLive) // A XOR B : we update content properties in live workspace if page status has changed
201                    {
202                        _updateProperties(liveContent, WebConstants.LIVE_WORKSPACE);
203                    }
204                }
205            } 
206        }
207        finally
208        {
209            // Restore workspace
210            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWsp);
211        }
212    }
213    
214    private void _updateProperties(Content content, String workspace) throws Exception
215    {
216        if (content instanceof WebContent)
217        {
218            // Update orphan status
219            _solrIndexer.updateSystemProperty(content, OrphanSearchField.NAME, workspace);
220            // Update pageIds
221            _solrIndexer.updateSystemProperty(content, PagesSearchField.NAME, workspace);
222        }
223    }
224}