001/*
002 *  Copyright 2018 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.cms.data.holder.group.impl;
017
018import java.io.IOException;
019import java.util.ArrayList;
020import java.util.Collections;
021import java.util.Comparator;
022import java.util.HashMap;
023import java.util.List;
024import java.util.Map;
025import java.util.Optional;
026import java.util.SortedSet;
027import java.util.TreeSet;
028
029import org.apache.cocoon.xml.AttributesImpl;
030import org.apache.cocoon.xml.XMLUtils;
031import org.apache.commons.lang3.StringUtils;
032import org.apache.solr.common.SolrInputDocument;
033import org.xml.sax.Attributes;
034import org.xml.sax.ContentHandler;
035import org.xml.sax.SAXException;
036
037import org.ametys.cms.content.indexing.solr.SolrFieldNames;
038import org.ametys.cms.data.holder.IndexableDataHolder;
039import org.ametys.cms.data.holder.group.IndexableRepeater;
040import org.ametys.cms.data.holder.group.IndexableRepeaterEntry;
041import org.ametys.cms.data.holder.impl.IndexableDataHolderHelper;
042import org.ametys.cms.data.type.indexing.IndexableDataContext;
043import org.ametys.plugins.repository.data.holder.DataHolder;
044import org.ametys.plugins.repository.data.holder.group.ModelAwareRepeaterEntry;
045import org.ametys.plugins.repository.data.holder.group.ModifiableRepeater;
046import org.ametys.plugins.repository.data.holder.group.ModifiableRepeaterEntry;
047import org.ametys.plugins.repository.data.holder.group.Repeater;
048import org.ametys.plugins.repository.data.holder.group.RepeaterEntry;
049import org.ametys.plugins.repository.data.repositorydata.RepositoryData;
050import org.ametys.plugins.repository.model.RepeaterDefinition;
051import org.ametys.runtime.model.ModelViewItemGroup;
052import org.ametys.runtime.model.ViewHelper;
053import org.ametys.runtime.model.ViewItemAccessor;
054import org.ametys.runtime.model.exception.BadItemTypeException;
055import org.ametys.runtime.model.exception.NotUniqueTypeException;
056import org.ametys.runtime.model.exception.UndefinedItemPathException;
057import org.ametys.runtime.model.exception.UnknownTypeException;
058import org.ametys.runtime.model.type.DataContext;
059
060/**
061 * Class for model aware repeaters
062 */
063public class DefaultModelAwareRepeater implements IndexableRepeater
064{    
065    /** Definition of this repeater */
066    protected RepeaterDefinition _definition;
067    
068    /** Parent of the current {@link Repeater} */
069    protected IndexableDataHolder _parent;
070    
071    /** Root {@link DataHolder} */
072    protected IndexableDataHolder _root;
073    
074    /** Repository data to use to store entries in the repository */
075    protected RepositoryData _repositoryData;
076    
077    /**
078     * Creates a model aware repeater
079     * @param repositoryData the repository data of the repeater
080     * @param parent the parent of the created {@link Repeater}
081     * @param root the root {@link DataHolder}
082     * @param definition the definition of the repeater
083     */
084    public DefaultModelAwareRepeater(RepositoryData repositoryData, IndexableDataHolder parent, IndexableDataHolder root, RepeaterDefinition definition)
085    {
086        _repositoryData = repositoryData;
087        _definition = definition;
088        _parent = parent;
089        _root = root;
090    }
091    
092    @Override
093    public List<? extends IndexableRepeaterEntry> getEntries()
094    {
095        SortedSet<IndexableRepeaterEntry> entries = new TreeSet<>(new Comparator<ModelAwareRepeaterEntry>()
096        {
097            public int compare(ModelAwareRepeaterEntry entry1, ModelAwareRepeaterEntry entry2)
098            {
099                return Integer.compare(entry1.getPosition(), entry2.getPosition());
100            }
101        });
102        
103        for (String entryName : _repositoryData.getDataNames())
104        {
105            IndexableRepeaterEntry entry = getEntry(Integer.parseInt(entryName));
106            entries.add(entry);
107        }
108        
109        return Collections.unmodifiableList(new ArrayList<>(entries));
110    }
111    
112    public IndexableRepeaterEntry getEntry(int position)
113    {
114        if (1 <= position && position <= getSize())
115        {
116            RepositoryData entryRepositoryData = _repositoryData.getRepositoryData(String.valueOf(position));
117            return new DefaultModelAwareRepeaterEntry(entryRepositoryData, this, _definition);
118        }
119        else if (-getSize() < position && position <= 0)
120        {
121            // Find the positive equivalent position and call the getEntry method with this position
122            return getEntry(getSize() + position);
123        }
124        else
125        {
126            return null;
127        }
128    }
129
130    public int getSize()
131    {
132        return _repositoryData.getDataNames().size();
133    }
134    
135    public boolean hasEntry(int position)
136    {
137        if (1 <= position)
138        {
139            return _repositoryData.hasValue(String.valueOf(position));
140        }
141        else
142        {
143            return _repositoryData.hasValue(String.valueOf(getSize() + position));
144        }
145    }
146    
147    /**
148     * Retrieves the repeater's model
149     * @return the repeater's model
150     */
151    public RepeaterDefinition getModel()
152    {
153        return _definition;
154    }
155    
156    public void dataToSAX(ContentHandler contentHandler, String dataPath, DataContext context) throws SAXException
157    {
158        for (ModelAwareRepeaterEntry entry : getEntries())
159        {
160            XMLUtils.startElement(contentHandler, "entry", _getEntryAttributes(entry));
161            entry.dataToSAX(contentHandler, dataPath, context);
162            XMLUtils.endElement(contentHandler, "entry");
163        }
164    }
165    
166    public void dataToSAX(ContentHandler contentHandler, DataContext context) throws SAXException, BadItemTypeException
167    {
168        ModelViewItemGroup viewItemGroup = ModelViewItemGroup.of(_definition);
169        dataToSAX(contentHandler, viewItemGroup, context);
170    }
171    
172    public void dataToSAX(ContentHandler contentHandler, ViewItemAccessor viewItemAccessor, DataContext context) throws SAXException, BadItemTypeException
173    {
174        for (ModelAwareRepeaterEntry entry : getEntries())
175        {
176            XMLUtils.startElement(contentHandler, "entry", _getEntryAttributes(entry));
177            entry.dataToSAX(contentHandler, viewItemAccessor, context);
178            XMLUtils.endElement(contentHandler, "entry");
179        }
180    }
181    
182    private Attributes _getEntryAttributes(ModelAwareRepeaterEntry entry)
183    {
184        AttributesImpl entryAttrs = new AttributesImpl();
185        String entryName = Integer.toString(entry.getPosition());
186        entryAttrs.addCDATAAttribute("name", entryName);
187        return entryAttrs;
188    }
189    
190    public Map<String, Object> dataToJSON(String dataPath, DataContext context) throws IOException
191    {
192        return _dataToJSON(Optional.of(dataPath), Optional.empty(), context);
193    }
194    
195    public Map<String, Object> dataToJSON(DataContext context) throws BadItemTypeException
196    {
197        ModelViewItemGroup viewItemGroup = ModelViewItemGroup.of(_definition);
198        return dataToJSON(viewItemGroup, context);
199    }
200    
201    public Map<String, Object> dataToJSON(ViewItemAccessor viewItemAccessor, DataContext context) throws BadItemTypeException
202    {
203        return _dataToJSON(Optional.empty(), Optional.of(viewItemAccessor), context);
204    }
205    
206    @SuppressWarnings("unchecked")
207    private Map<String, Object> _dataToJSON(Optional<String> dataPath, Optional<ViewItemAccessor> viewItemAccessor, DataContext context) throws BadItemTypeException
208    {
209        List<Map<String, Object>> entriesValues = new ArrayList<>();
210        for (ModelAwareRepeaterEntry entry : getEntries())
211        {
212            DataContext entryContext = context.cloneContext();
213            if (StringUtils.isNotEmpty(context.getDataPath()))
214            {
215                entryContext.addSuffixToLastSegment("[" + entry.getPosition() + "]");
216            }
217            
218            Map<String, Object> entryValues = null;
219            if (dataPath.isPresent())
220            {
221                entryValues = (Map<String, Object>) entry.dataToJSON(dataPath.get(), entryContext);
222            }
223            else if (viewItemAccessor.isPresent())
224            {
225                entryValues = entry.dataToJSON(viewItemAccessor.get(), entryContext);
226            }
227            
228            entriesValues.add(entryValues);
229        }
230        
231        Map<String, Object> result = new HashMap<>();
232        result.put("entryCount", getSize());
233        result.put("entries", entriesValues);
234        return result;
235    }
236    
237    /**
238     * Generates SAX events for the comments of the data in the given view in the current {@link DataHolder}
239     * @param contentHandler the {@link ContentHandler} that will receive the SAX events
240     * @param viewItemAccessor the {@link ViewItemAccessor} referencing the items for which generate SAX events
241     * @throws SAXException if an error occurs during the SAX events generation
242     */
243    public void commentsToSAX(ContentHandler contentHandler, ViewItemAccessor viewItemAccessor) throws SAXException
244    {
245        for (ModelAwareRepeaterEntry entry : getEntries())
246        {
247            entry.commentsToSAX(contentHandler, viewItemAccessor);
248        }
249    }
250    
251    public void copyTo(ModifiableRepeater repeater) throws UndefinedItemPathException, BadItemTypeException, UnknownTypeException, NotUniqueTypeException
252    {
253        for (RepeaterEntry entry : getEntries())
254        {
255            ModifiableRepeaterEntry entryDestination = repeater.addEntry(entry.getPosition());
256            entry.copyTo(entryDestination);
257        }
258    }
259    
260    public List<SolrInputDocument> indexData(SolrInputDocument document, SolrInputDocument rootDocument, String solrFieldPrefix, IndexableDataContext context) throws BadItemTypeException
261    {
262        List<SolrInputDocument> additionalDocuments = new ArrayList<>();
263        String solrFieldName = solrFieldPrefix + context.getDataPathLastSegment();
264        
265        for (IndexableRepeaterEntry entry : getEntries())
266        {
267            // Update the context with entry position
268            IndexableDataContext newContext = context.cloneContext()
269                                            .addSuffixToLastSegment("[" + entry.getPosition() + "]");
270            
271            SolrInputDocument repeaterEntryDoc = new SolrInputDocument();
272            
273            if (!context.indexForFullTextField())
274            {
275                // Creates a new Solr document for each entry
276                String repeaterEntryDocId = document.getField("id").getFirstValue().toString() + "/" + solrFieldName + "/" + entry.getPosition();
277                repeaterEntryDoc.addField("id", repeaterEntryDocId);
278                repeaterEntryDoc.addField(SolrFieldNames.DOCUMENT_TYPE, SolrFieldNames.TYPE_REPEATER);
279                repeaterEntryDoc.addField(SolrFieldNames.REPEATER_ENTRY_POSITION, entry.getPosition());
280                
281                document.addField(solrFieldName + "_s_dv", repeaterEntryDocId);
282            }
283            
284            // Add the created document to additional documents
285            additionalDocuments.add(repeaterEntryDoc);
286            
287            ViewItemAccessor viewItemAccessor = context.getViewItem()
288                .map(ViewItemAccessor.class::cast)
289                .orElse(ViewHelper.createViewItemAccessor(entry.getModel()));
290            additionalDocuments.addAll(IndexableDataHolderHelper.indexData(entry, viewItemAccessor, repeaterEntryDoc, rootDocument, StringUtils.EMPTY, newContext));
291        }
292        
293        return additionalDocuments;
294    }
295    
296    public RepositoryData getRepositoryData()
297    {
298        return _repositoryData;
299    }
300    
301    public IndexableDataHolder getParentDataHolder()
302    {
303        return _parent;
304    }
305    
306    public IndexableDataHolder getRootDataHolder()
307    {
308        return _root;
309    }
310}