001/*
002 *  Copyright 2016 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.project.observers;
017
018import java.io.File;
019import java.io.IOException;
020import java.util.ArrayList;
021import java.util.Collection;
022import java.util.HashMap;
023import java.util.List;
024import java.util.Map;
025
026import org.apache.avalon.framework.configuration.Configuration;
027import org.apache.avalon.framework.configuration.ConfigurationException;
028import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder;
029import org.apache.avalon.framework.context.Context;
030import org.apache.avalon.framework.context.ContextException;
031import org.apache.avalon.framework.context.Contextualizable;
032import org.apache.avalon.framework.service.ServiceException;
033import org.apache.avalon.framework.service.ServiceManager;
034import org.apache.avalon.framework.service.Serviceable;
035import org.apache.cocoon.Constants;
036import org.apache.commons.lang.StringUtils;
037import org.xml.sax.SAXException;
038
039import org.ametys.core.model.type.AbstractStringElementType;
040import org.ametys.core.observation.Event;
041import org.ametys.core.observation.ObservationManager;
042import org.ametys.core.observation.Observer;
043import org.ametys.core.user.CurrentUserProvider;
044import org.ametys.core.util.I18nUtils;
045import org.ametys.plugins.repository.data.holder.ModifiableModelAwareDataHolder;
046import org.ametys.plugins.repository.data.holder.group.impl.ModifiableModelAwareRepeaterEntry;
047import org.ametys.plugins.repository.model.RepeaterDefinition;
048import org.ametys.plugins.workflow.support.WorkflowProvider;
049import org.ametys.plugins.workspaces.project.ProjectManager;
050import org.ametys.plugins.workspaces.project.objects.Project;
051import org.ametys.runtime.i18n.I18nizableText;
052import org.ametys.runtime.model.ElementDefinition;
053import org.ametys.runtime.model.ModelItem;
054import org.ametys.runtime.plugin.component.AbstractLogEnabled;
055import org.ametys.runtime.plugin.component.PluginAware;
056import org.ametys.web.ObservationConstants;
057import org.ametys.web.repository.page.ModifiablePage;
058import org.ametys.web.repository.page.ModifiableZone;
059import org.ametys.web.repository.page.ModifiableZoneItem;
060import org.ametys.web.repository.page.Page.PageType;
061import org.ametys.web.repository.page.ZoneItem.ZoneType;
062import org.ametys.web.repository.site.Site;
063import org.ametys.web.repository.sitemap.Sitemap;
064import org.ametys.web.service.Service;
065import org.ametys.web.service.ServiceExtensionPoint;
066import org.ametys.web.service.ServiceParameter;
067import org.ametys.web.skin.Skin;
068import org.ametys.web.skin.SkinTemplate;
069import org.ametys.web.skin.SkinTemplateZone;
070import org.ametys.web.skin.SkinsManager;
071
072/**
073 * Initializes all project workspace pages when a sitemap is created.
074 */
075public class InitializeProjectSitemapObserver extends AbstractLogEnabled implements Observer, Serviceable, PluginAware, Contextualizable
076{
077    /** Workspaces project manager */
078    protected ProjectManager _projectManager;
079    
080    /** The i18n utils. */
081    protected I18nUtils _i18nUtils;
082    
083    /** The observation manager. */
084    protected ObservationManager _observationManager;
085    
086    /** The skins manager. */
087    protected SkinsManager _skinsManager;
088    
089    /** Current user provider */
090    protected CurrentUserProvider _currentUserProvider;
091    
092    /** The service extension point. */
093    protected ServiceExtensionPoint _serviceEP;
094    
095    /** The plugin name. */
096    protected String _pluginName;
097    
098    /** The i18n catalogue. */
099    protected String _i18nCatalogue;
100
101    /** The workflow provider */
102    protected WorkflowProvider _workflowProvider;
103
104    private org.apache.cocoon.environment.Context _cocoonContext;
105
106    @Override
107    public void setPluginInfo(String pluginName, String featureName, String id)
108    {
109        _pluginName = pluginName;
110        _i18nCatalogue = "plugin." + pluginName;
111    }
112    
113    @Override
114    public void contextualize(Context context) throws ContextException
115    {
116        _cocoonContext = (org.apache.cocoon.environment.Context) context.get(Constants.CONTEXT_ENVIRONMENT_CONTEXT);
117    }
118    
119    @Override
120    public void service(ServiceManager manager) throws ServiceException
121    {
122        _projectManager = (ProjectManager) manager.lookup(ProjectManager.ROLE);
123        _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE);
124        _observationManager = (ObservationManager) manager.lookup(ObservationManager.ROLE);
125        _skinsManager = (SkinsManager) manager.lookup(SkinsManager.ROLE);
126        _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE);
127        _serviceEP = (ServiceExtensionPoint) manager.lookup(ServiceExtensionPoint.ROLE);
128        _workflowProvider = (WorkflowProvider) manager.lookup(WorkflowProvider.ROLE);
129    }
130    
131    @Override
132    public boolean supports(Event event)
133    {
134        // Listen on site updated because it is too early to listen on sitemap added (we need the site be configured and synchronized in live)
135        return event.getId().equals(ObservationConstants.EVENT_SITE_UPDATED);
136    }
137
138    @Override
139    public int getPriority(Event event)
140    {
141        // Will be processed after live synchronization observers
142        return MAX_PRIORITY + 2000;
143    }
144
145    @Override
146    public void observe(Event event, Map<String, Object> transientVars) throws Exception
147    {
148        Site site = (Site) event.getArguments().get(ObservationConstants.ARGS_SITE);
149        if (site != null)
150        {
151            List<Project> projects = _projectManager.getProjectsForSite(site);
152            if (!projects.isEmpty())
153            {
154                Project project = projects.get(0);
155                
156                // Initialize each sitemap with a set of predefined and configured pages
157                for (Sitemap sitemap : site.getSitemaps())
158                {
159                    _initializeSitemap(sitemap, project);
160                }
161            }
162        }
163    }
164    
165    /**
166     * Initialize the given sitemap.
167     * @param sitemap the Sitemap object.
168     * @param project the corresponding project
169     */
170    protected void _initializeSitemap(Sitemap sitemap, Project project)
171    {
172        // Home page (index)
173        ModifiablePage indexPage = _createPage(sitemap, "index", new I18nizableText(_i18nCatalogue, "PLUGINS_WORKSPACES_PROJECT_WORKSPACE_PAGE_INDEX_TITLE"));
174        if (indexPage != null)
175        {
176            _initializeIndexPage(project, indexPage);
177        }
178        
179        _projectManager.initializeModulesSitemap(project, sitemap);
180        
181        // Notify of the sitemap change.
182        Map<String, Object> eventParams = new HashMap<>();
183        eventParams.put(ObservationConstants.ARGS_SITEMAP, sitemap);
184        _observationManager.notify(new Event(ObservationConstants.EVENT_SITEMAP_UPDATED, _currentUserProvider.getUser(), eventParams));
185    }
186    
187    /**
188     * Create a new page if not already exists
189     * @param sitemap The sitemap where the page will be created
190     * @param name The page's name
191     * @param i18nTitle The page's title
192     * @return the created page or <code>null</code> if page already exists
193     */
194    protected ModifiablePage _createPage(Sitemap sitemap, String name, I18nizableText i18nTitle)
195    {
196        if (!sitemap.hasChild(name))
197        {
198            ModifiablePage page = sitemap.createChild(name, "ametys:defaultPage");
199            
200            // Title should not be missing, but just in case if the i18n message or the whole catalog does not exists in the requested language
201            // to prevent a non-user-friendly error and still generate the project workspace.
202            page.setTitle(StringUtils.defaultIfEmpty(_i18nUtils.translate(i18nTitle, sitemap.getName()), "Missing title"));
203            page.setType(PageType.NODE);
204            page.setSiteName(sitemap.getSiteName());
205            page.setSitemapName(sitemap.getName());
206            
207            sitemap.saveChanges();
208            
209            return page;
210        }
211        else
212        {
213            return null;
214        }
215    }
216    
217    /**
218     * Initialize the index page.
219     * @param project The project
220     * @param indexPage the index page.
221     */
222    protected void _initializeIndexPage(Project project, ModifiablePage indexPage)
223    {
224        Site site = indexPage.getSite();
225        Skin skin = _skinsManager.getSkin(site.getSkinId());
226        
227        String filePath = _cocoonContext.getRealPath("/skins/" + skin.getId() + "/conf/project-home-model.xml");
228        File cfgFile = new File (filePath);
229        
230        if (cfgFile.exists())
231        {
232            _initializeIndexPage(project, skin, indexPage, cfgFile);
233        }
234        else
235        {
236            getLogger().error("The model file '{}' for project home page does not exists. The '%s' page of the project workspace %s will be initialized with default values", filePath, indexPage.getPathInSitemap(), project.getName());
237        }
238        
239    }
240    
241    /**
242     * Initialize the index page from a configuration file
243     * @param project the project
244     * @param skin the skin
245     * @param indexPage the index page
246     * @param cfgFile the configuration file
247     */
248    protected void _initializeIndexPage(Project project, Skin skin, ModifiablePage indexPage, File cfgFile)
249    {
250        try
251        {
252            Configuration configuration = new DefaultConfigurationBuilder().buildFromFile(cfgFile);
253            String templateId = configuration.getAttribute("template", "project-index");
254            
255            SkinTemplate template = skin.getTemplate(templateId);
256            if (template == null)
257            {
258                getLogger().error("The project home page template use an unexsiting template '{}'. The '{}' page of the project workspace '{}' could not be initialized.", templateId, indexPage.getPathInSitemap(), project.getName());
259                return;
260            }
261            
262            // Set the type and template.
263            indexPage.setType(PageType.CONTAINER);
264            indexPage.setTemplate(template.getId());
265            
266            // Initialize the zones.
267            Map<String, SkinTemplateZone> templateZones = template.getZones();
268            
269            for (Configuration zoneConf : configuration.getChildren())
270            {
271                String zoneName = zoneConf.getAttribute("id");
272                if (templateZones.containsKey(zoneName))
273                {
274                    ModifiableZone zone = indexPage.createZone(zoneName);
275                    
276                    for (Configuration serviceConf : zoneConf.getChildren("service"))
277                    {
278                        String serviceId = serviceConf.getAttribute("id");
279                        Configuration paramsConf = serviceConf.getChild("parameters", true);
280                        
281                        Service service = _serviceEP.getExtension(serviceId);
282                        
283                        if (service != null)
284                        {
285                            ModifiableZoneItem zoneItem = zone.addZoneItem();
286                            zoneItem.setType(ZoneType.SERVICE);
287                            zoneItem.setServiceId(serviceId);
288                            
289                            Map<String, ModelItem> serviceModelParams = service.getParameters();
290                            _setServiceParameters(serviceModelParams.values(), zoneItem.getServiceParameters(), paramsConf, indexPage.getSitemapName());
291                        }
292                        else
293                        {
294                            getLogger().error("The project home page template defines an unexsiting service '{}'. The '{}' page of the project workspace {} could not be initialized properly.", serviceId, indexPage.getPathInSitemap(), project.getName());
295                        }
296                    }
297                }
298                else
299                {
300                    getLogger().error("The project home page template defines an unexsiting zone '{}' for template '{}'. The '{}' page of the project workspace {} could not be initialized properly.", zoneName, templateId, indexPage.getPathInSitemap(), project.getName());
301                }
302            }
303            
304            indexPage.saveChanges();
305            
306            Map<String, Object> eventParams = new HashMap<>();
307            eventParams.put(ObservationConstants.ARGS_PAGE, indexPage);
308            _observationManager.notify(new Event(ObservationConstants.EVENT_PAGE_ADDED, _currentUserProvider.getUser(), eventParams));
309        }
310        catch (ConfigurationException | SAXException | IOException e)
311        {
312            getLogger().error("Fail to read the project home page model '{}'. The '%s' page of the project workspace %s could not be initialized", cfgFile.getAbsolutePath(), indexPage.getPathInSitemap(), project.getName(), e);
313        }
314    }
315    
316    private void _setServiceParameters (Collection<ModelItem> modelItems, ModifiableModelAwareDataHolder dataHolder, Configuration paramsConf, String lang) throws ConfigurationException
317    {
318        for (ModelItem modelItem : modelItems)
319        {
320            String paramName = modelItem.getName();
321            
322            if (modelItem instanceof ElementDefinition)
323            {
324                Configuration paramConf = paramsConf.getChild(paramName, false);
325                if (paramConf != null)
326                {
327                    if (((ElementDefinition) modelItem).isMultiple())
328                    {
329                        // FIXME we need a XML service descriptor, for now only multiple values of type string are supported
330                        if (((ElementDefinition) modelItem).getType() instanceof AbstractStringElementType)
331                        {
332                            List<String> typedValues = new ArrayList<>();
333                            
334                            Configuration[] valuesConf = paramConf.getChildren("value");
335                            for (Configuration valueConf : valuesConf)
336                            {
337                                typedValues.add(valueConf.getValue());
338                            }
339                            
340                            dataHolder.setValue(paramName, typedValues.toArray(new String[typedValues.size()]));
341                        }
342                        else
343                        {
344                            getLogger().warn("Only string multiple values are supported in the XML configuration of a service. Can not evaluate the service parameter " + modelItem.getPath());
345                        }
346                        
347                    }
348                    else
349                    {
350                        String paramValue = paramConf.getValue("");
351                        boolean i18n = paramConf.getAttributeAsBoolean("i18n", false);
352                        if (i18n)
353                        {
354                            I18nizableText i18nText = I18nizableText.parseI18nizableText(paramConf, "application");
355                            paramValue = _i18nUtils.translate(i18nText, lang);
356                        }
357                        
358                        Object typedValue = ((ElementDefinition) modelItem).getType().castValue(paramValue);
359                        dataHolder.setValue(paramName, typedValue);
360                    }
361                    
362                    
363                }
364                else if (((ElementDefinition) modelItem).getDefaultValue() != null)
365                {
366                    // set default value
367                    dataHolder.setValue(paramName, ((ServiceParameter) modelItem).getDefaultValue());
368                }
369            }
370            else if (modelItem instanceof RepeaterDefinition)
371            {
372                Configuration[] entriesConf = paramsConf.getChild(paramName, true).getChildren("entry");
373                
374                List<ModelItem> subModelItems = ((RepeaterDefinition) modelItem).getChildren();
375                
376                int entryCount = 1;
377                for (Configuration entryConf : entriesConf)
378                {
379                    ModifiableModelAwareRepeaterEntry entry = dataHolder.getRepeater(paramName, true).addEntry(entryCount);
380                    _setServiceParameters(subModelItems, entry, entryConf, lang);
381                    entryCount++;
382                }
383            }
384        }
385        
386    }
387}