001/*
002 *  Copyright 2010 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.repository.page.jcr;
017
018import java.util.HashMap;
019import java.util.List;
020import java.util.Map;
021import java.util.Optional;
022
023import javax.jcr.Node;
024import javax.jcr.RepositoryException;
025
026import org.apache.commons.collections4.MapUtils;
027import org.slf4j.Logger;
028import org.slf4j.LoggerFactory;
029import org.xml.sax.ContentHandler;
030import org.xml.sax.SAXException;
031
032import org.ametys.cms.data.holder.impl.DefaultModifiableModelAwareDataHolder;
033import org.ametys.cms.repository.Content;
034import org.ametys.plugins.repository.AmetysObject;
035import org.ametys.plugins.repository.AmetysRepositoryException;
036import org.ametys.plugins.repository.CopiableAmetysObject;
037import org.ametys.plugins.repository.ModifiableTraversableAmetysObject;
038import org.ametys.plugins.repository.RepositoryConstants;
039import org.ametys.plugins.repository.RepositoryIntegrityViolationException;
040import org.ametys.plugins.repository.data.holder.ModifiableModelAwareDataHolder;
041import org.ametys.plugins.repository.data.holder.ModifiableModelLessDataHolder;
042import org.ametys.plugins.repository.data.holder.impl.DefaultModifiableModelLessDataHolder;
043import org.ametys.plugins.repository.data.repositorydata.ModifiableRepositoryData;
044import org.ametys.plugins.repository.data.repositorydata.impl.JCRRepositoryData;
045import org.ametys.plugins.repository.jcr.JCRAmetysObject;
046import org.ametys.plugins.repository.jcr.SimpleAmetysObject;
047import org.ametys.plugins.repository.metadata.ModifiableCompositeMetadata;
048import org.ametys.runtime.model.View;
049import org.ametys.runtime.model.exception.NotUniqueTypeException;
050import org.ametys.runtime.model.exception.UnknownTypeException;
051import org.ametys.runtime.model.type.DataContext;
052import org.ametys.web.parameters.view.ViewParametersManager;
053import org.ametys.web.parameters.view.ViewParametersModel;
054import org.ametys.web.repository.content.SharedContent;
055import org.ametys.web.repository.content.WebContent;
056import org.ametys.web.repository.content.jcr.DefaultWebContent;
057import org.ametys.web.repository.page.ModifiableZone;
058import org.ametys.web.repository.page.ModifiableZoneItem;
059import org.ametys.web.repository.page.SitemapElement;
060import org.ametys.web.repository.page.Zone;
061import org.ametys.web.service.Service;
062
063import com.opensymphony.workflow.WorkflowException;
064
065/**
066 *  implementation of a {@link ModifiableZoneItem}.
067 */
068public class DefaultZoneItem extends SimpleAmetysObject<DefaultZoneItemFactory> implements ModifiableZoneItem, CopiableAmetysObject
069{
070    /** Constant for title metadata. */
071    public static final String METADATA_TYPE = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":type";
072    /** Constant for service metadata. */
073    public static final String METADATA_SERVICE = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":service";
074    /** Constant for content metadata. */
075    public static final String METADATA_CONTENT = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":content";
076    /** Constant service parameters node type. */
077    public static final String SERVICE_PARAM_NODETYPE = RepositoryConstants.NAMESPACE_PREFIX + ":compositeMetadata";
078    
079    /**
080     * Constant for "metadata set name" metadata.
081     * @deprecated Use {@link #METADATA_VIEW_NAME} instead
082     */
083    @Deprecated
084    public static final String METADATA_METADATASET_NAME = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":metadataSetName";
085    
086    /**
087     * Constant for view name metadata.
088     * TODO NEWATTRIBUTEAPI_SERVICE: Change the name of this metadata to viewName (there may be a lot of impacts..)
089     */
090    public static final String METADATA_VIEW_NAME = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":metadataSetName";
091    
092    private static final Logger __LOGGER = LoggerFactory.getLogger(DefaultZoneItem.class);
093
094    /**
095     * Creates a {@link DefaultZoneItem}.
096     * 
097     * @param node the node backing this {@link AmetysObject}.
098     * @param parentPath the parent path in the Ametys hierarchy.
099     * @param factory the {@link DefaultZoneItemFactory} which creates the AmetysObject.
100     */
101    public DefaultZoneItem(Node node, String parentPath, DefaultZoneItemFactory factory)
102    {
103        super(node, parentPath, factory);
104    }
105    
106    public ZoneType getType() throws AmetysRepositoryException
107    {
108        try
109        {
110            return ZoneType.valueOf(getNode().getProperty(METADATA_TYPE).getString());
111        }
112        catch (RepositoryException e)
113        {
114            throw new AmetysRepositoryException("Unable to get type property", e);
115        }
116    }
117
118    public void setType(ZoneType type) throws AmetysRepositoryException
119    {
120        try
121        {
122            getNode().setProperty(METADATA_TYPE, type.name());
123        }
124        catch (RepositoryException e)
125        {
126            throw new AmetysRepositoryException("Unable to set type property", e);
127        }
128    }
129
130    public <C extends Content> C getContent() throws AmetysRepositoryException
131    {
132        try
133        {
134            return _getFactory().<C>resolveAmetysObject(getNode().getProperty(METADATA_CONTENT).getNode());
135        }
136        catch (RepositoryException e)
137        {
138            throw new AmetysRepositoryException("Unable to get content property", e);
139        }
140    }
141
142    public <C extends Content> void setContent(C content) throws AmetysRepositoryException
143    {
144        try
145        {
146            // FIXME stop using getNode that is JCR but use AmetysObject API... if it seems cool to you
147            getNode().setProperty(METADATA_CONTENT, ((JCRAmetysObject) content).getNode());
148        }
149        catch (RepositoryException e)
150        {
151            throw new AmetysRepositoryException("Unable to set content property", e);
152        }    
153    }
154    
155    public String getViewName() throws AmetysRepositoryException
156    {
157        try
158        {
159            if (getNode().hasProperty(METADATA_VIEW_NAME))
160            {
161                return getNode().getProperty(METADATA_VIEW_NAME).getString();
162            }
163            return null;
164        }
165        catch (RepositoryException e)
166        {
167            throw new AmetysRepositoryException("Unable to get view name property.", e);
168        }
169    }
170    
171    @Override
172    public void setViewName(String viewName) throws AmetysRepositoryException
173    {
174        try
175        {
176            getNode().setProperty(METADATA_VIEW_NAME, viewName);
177        }
178        catch (RepositoryException e)
179        {
180            throw new AmetysRepositoryException("Unable to set view name property.", e);
181        }
182    }
183    
184    @Override
185    public String getServiceId() throws AmetysRepositoryException
186    {
187        try
188        {
189            return getNode().getProperty(METADATA_SERVICE).getString();
190        }
191        catch (RepositoryException e)
192        {
193            throw new AmetysRepositoryException("Unable to get service property", e);
194        }
195    }
196    
197    @Override
198    public void setServiceId(String serviceId) throws AmetysRepositoryException
199    {
200        try
201        {
202            getNode().setProperty(METADATA_SERVICE, serviceId);
203        }
204        catch (RepositoryException e)
205        {
206            throw new AmetysRepositoryException("Unable to set service property", e);
207        }
208    }
209    
210    /**
211     * {@inheritDoc}
212     * @throws IllegalStateException if the service referenced by this zone item does not exist
213     */
214    public ModifiableModelAwareDataHolder getServiceParameters() throws AmetysRepositoryException, IllegalStateException
215    {
216        String serviceId = getServiceId();
217        Service service = _getFactory().getService(serviceId);
218        if (service != null)
219        {
220            ModifiableRepositoryData repositoryData = _getServiceParametersRepositoryData();
221            return new DefaultModifiableModelAwareDataHolder(repositoryData, service);
222        }
223        else
224        {
225            throw new IllegalStateException("The service '" + serviceId + "' referenced by the zone item '" + getId() + "' (" + getPath() + ") does not exist");
226        }
227    }
228    
229    /**
230     * Retrieves the {@link ModifiableRepositoryData} containing the service parameters of this zone item
231     * @return the {@link ModifiableRepositoryData} containing the service parameters of this zone item
232     */
233    protected ModifiableRepositoryData _getServiceParametersRepositoryData()
234    {
235        JCRRepositoryData zoneItemRepositoryData = new JCRRepositoryData(getNode());
236        return zoneItemRepositoryData.hasValue(SERVICE_PARAMETERS_DATA_NAME) ? zoneItemRepositoryData.getRepositoryData(SERVICE_PARAMETERS_DATA_NAME) : zoneItemRepositoryData.addRepositoryData(SERVICE_PARAMETERS_DATA_NAME, SERVICE_PARAM_NODETYPE);
237    }
238    
239    public ModifiableModelAwareDataHolder getZoneItemParametersHolder() throws AmetysRepositoryException
240    {
241        JCRRepositoryData zoneItemRepositoryData = new JCRRepositoryData(getNode());
242        
243        ViewParametersManager viewParametersManager = _getFactory().getViewParametersManager();
244        Optional<ViewParametersModel> zoneItemViewParameters = viewParametersManager.getZoneItemViewParametersModel(this);
245        if (zoneItemViewParameters.isEmpty())
246        {
247            zoneItemViewParameters = Optional.of(new ViewParametersModel("zoneitem-no-param", new View(), MapUtils.EMPTY_SORTED_MAP));
248        }
249        return viewParametersManager.getParametersHolder(zoneItemRepositoryData, zoneItemViewParameters.get());
250    }
251    
252    public ModifiableModelLessDataHolder getDataHolder()
253    {
254        ModifiableRepositoryData repositoryData = new JCRRepositoryData(getNode());
255        return new DefaultModifiableModelLessDataHolder(_getFactory().getPageDataTypeExtensionPoint(), repositoryData);
256    }
257    
258    public void dataToSAX(ContentHandler contentHandler, DataContext context) throws SAXException, UnknownTypeException, NotUniqueTypeException
259    {
260        for (String dataName : getDataNames())
261        {
262            DataContext newContext = context.cloneContext().addSegmentToDataPath(dataName);
263            dataToSAX(contentHandler, dataName, newContext);
264        }
265    }
266    
267    public void dataToSAX(ContentHandler contentHandler, String dataPath, DataContext context) throws SAXException
268    {
269        // Do not generate SAX events for service and view parameters
270        if (!_isViewOrServiceParameterRoot(dataPath))
271        {
272            ModifiableZoneItem.super.dataToSAX(contentHandler, dataPath, context);
273        }
274    }
275    
276    public Map<String, Object> dataToJSON(DataContext context) throws UnknownTypeException, NotUniqueTypeException
277    {
278        Map<String, Object> result = new HashMap<>();
279        for (String dataName : getDataNames())
280        {
281            DataContext newContext = context.cloneContext().addSegmentToDataPath(dataName);
282            result.put(dataName, dataToJSON(dataName, newContext));
283        }
284        
285        return result;
286    }
287    
288    public Object dataToJSON(String dataPath, DataContext context)
289    {
290        // Do not convert service and view parameters
291        return !_isViewOrServiceParameterRoot(dataPath)
292                ? ModifiableZoneItem.super.dataToJSON(dataPath, context)
293                : null;
294    }
295    
296    private boolean _isViewOrServiceParameterRoot(String dataPath)
297    {
298        return SERVICE_PARAMETERS_DATA_NAME.equals(dataPath)
299                || ViewParametersManager.CONTENT_VIEW_PARAMETERS_COMPOSITE_NAME.equals(dataPath)
300                || ViewParametersManager.SERVICE_VIEW_PARAMETERS_COMPOSITE_NAME.equals(dataPath)
301                || ViewParametersManager.VIEW_PARAMETERS_COMPOSITE_NAME.equals(dataPath);
302    }
303    
304    @Override
305    public ModifiableCompositeMetadata getMetadataHolder()
306    {
307        __LOGGER.warn("The getMetadataHolder method of DefaultZoneItem is deprecated. Use the getDataHolder instead. StackTrace: ", new Exception());
308        return super.getMetadataHolder();
309    }
310    
311    @Override
312    public ModifiableZoneItem copyTo(ModifiableTraversableAmetysObject parent, String name, List<String> restrictTo) throws AmetysRepositoryException
313    {
314        return copyTo(parent, name);
315    }
316    
317    @Override
318    public ModifiableZoneItem copyTo(ModifiableTraversableAmetysObject parent, String name) throws AmetysRepositoryException
319    {
320        if (parent instanceof DefaultZone)
321        {
322            ModifiableZone parentZone = (ModifiableZone) parent;
323            ModifiableZoneItem cZoneItem = parentZone.addZoneItem();
324            
325            ZoneType type = getType();
326            cZoneItem.setType(type);
327            
328            // Copy view parameters
329            getZoneItemParametersHolder().copyTo(cZoneItem.getZoneItemParametersHolder());
330
331            ViewParametersManager viewParametersManager = _getFactory().getViewParametersManager();
332            if (ZoneType.SERVICE.equals(type))
333            {
334                cZoneItem.setServiceId(getServiceId());
335                getServiceParameters().copyTo(cZoneItem.getServiceParameters());
336                
337                // Copy view parameters
338                viewParametersManager.copyServiceViewParameters(this, cZoneItem);
339            }
340            else
341            {
342                Content content = getContent();
343                if (content instanceof WebContent)
344                {
345                    WebContent webContent = (WebContent) content;
346                    if (getZone().getSitemapElement().getSiteName().equals(webContent.getSiteName()))
347                    {
348                        if (webContent instanceof SharedContent)
349                        {
350                            // No need to copy a shared content, set the reference to the shared content
351                            cZoneItem.setContent(webContent);
352                            cZoneItem.setViewName(getViewName());
353                        }
354                        else if (webContent instanceof DefaultWebContent modifiableContent)
355                        {
356                            Content cContent = modifiableContent.copyTo(webContent.getSite().getRootContents(), null, 0, false);
357                            
358                            // Update references to AmetysObject in RichText
359                            _getFactory().getCopySiteComponent().updateLinksInRichText(getZone().getSitemapElement(), cZoneItem.getZone().getSitemapElement(), content, cContent);
360                            
361                            // need to save before event notification
362                            modifiableContent.saveChanges();
363                            modifiableContent.checkpoint();
364
365                            // link to copied ZoneItem
366                            cZoneItem.setContent(cContent);
367                            cZoneItem.saveChanges();
368                            
369                            try
370                            {
371                                _getFactory().getContentDAO().notifyContentCopied(cContent, false);
372                            }
373                            catch (WorkflowException e)
374                            {
375                                throw new AmetysRepositoryException(e);
376                            }
377                        }
378                    }
379                    else
380                    {
381                        // Copy reference  
382                        cZoneItem.setContent(getContent());
383                    }
384                }
385                else
386                {
387                    // Copy reference
388                    cZoneItem.setContent(getContent());
389                }
390                
391                String viewName = getViewName();
392                if (viewName != null)
393                {
394                    cZoneItem.setViewName(viewName);
395                }
396                
397                // Copy view parameters
398                viewParametersManager.copyContentViewParameters(this, cZoneItem);
399            }
400            
401            return cZoneItem;
402        }
403        else
404        {
405            throw new AmetysRepositoryException("Can not copy a zone item " + getId() + " of type DefaultZoneItem to something that is not a DefaultZone " + parent.getId());
406        }
407    }
408
409    @Override
410    public boolean canMoveTo(AmetysObject newParent) throws AmetysRepositoryException
411    {
412        return newParent instanceof DefaultZone;
413    }
414    
415    @Override
416    public void moveTo(AmetysObject newParent, boolean renameIfExist) throws AmetysRepositoryException, RepositoryIntegrityViolationException
417    {
418        if (!canMoveTo(newParent))
419        {
420            throw new AmetysRepositoryException("Can not move a zone item " + getId() + " of type DefaultZoneItem to something that is not a DefaultZone " + newParent.getId());
421        }
422        
423        Node node = getNode();
424
425        try
426        {
427            DefaultZone newParentZone = (DefaultZone) newParent;
428                
429            // Move node
430            node.getSession().move(node.getPath(), newParentZone.getNode().getPath() + "/" + DefaultZone.ZONEITEMS_NODE_NAME + "/" + node.getName());
431        }
432        catch (RepositoryException e)
433        {
434            throw new AmetysRepositoryException("Can not move DefaultZoneItem " + getId() + " to DefaultZone " + newParent.getId(), e);
435        }
436    }
437    
438    @Override
439    public void orderBefore(AmetysObject siblingObject) throws AmetysRepositoryException
440    {
441        if (siblingObject != null && !(siblingObject instanceof JCRAmetysObject))
442        {
443            throw new AmetysRepositoryException(String.format("Unable to order zone item '%s' before sibling '%s' (sibling is not a JCRAmetysObject)", this.getId(), siblingObject.getId()));
444        }
445        
446        Node node = getNode();
447        Node siblingNode = siblingObject != null ? ((JCRAmetysObject) siblingObject).getNode() : null;
448        
449        try
450        {
451            node.getParent().orderBefore(node.getName() + "[" + node.getIndex() + "]", siblingNode != null ? siblingNode.getName() + "[" + siblingNode.getIndex() + "]" : null);
452        }
453        catch (RepositoryException e)
454        {
455            throw new AmetysRepositoryException(String.format("Unable to order zone item '%s' before sibling '%s'", this.getId(), siblingObject != null ? siblingObject.getId() : ""), e);
456        }
457    }
458    
459    @Override
460    public Zone getZone()
461    {
462        return getParent().getParent();
463    }
464
465    public ModifiableModelAwareDataHolder getContentViewParametersHolder(String contentViewName) throws AmetysRepositoryException
466    {
467        if (getType() != ZoneType.CONTENT)
468        {
469            throw new AmetysRepositoryException("Can't get content view parameters from a not content zone item");
470        }
471        
472        JCRRepositoryData zoneItemRepositoryData = new JCRRepositoryData(getNode());
473        
474        Content content = getContent();
475        
476        // Get the skin Id
477        SitemapElement sitemapElement = getZone().getSitemapElement();
478        String skinId = sitemapElement.getSite().getSkinId();
479        
480        ViewParametersManager viewParametersManager = _getFactory().getViewParametersManager();
481        Optional<ViewParametersModel> contentViewParameters = viewParametersManager.getContentViewParametersModel(skinId, content, contentViewName);
482        if (contentViewParameters.isEmpty())
483        {
484            contentViewParameters = Optional.of(new ViewParametersModel("content-no-param", new View(), MapUtils.EMPTY_SORTED_MAP));
485        }
486        
487        return viewParametersManager.getContentViewParametersHolder(zoneItemRepositoryData, contentViewName, contentViewParameters.get());
488    }
489
490    public ModifiableModelAwareDataHolder getServiceViewParametersHolder(String serviceViewName) throws AmetysRepositoryException
491    {
492        if (getType() != ZoneType.SERVICE)
493        {
494            throw new AmetysRepositoryException("Can't get service view parameters from a not service zone item");
495        }
496        
497        JCRRepositoryData zoneItemRepositoryData = new JCRRepositoryData(getNode());
498        
499        String serviceId = getServiceId();
500        
501        // Get the skin Id
502        SitemapElement sitemapElement = getZone().getSitemapElement();
503        String skinId = sitemapElement.getSite().getSkinId();
504        
505        ViewParametersManager viewParametersManager = _getFactory().getViewParametersManager();
506        Optional<ViewParametersModel> serviceViewParameters = viewParametersManager.getServiceViewParametersModel(skinId, serviceId, serviceViewName);
507        if (serviceViewParameters.isEmpty())
508        {
509            serviceViewParameters = Optional.of(new ViewParametersModel("service-no-param", new View(), MapUtils.EMPTY_SORTED_MAP));
510        }
511        
512        return viewParametersManager.getServiceViewParametersHolder(zoneItemRepositoryData, serviceViewName, serviceViewParameters.get());
513    }
514}