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