001/*
002 *  Copyright 2012 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.clientsideelement;
017
018import java.util.HashMap;
019import java.util.Map;
020import java.util.Set;
021
022import org.apache.avalon.framework.configuration.Configuration;
023import org.apache.avalon.framework.configuration.ConfigurationException;
024import org.apache.avalon.framework.configuration.DefaultConfiguration;
025import org.apache.avalon.framework.service.ServiceException;
026import org.apache.avalon.framework.service.ServiceManager;
027import org.apache.commons.lang.StringUtils;
028
029import org.ametys.cms.contenttype.ContentType;
030import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
031import org.ametys.cms.contenttype.ContentTypesHelper;
032import org.ametys.cms.repository.Content;
033import org.ametys.core.DevMode;
034import org.ametys.core.DevMode.DEVMODE;
035import org.ametys.core.observation.Event;
036import org.ametys.core.observation.ObservationManager;
037import org.ametys.core.right.RightManager.RightResult;
038import org.ametys.core.ui.Callable;
039import org.ametys.core.ui.SimpleMenu;
040import org.ametys.core.ui.StaticClientSideElement;
041import org.ametys.core.util.I18nUtils;
042import org.ametys.plugins.repository.AmetysObject;
043import org.ametys.plugins.repository.AmetysObjectResolver;
044import org.ametys.runtime.authentication.AccessDeniedException;
045import org.ametys.runtime.i18n.I18nizableText;
046import org.ametys.runtime.model.View;
047import org.ametys.web.ObservationConstants;
048import org.ametys.web.contenttype.SkinContentViewHelper;
049import org.ametys.web.contenttype.SkinContentViewHelper.SkinContentView;
050import org.ametys.web.repository.page.ContentTypesAssignmentHandler;
051import org.ametys.web.repository.page.LockablePage;
052import org.ametys.web.repository.page.ModifiableZoneItem;
053import org.ametys.web.repository.page.SitemapElement;
054import org.ametys.web.repository.page.Zone;
055import org.ametys.web.repository.page.ZoneItem;
056import org.ametys.web.repository.page.ZoneItem.ZoneType;
057import org.ametys.web.skin.SkinsManager;
058
059/**
060 * Menu giving the possibility to change the view rendering the content in a given ZoneItem.
061 */
062public class SetContentViewMenu extends SimpleMenu
063{
064    /** The content type extension point. */
065    protected ContentTypeExtensionPoint _contentTypeExtensionPoint;
066    
067    /** The content types assignment handler */
068    protected ContentTypesAssignmentHandler _cTypeHandler;
069    
070    /** The content types helper */
071    protected ContentTypesHelper _contentTypesHelper;
072    
073    /** Repository content */
074    protected AmetysObjectResolver _resolver;
075    
076    /** Helper for skin views */
077    protected SkinContentViewHelper _skinContentViewHelper;
078    
079    /** The observation manager */
080    protected ObservationManager _observationManager;
081
082    /** The skins manager */
083    protected SkinsManager _skinsManager;
084    
085    /** The i18n utils */
086    protected I18nUtils _i18nUtils;
087    
088    /** The Ametys object resolver instance */
089    protected AmetysObjectResolver _ametysObjectResolver;
090    
091    /** The content types assignment handler instance */
092    protected ContentTypesAssignmentHandler _contentTypesAssignmentHandler;
093    
094    private boolean _galleryInitialized;
095
096    
097    @Override
098    public void service(ServiceManager serviceManager) throws ServiceException
099    {
100        super.service(serviceManager);
101        _contentTypeExtensionPoint = (ContentTypeExtensionPoint) serviceManager.lookup(ContentTypeExtensionPoint.ROLE);
102        _cTypeHandler = (ContentTypesAssignmentHandler) serviceManager.lookup(ContentTypesAssignmentHandler.ROLE);
103        _resolver = (AmetysObjectResolver) serviceManager.lookup(AmetysObjectResolver.ROLE);
104        _i18nUtils = (I18nUtils) serviceManager.lookup(I18nUtils.ROLE);
105        _contentTypesHelper = (ContentTypesHelper) serviceManager.lookup(ContentTypesHelper.ROLE);
106        _observationManager = (ObservationManager) serviceManager.lookup(ObservationManager.ROLE);
107        _skinsManager = (SkinsManager) serviceManager.lookup(SkinsManager.ROLE);
108        _skinContentViewHelper = (SkinContentViewHelper) serviceManager.lookup(SkinContentViewHelper.ROLE);
109        _ametysObjectResolver = (AmetysObjectResolver) serviceManager.lookup(AmetysObjectResolver.ROLE);
110        _contentTypesAssignmentHandler = (ContentTypesAssignmentHandler) serviceManager.lookup(ContentTypesAssignmentHandler.ROLE);
111    }
112    
113    @Override
114    protected void _getGalleryItems(Map<String, Object> parameters, Map<String, Object> contextualParameters)
115    {
116        try
117        {
118            _lazyInitializeContentViewGallery();
119        }
120        catch (Exception e)
121        {
122            throw new IllegalStateException("Unable to lookup client side element local components", e);
123        }
124        
125        super._getGalleryItems(parameters, contextualParameters);
126    }
127    
128    private synchronized void _lazyInitializeContentViewGallery() throws ConfigurationException
129    {
130        DEVMODE devMode = DevMode.getDeveloperMode(null);
131        
132        if (!_galleryInitialized || devMode == DEVMODE.DEVELOPMENT || devMode == DEVMODE.SUPER_DEVELOPPMENT)
133        {
134            if (_galleryInitialized)
135            {
136                // reinitialize the gallery items
137                _galleryItems.clear();
138                _initializeGalleryItemManager();
139            }
140            
141            Set<String> contentTypesIds = _contentTypeExtensionPoint.getExtensionsIds();
142            
143            if (contentTypesIds.size() > 0)
144            {
145                GalleryItem galleryItem = new GalleryItem();
146                
147                for (String contentTypeId : contentTypesIds)
148                {
149                    ContentType contentType = _contentTypeExtensionPoint.getExtension(contentTypeId);
150                    
151                    GalleryGroup galleryGroup = new GalleryGroup(contentType.getLabel());
152                    galleryItem.addGroup(galleryGroup);
153                    
154                    for (String viewName : contentType.getViewNames(false))
155                    {
156                        View view = contentType.getView(viewName);
157                        
158                        String id = this.getId() + "." + contentTypeId + "." + viewName;
159                        
160                        Configuration conf = _getViewItemConfiguration(id, view, contentTypeId);
161                        _getGalleryItemManager().addComponent(_pluginName, null, id, StaticClientSideElement.class, conf);
162                        galleryGroup.addItem(new UnresolvedItem(id, true));
163                    }
164                    
165                    // Get other views from all skins
166                    for (String skinName : _skinsManager.getSkins())
167                    {
168                        Set<SkinContentView> skinViews = _skinContentViewHelper.getContentViewsFromSkin(skinName, contentType);
169                        for (SkinContentView skinView : skinViews)
170                        {
171                            String id = this.getId() + "." + skinName + "." + contentTypeId + "." + skinView.name();
172                            Configuration conf = _getViewItemConfiguration(id, skinView, contentTypeId);
173                            _getGalleryItemManager().addComponent(_pluginName, null, id, StaticClientSideElement.class, conf);
174                            galleryGroup.addItem(new UnresolvedItem(id, true));
175                        }
176                    }
177                }
178                
179                _galleryItems.add(galleryItem);
180            }
181            
182            if (_galleryItems.size() > 0)
183            {
184                try
185                {
186                    _getGalleryItemManager().initialize();
187                }
188                catch (Exception e)
189                {
190                    throw new ConfigurationException("Unable to lookup parameter local components", e);
191                }
192            }
193        }
194        
195        _galleryInitialized = true;
196    }
197    
198    /**
199     * Get the configuration for a view item
200     * @param itemId The item id
201     * @param view The view
202     * @param cTypeId The content type id
203     * @return The configuration
204     */
205    protected Configuration _getViewItemConfiguration(String itemId, View view, String cTypeId)
206    {
207        return new ViewItemConfigurationBuilder(itemId, this._script, _i18nUtils)
208                    .withName(view.getName())
209                    .withLabel(view.getLabel())
210                    .withDescription(view.getDescription())
211                    .withContentType(cTypeId)
212                    .withIconGlyph(view.getIconGlyph(), view.getIconDecorator())
213                    .withIcon(view.getSmallIcon(), view.getMediumIcon(), view.getLargeIcon())
214                    .build();
215    }
216    
217    /**
218     * Get the configuration for a skin view item
219     * @param itemId The item id
220     * @param view The skin view
221     * @param cTypeId The content type id
222     * @return The configuration
223     */
224    protected Configuration _getViewItemConfiguration(String itemId, SkinContentView view, String cTypeId)
225    {
226        return new ViewItemConfigurationBuilder(itemId, this._script, _i18nUtils)
227                .withName(view.name())
228                .withSkin(view.skinName())
229                .withLabel(view.label())
230                .withDescription(view.description())
231                .withContentType(cTypeId)
232                .withIconGlyph(view.iconGlyph(), view.iconDecorator())
233                .withIcon(view.iconSmall(), view.iconMedium(), view.iconLarge())
234                .build();
235    }
236    
237    
238    /**
239     * Builder to get a view item configuration
240     *
241     */
242    public static class ViewItemConfigurationBuilder
243    {
244        private DefaultConfiguration _classConf;
245        private I18nUtils _i18nUtils;
246        private String _itemId;
247        private Script _script;
248
249        ViewItemConfigurationBuilder(String itemId, Script script, I18nUtils i18nUtils)
250        {
251            _itemId = itemId;
252            _script = script;
253            _i18nUtils = i18nUtils;
254            _classConf = new DefaultConfiguration("class");
255            _classConf.setAttribute("name", "Ametys.ribbon.element.ui.ButtonController");
256        }
257        
258        /**
259         * Set the view name. This parameter is mandatory
260         * @param viewName the view name
261         * @return this view item configuration
262         */
263        public ViewItemConfigurationBuilder withName(String viewName)
264        {
265            DefaultConfiguration nameConf = new DefaultConfiguration("name");
266            nameConf.setValue(viewName);
267            _classConf.addChild(nameConf);
268            return this;
269        }
270        
271        /**
272         * Set the skin name. May be null for a view common to all skin
273         * @param skinName the skin name
274         * @return this view item configuration
275         */
276        public ViewItemConfigurationBuilder withSkin(String skinName)
277        {
278            DefaultConfiguration nameConf = new DefaultConfiguration("skinName");
279            nameConf.setValue(skinName);
280            _classConf.addChild(nameConf);
281            return this;
282        }
283        /**
284         * Set the item's label. This parameter is mandatory
285         * @param label the label
286         * @return this view item configuration
287         */
288        public ViewItemConfigurationBuilder withLabel(I18nizableText label)
289        {
290            DefaultConfiguration labelConf = new DefaultConfiguration("label");
291            labelConf.setValue(_i18nUtils.translate(label));
292            _classConf.addChild(labelConf);
293            return this;
294        }
295        
296        /**
297         * Set the item's description. This parameter is mandatory
298         * @param description the description
299         * @return this view item configuration
300         */
301        public ViewItemConfigurationBuilder withDescription(I18nizableText description)
302        {
303            DefaultConfiguration descConf = new DefaultConfiguration("description");
304            descConf.setValue(_i18nUtils.translate(description));
305            _classConf.addChild(descConf);
306            return this;
307        }
308        
309        /**
310         * Set the content type. This parameter is mandatory
311         * @param cTypeId the content type id.
312         * @return this view item configuration
313         */
314        public ViewItemConfigurationBuilder withContentType(String cTypeId)
315        {
316            DefaultConfiguration cTypeConf = new DefaultConfiguration("contentType");
317            cTypeConf.setValue(cTypeId);
318            _classConf.addChild(cTypeConf);
319            return this;
320        }
321        
322        /**
323         * Set the icon glyph and decorator. May be null.
324         * @param iconGlyph the icon glyph. May be null.
325         * @param iconDecorator the icon decorator. May be null.
326         * @return this view item configuration
327         */
328        public ViewItemConfigurationBuilder withIconGlyph(String iconGlyph, String iconDecorator)
329        {
330            if (iconGlyph != null)
331            {
332                DefaultConfiguration iconGlyphConf = new DefaultConfiguration("icon-glyph");
333                iconGlyphConf.setValue(iconGlyph);
334                _classConf.addChild(iconGlyphConf);
335                
336                if (iconDecorator != null)
337                {
338                    DefaultConfiguration iconDecoratorConf = new DefaultConfiguration("icon-decorator");
339                    iconDecoratorConf.setValue(iconDecorator);
340                    _classConf.addChild(iconDecoratorConf);
341                }
342            }
343            
344            return this;
345        }
346        
347        /**
348         * Set the icon. May be null.
349         * @param iconSmall the small icon (16x16 pixels)
350         * @param iconMedium the medium icon (32x32 pixels)
351         * @param iconLarge the large icon (48x48 pixels)
352         * @return this view item configuration
353         */
354        public ViewItemConfigurationBuilder withIcon(String iconSmall, String iconMedium, String iconLarge)
355        {
356            if (iconMedium != null)
357            {
358                DefaultConfiguration iconSmallConf = new DefaultConfiguration("icon-small");
359                iconSmallConf.setValue(iconSmall);
360                _classConf.addChild(iconSmallConf);
361                DefaultConfiguration iconMediumConf = new DefaultConfiguration("icon-medium");
362                iconMediumConf.setValue(iconMedium);
363                _classConf.addChild(iconMediumConf);
364                DefaultConfiguration iconLargeConf = new DefaultConfiguration("icon-large");
365                iconLargeConf.setValue(iconLarge);
366                _classConf.addChild(iconLargeConf);
367            }
368            return this;
369        }
370        
371        /**
372         * Build the configuration
373         * @return the configuration
374         */
375        public Configuration build()
376        {
377            DefaultConfiguration conf = new DefaultConfiguration("extension");
378            conf.setAttribute("id", _itemId);
379            
380            // Common configuration
381            @SuppressWarnings("unchecked")
382            Map<String, Object> commonConfig = (Map<String, Object>) _script.getParameters().get("items-config");
383            for (String tagName : commonConfig.keySet())
384            {
385                DefaultConfiguration c = new DefaultConfiguration(tagName);
386                c.setValue(String.valueOf(commonConfig.get(tagName)));
387                _classConf.addChild(c);
388            }
389            
390            conf.addChild(_classConf);
391            return conf;
392        }
393    }
394    
395    /**
396     * Set the view of a content
397     * @param zoneItemId the id of the zone item
398     * @param viewName the name of the view to use
399      */
400    @Callable(rights = Callable.CHECKED_BY_IMPLEMENTATION)
401    public void setContentView(String zoneItemId, String viewName)
402    {
403        AmetysObject object = _resolver.resolveById(zoneItemId);
404        if (!(object instanceof ModifiableZoneItem zoneItem))
405        {
406            throw new IllegalArgumentException("The provided ID '" + zoneItemId + "' does not reference a modifiable zone item.");
407        }
408        
409        SitemapElement sitemapElement = zoneItem.getZone().getSitemapElement();
410        if (_rightManager.currentUserHasRight("Web_Rights_Page_SetContentView", sitemapElement) != RightResult.RIGHT_ALLOW)
411        {
412            throw new AccessDeniedException(_currentUserProvider.getUser() + " tried to set the content view of zone item " + zoneItemId + " without sufficient right");
413        }
414        if (!zoneItem.getType().equals(ZoneType.CONTENT))
415        {
416            throw new IllegalArgumentException("The zone item '" + zoneItemId + "' does not contain a content.");
417        }
418        if (sitemapElement instanceof LockablePage lockablePage && lockablePage.isLocked())
419        {
420            throw new IllegalArgumentException("You cannot change view of a content in a locked page " + lockablePage.getId());
421        }
422        
423        Content content = zoneItem.getContent();
424        
425        String modelViewName = viewName;
426        SkinContentView skinContentView = _skinContentViewHelper.getContentViewFromSkin(viewName, content);
427        if (skinContentView != null)
428        {
429            // the requested view is a skin view, get the associated model view to check model
430            modelViewName = skinContentView.modelViewName();
431        }
432        
433        View view = _contentTypesHelper.getView(modelViewName, content.getTypes(), content.getMixinTypes());
434        if (view == null)
435        {
436            throw new IllegalArgumentException("The view '" + viewName + "' can't be assigned on the content '" + StringUtils.join(content.getTypes(), ",")  + "'.");
437        }
438        
439        if (getLogger().isDebugEnabled())
440        {
441            getLogger().debug("Setting view '" + viewName + "' in the content zone item '" + zoneItemId + "'.");
442        }
443        
444        // Set the view name.
445        zoneItem.setViewName(viewName);
446        
447        zoneItem.saveChanges();
448        
449        Map<String, Object> eventParams = new HashMap<>();
450        eventParams.put(ObservationConstants.ARGS_SITEMAP_ELEMENT, sitemapElement);
451        eventParams.put(ObservationConstants.ARGS_ZONE_ITEM_ID, zoneItemId);
452        eventParams.put(ObservationConstants.ARGS_ZONE_TYPE, ZoneType.CONTENT);
453        _observationManager.notify(new Event(ObservationConstants.EVENT_ZONEITEM_MOVED, _currentUserProvider.getUser(), eventParams));
454    }
455    
456    /**
457     * Get the list of the available views for the given zone item denoting a content
458     * @param zoneitemId The zone item id
459     * @return The list of the view names by content type. Can be null or empty.
460     */
461    @Callable(rights = Callable.NO_CHECK_REQUIRED)
462    public Map<String, Set<String>> getViews(String zoneitemId)
463    {
464        ZoneItem zoneItem = _ametysObjectResolver.resolveById(zoneitemId);
465        Zone zone = zoneItem.getZone();
466        SitemapElement sitemapElement = zone.getSitemapElement();
467        
468        if (zoneItem.getType() == ZoneType.CONTENT)
469        {
470            Map<String, Set<String>> results = new HashMap<>();
471            for (String contentTypeId : zoneItem.getContent().getTypes())
472            {
473                results.put(contentTypeId, _contentTypesAssignmentHandler.getAvailableContentViews(sitemapElement, zone.getName(), contentTypeId));
474            }
475            return results;
476        }
477        else
478        {
479            // Not supported
480            return null;
481        }
482    }
483}