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