001/*
002 *  Copyright 2020 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.plugins.workspaces.minisite;
017
018import java.util.HashMap;
019import java.util.List;
020import java.util.Map;
021import java.util.Objects;
022import java.util.Set;
023
024import org.apache.avalon.framework.service.ServiceException;
025import org.apache.avalon.framework.service.ServiceManager;
026import org.apache.commons.lang.IllegalClassException;
027import org.apache.commons.lang.StringUtils;
028
029import org.ametys.cms.FilterNameHelper;
030import org.ametys.cms.data.Binary;
031import org.ametys.cms.repository.Content;
032import org.ametys.cms.repository.ModifiableWorkflowAwareContent;
033import org.ametys.cms.search.content.ContentSearcherFactory;
034import org.ametys.core.observation.Event;
035import org.ametys.plugins.explorer.ExplorerNode;
036import org.ametys.plugins.explorer.resources.ModifiableResourceCollection;
037import org.ametys.plugins.explorer.resources.jcr.JCRResourcesCollectionFactory;
038import org.ametys.plugins.repository.AmetysObjectIterable;
039import org.ametys.plugins.repository.AmetysRepositoryException;
040import org.ametys.plugins.workflow.AbstractWorkflowComponent;
041import org.ametys.plugins.workflow.support.WorkflowProvider;
042import org.ametys.plugins.workflow.support.WorkflowProvider.AmetysObjectWorkflow;
043import org.ametys.plugins.workspaces.AbstractWorkspaceModule;
044import org.ametys.plugins.workspaces.ObservationConstants;
045import org.ametys.plugins.workspaces.WorkspacesConstants;
046import org.ametys.plugins.workspaces.project.ProjectConstants;
047import org.ametys.plugins.workspaces.project.objects.Project;
048import org.ametys.plugins.workspaces.util.StatisticColumn;
049import org.ametys.plugins.workspaces.util.StatisticsColumnType;
050import org.ametys.runtime.i18n.I18nizableText;
051import org.ametys.web.repository.page.ModifiablePage;
052import org.ametys.web.repository.page.ModifiableZone;
053import org.ametys.web.repository.page.ModifiableZoneItem;
054import org.ametys.web.repository.page.Page.PageType;
055import org.ametys.web.repository.page.ZoneItem.ZoneType;
056import org.ametys.web.repository.site.Site;
057import org.ametys.web.search.query.SiteQuery;
058import org.ametys.web.workflow.CreateContentFunction;
059
060import com.google.common.collect.ImmutableSet;
061import com.opensymphony.workflow.WorkflowException;
062
063/**
064 * Workspaces module for editorial pages
065 */
066public class MiniSiteWorkspaceModule extends AbstractWorkspaceModule
067{
068    /** Avalon ROLE */
069    public static final String MINISITE_MODULE_ID = MiniSiteWorkspaceModule.class.getName();
070    
071    /** Workspaces tasks list node name */
072    private static final String __WORKSPACES_MINISITE_NODE_NAME = "minisite";
073    
074    private static final int __INITIAL_WORKFLOW_ACTION_ID = 13;
075    
076    private static final String __ARTICLE_NUMBER_HEADER_ID = __WORKSPACES_MINISITE_NODE_NAME + "$article_number";
077    
078    /** The workflow provider */
079    protected WorkflowProvider _workflowProvider;
080
081    /** The content searcher factory */
082    protected ContentSearcherFactory _contentSearcherFactory;
083    
084    @Override
085    public void service(ServiceManager manager) throws ServiceException
086    {
087        super.service(manager);
088        _workflowProvider = (WorkflowProvider) manager.lookup(WorkflowProvider.ROLE);
089        _contentSearcherFactory = (ContentSearcherFactory) manager.lookup(ContentSearcherFactory.ROLE);
090    }
091    
092    @Override
093    public String getId()
094    {
095        return MINISITE_MODULE_ID;
096    }
097    
098    public boolean isUnactivatedByDefault()
099    {
100        return true;
101    }
102    
103    @Override
104    public String getModuleName()
105    {
106        return __WORKSPACES_MINISITE_NODE_NAME;
107    }
108    
109    public int getOrder()
110    {
111        return ORDER_MINISITE;
112    }
113    
114    @Override
115    protected String getModulePageName()
116    {
117        return "minisite";
118    }
119    
120    public I18nizableText getModuleTitle()
121    {
122        return new I18nizableText("plugin." + _pluginName, "PLUGINS_WORKSPACES_PROJECT_SERVICE_MODULE_MINISITE_LABEL");
123    }
124    public I18nizableText getModuleDescription()
125    {
126        return new I18nizableText("plugin." + _pluginName, "PLUGINS_WORKSPACES_PROJECT_SERVICE_MODULE_MINISITE_DESCRIPTION");
127    }
128    @Override
129    protected I18nizableText getModulePageTitle()
130    {
131        return new I18nizableText("plugin." + _pluginName, "PLUGINS_WORKSPACES_PROJECT_WORKSPACE_PAGE_MINISITE_TITLE");
132    }
133    
134    /**
135     * Get the name of the first section of minisite
136     * @return the section's name
137     */
138    protected String getFirstSectionName()
139    {
140        return "section";
141    }
142    
143    /**
144     * Get the title of the first section of minisite
145     * @return the section's title
146     */
147    protected I18nizableText getFirstSectionTitle()
148    {
149        return new I18nizableText("plugin." + _pluginName, "PLUGINS_WORKSPACES_PROJECT_WORKSPACE_PAGE_MINISITE_FIRST_SECTION_TITLE");
150    }
151    
152    @Override
153    protected String getModulePageTemplate()
154    {
155        // the module page is a NODE page
156        return null;
157    }
158    
159    @Override
160    protected void initializeModulePage(ModifiablePage modulePage)
161    {
162        modulePage.untag("SECTION");
163        
164        ModifiablePage firstSection = _createAndInitializeFirstSectionPage(modulePage, getFirstSectionName(), getFirstSectionTitle());
165        if (firstSection != null)
166        {
167            _initializeFirstSectionDefaultZone(firstSection);
168        }
169    }
170    
171    /**
172     * Create the first section of minisite if does not exist
173     * @param modulePage the module page
174     * @param name the name of section to create
175     * @param pageTitle the page title
176     * @return the created page or null if already exist
177     */
178    protected ModifiablePage _createAndInitializeFirstSectionPage(ModifiablePage modulePage, String name, I18nizableText pageTitle)
179    {
180        if (!modulePage.hasChild(name))
181        {
182            ModifiablePage page = modulePage.createChild(name, "ametys:defaultPage");
183            
184            // Title should not be missing, but just in case if the i18n message or the whole catalog does not exists in the requested language
185            // to prevent a non-user-friendly error and still generate the project workspace.
186            page.setTitle(StringUtils.defaultIfEmpty(_i18nUtils.translate(pageTitle, modulePage.getSitemapName()), "Missing title"));
187            page.setType(PageType.CONTAINER);
188            page.setSiteName(modulePage.getSiteName());
189            page.setSitemapName(modulePage.getSitemapName());
190            page.setTemplate(ProjectConstants.MINISITE_TEMPLATE);
191            page.tag("SECTION");
192            
193            page.saveChanges();
194            
195            Map<String, Object> eventParams = new HashMap<>();
196            eventParams.put(org.ametys.web.ObservationConstants.ARGS_PAGE, page);
197            _observationManager.notify(new Event(org.ametys.web.ObservationConstants.EVENT_PAGE_ADDED, _currentUserProvider.getUser(), eventParams));
198            
199            return page;
200        }
201        else
202        {
203            return null;
204        }
205    }
206    
207    /**
208     * Initialize the default zone for the first section of mini site
209     * @param page The first page of minisite
210     */
211    protected void _initializeFirstSectionDefaultZone(ModifiablePage page)
212    {
213        ModifiableZone defaultZone = page.createZone("default");
214        
215        ModifiableZoneItem defaultZoneItem = defaultZone.addZoneItem();
216        defaultZoneItem.setType(ZoneType.CONTENT);
217        
218        try
219        {
220            ModifiableWorkflowAwareContent content = _createDefaultContent(page.getSite(), page.getSitemapName(), page.getTitle());
221            defaultZoneItem.setContent(content);
222            content.saveChanges();
223        }
224        catch (WorkflowException e)
225        {
226            getLogger().error("Unable to initialize the first page for minisite module, the page will not be editable until the content is manually created in the BackOffice", e);
227        }
228    }
229    
230    /**
231     * Create a new content for a minisite page of the minisite module
232     * @param site The site
233     * @param sitemapName the name of the sitemap
234     * @param title The content title
235     * @return The content
236     * @throws WorkflowException if an error occurred
237     */
238    protected ModifiableWorkflowAwareContent _createDefaultContent(Site site, String sitemapName, String title) throws WorkflowException
239    {
240        Map<String, Object> inputs = new HashMap<>();
241        inputs.put(org.ametys.cms.workflow.CreateContentFunction.CONTENT_TITLE_KEY, title);
242        inputs.put(org.ametys.cms.workflow.CreateContentFunction.CONTENT_NAME_KEY, FilterNameHelper.filterName(title));
243        inputs.put(org.ametys.cms.workflow.CreateContentFunction.CONTENT_TYPES_KEY, new String[] {WorkspacesConstants.PROJECT_ARTICLE_CONTENT_TYPE});
244        inputs.put(org.ametys.cms.workflow.CreateContentFunction.CONTENT_LANGUAGE_KEY, sitemapName);
245        inputs.put(CreateContentFunction.SITE_KEY, site.getName());
246        
247        AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow();
248        workflow.initialize(WorkspacesConstants.CONTENT_WORKFLOW_NAME, __INITIAL_WORKFLOW_ACTION_ID, inputs);
249    
250        @SuppressWarnings("unchecked")
251        Map<String, Object> results = (Map<String, Object>) inputs.get(AbstractWorkflowComponent.RESULT_MAP_KEY);
252        ModifiableWorkflowAwareContent content = (ModifiableWorkflowAwareContent) results.get(Content.class.getName());
253        
254        return content;
255    }
256    
257    @Override
258    public ModifiableResourceCollection getModuleRoot(Project project, boolean create)
259    {
260        try
261        {
262            ExplorerNode projectRootNode = project.getExplorerRootNode();
263            
264            if (projectRootNode instanceof ModifiableResourceCollection)
265            {
266                ModifiableResourceCollection projectRootNodeRc = (ModifiableResourceCollection) projectRootNode;
267                return _getAmetysObject(projectRootNodeRc, __WORKSPACES_MINISITE_NODE_NAME, JCRResourcesCollectionFactory.RESOURCESCOLLECTION_NODETYPE, create);
268            }
269            else
270            {
271                throw new IllegalClassException(ModifiableResourceCollection.class, projectRootNode.getClass());
272            }
273        }
274        catch (AmetysRepositoryException e)
275        {
276            throw new AmetysRepositoryException("Error getting the minisite root node.", e);
277        }
278    }
279    
280    @Override
281    public Set<String> getAllowedEventTypes()
282    {
283        return ImmutableSet.of(ObservationConstants.EVENT_MINISITE_PAGE_CREATED,
284                               ObservationConstants.EVENT_MINISITE_PAGE_UPDATED,
285                               ObservationConstants.EVENT_MINISITE_PAGE_RENAMED,
286                               ObservationConstants.EVENT_MINISITE_PAGE_DELETED);
287    }
288
289    @Override
290    public Map<String, Object> _getInternalStatistics(Project project, boolean isActive)
291    { 
292        if (isActive)
293        {
294            Map<String, Object> statistics = new HashMap<>();
295            try
296            {
297                AmetysObjectIterable<Content> results = _contentSearcherFactory.create(WorkspacesConstants.PROJECT_ARTICLE_CONTENT_TYPE)
298                        .search(new SiteQuery(project.getName()));
299                statistics.put(__ARTICLE_NUMBER_HEADER_ID, results.getSize());
300            }
301            catch (Exception e)
302            {
303                getLogger().error("Error searching wall content in project " + project.getId(), e);
304            }
305            return statistics;
306        }
307        else
308        {
309            return Map.of(__ARTICLE_NUMBER_HEADER_ID, __SIZE_INACTIVE);
310        }
311    }
312    
313    @Override
314    public List<StatisticColumn> _getInternalStatisticModel()
315    {
316        return List.of(new StatisticColumn(__ARTICLE_NUMBER_HEADER_ID, new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_PROJECT_STATISTICS_TOOL_COLUMN_MINISITE_ARTICLE_NUMBER"))
317                .withRenderer("Ametys.plugins.workspaces.project.tool.ProjectsGridHelper.renderElements")
318                .withType(StatisticsColumnType.LONG)
319                .withGroup(GROUP_HEADER_ELEMENTS_ID));
320    }
321    
322    @Override
323    protected long _getModuleSize(Project project)
324    {
325        try
326        {
327            AmetysObjectIterable<Content> resultsIterable = _contentSearcherFactory.create(WorkspacesConstants.PROJECT_ARTICLE_CONTENT_TYPE)
328                    .search(new SiteQuery(project.getName()));
329
330            return resultsIterable.stream()
331                .map(content -> content.getValue("illustration/image"))
332                .filter(Objects::nonNull)
333                .filter(Binary.class::isInstance)
334                .map(Binary.class::cast)
335                .mapToLong(Binary::getLength)
336                .sum();
337        }
338        catch (Exception e)
339        {
340            getLogger().error("Error searching minisite content images in project " + project.getId(), e);
341            return __SIZE_ERROR;
342        }
343    }
344
345    @Override
346    protected boolean _showModuleSize()
347    {
348        return true;
349    }
350}