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