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}