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