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