001/*
002 *  Copyright 2014 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.tag.jcr;
017
018import java.io.File;
019import java.io.FileInputStream;
020import java.io.IOException;
021import java.io.InputStream;
022import java.util.Collections;
023import java.util.HashMap;
024import java.util.HashSet;
025import java.util.Map;
026import java.util.Set;
027
028import javax.xml.parsers.SAXParserFactory;
029
030import org.apache.avalon.framework.configuration.Configuration;
031import org.apache.avalon.framework.configuration.ConfigurationException;
032import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder;
033import org.apache.avalon.framework.parameters.Parameters;
034import org.apache.avalon.framework.service.ServiceException;
035import org.apache.avalon.framework.service.ServiceManager;
036import org.apache.cocoon.environment.ObjectModelHelper;
037import org.apache.cocoon.environment.Redirector;
038import org.apache.cocoon.environment.Request;
039import org.apache.cocoon.environment.SourceResolver;
040import org.apache.cocoon.servlet.multipart.Part;
041import org.apache.cocoon.servlet.multipart.PartOnDisk;
042import org.apache.cocoon.servlet.multipart.RejectedPart;
043import org.apache.commons.io.FilenameUtils;
044import org.apache.commons.lang3.StringUtils;
045import org.xml.sax.XMLReader;
046
047import org.ametys.cms.ObservationConstants;
048import org.ametys.cms.tag.Tag;
049import org.ametys.cms.tag.Tag.TagVisibility;
050import org.ametys.cms.tag.TagProvider;
051import org.ametys.cms.tag.TagProviderExtensionPoint;
052import org.ametys.cms.tag.TagTargetType;
053import org.ametys.cms.tag.TagTargetTypeExtensionPoint;
054import org.ametys.core.cocoon.JSonReader;
055import org.ametys.core.observation.AbstractNotifierAction;
056import org.ametys.core.observation.Event;
057import org.ametys.core.util.JSONUtils;
058import org.ametys.plugins.repository.AmetysObjectResolver;
059import org.ametys.plugins.repository.TraversableAmetysObject;
060import org.ametys.plugins.repository.jcr.DefaultTraversableAmetysObject;
061import org.ametys.runtime.i18n.I18nizableText;
062
063/**
064 * Import subscribers from a CSV or text file.
065 */
066public class ImportTagsAction extends AbstractNotifierAction
067{
068    private static final String[] _ALLOWED_EXTENSIONS = new String[] {"xml"};
069    
070    /** The tag provider extension point */
071    protected TagProviderExtensionPoint _tagProviderExtPt;
072    /** The Ametys object resolver */
073    protected AmetysObjectResolver _resolver;
074    /** Target types */
075    protected TagTargetTypeExtensionPoint _targetTypeEP;
076    /** The JSon Utils Component */
077    protected JSONUtils _jsonUtils;
078    
079    /** The count of new tags **/
080    private int _createdTagsCount;
081    /** The count of updated tags **/
082    private int _updatedTagsCount;
083    /** The count of error **/
084    private int _errorCount;
085
086    @Override
087    public void service(ServiceManager smanager) throws ServiceException
088    {
089        super.service(smanager);
090        _tagProviderExtPt = (TagProviderExtensionPoint) smanager.lookup(TagProviderExtensionPoint.ROLE);
091        _resolver = (AmetysObjectResolver) smanager.lookup(AmetysObjectResolver.ROLE);
092        _targetTypeEP = (TagTargetTypeExtensionPoint) smanager.lookup(TagTargetTypeExtensionPoint.ROLE);
093        _jsonUtils = (JSONUtils) smanager.lookup(JSONUtils.ROLE);
094    }
095    
096    @Override
097    public Map act(Redirector redirector, SourceResolver resolver, Map objectModel, String source, Parameters parameters) throws Exception
098    {
099        Map<String, Object> result = new HashMap<>();
100
101        Request request = ObjectModelHelper.getRequest(objectModel);
102
103        String parentId = request.getParameter("tagId");
104        String forceProvider = request.getParameter("tagProvider");
105        if (StringUtils.isEmpty(forceProvider))
106        {
107            forceProvider = JCRTagProvider.class.getName();
108        }
109        
110        Part part = (Part) request.get("importFile");
111        if (part instanceof RejectedPart)
112        {
113            return Collections.singletonMap("error", "rejected-file");
114        }
115        
116        PartOnDisk uploadedFilePart = (PartOnDisk) part;
117        File uploadedFile = (uploadedFilePart != null) ? uploadedFilePart.getFile() : null;
118        String filename = (uploadedFilePart != null) ? uploadedFilePart.getFileName().toLowerCase() : null;
119        
120        if (!FilenameUtils.isExtension(filename, _ALLOWED_EXTENSIONS))
121        {
122            result.put("success", false);
123            result.put("error", "invalid-extension");
124        }
125        else if (uploadedFile == null)
126        {
127            result.put("success", false);
128            result.put("error", "no-file");
129        }
130        else
131        {
132            String contextualParametersAsStr = request.getParameter("contextualParameters");
133            Map<String, Object> contextualParameters = _jsonUtils.convertJsonToMap(contextualParametersAsStr);
134            
135            Configuration configuration = initializeTags(uploadedFile.getPath());
136            Map<String, Tag> tags = configureTags(configuration, null, "plugin.cms");
137            
138            _createdTagsCount = 0;
139            _updatedTagsCount = 0;
140            _errorCount = 0;
141            
142            DefaultTraversableAmetysObject<?> parent = _resolver.resolveById(parentId);
143            createOrUpdateTags(parent, tags, contextualParameters);
144            
145            JCRTagProvider provider = (JCRTagProvider) _tagProviderExtPt.getExtension(forceProvider);
146            TraversableAmetysObject rootNode = provider.getRootNode(contextualParameters);
147
148            result.put("success", true);
149            result.put("rootId", rootNode.getId());
150            result.put("createdCount", _createdTagsCount);
151            result.put("updatedCount", _updatedTagsCount);
152            result.put("errorCount", _errorCount);
153        }
154        
155        request.setAttribute(JSonReader.OBJECT_TO_READ, result);
156        return EMPTY_MAP;
157    }
158    
159    /**
160     * Save or update a tag
161     * @param parent the JCR parent tag or provider
162     * @param tags the list of tags to add or update
163     * @param contextualParameters Contextual parameters transmitted by the environment.
164     */
165    protected void createOrUpdateTags(DefaultTraversableAmetysObject<?> parent, Map<String, Tag> tags, Map<String, Object> contextualParameters)
166    {
167        boolean update = false;
168        for (Tag tag : tags.values())
169        {
170            JCRTag jcrTag = null;
171            
172            try
173            {
174                if (parent.hasChild(tag.getName()))
175                {
176                    // Update existing tag
177                    jcrTag = parent.getChild(tag.getName());
178                    update = true;
179                }
180                else
181                {
182                    // Create new tag
183                    String name = _findUniqueName (tag.getId(), contextualParameters);
184                    jcrTag = parent.createChild(name, "ametys:tag");
185                }
186                
187                jcrTag.setTitle(tag.getTitle().getLabel());
188                jcrTag.setDescription(tag.getDescription().getLabel());
189                jcrTag.setVisibility(tag.getVisibility());
190                jcrTag.setTargetType(tag.getTarget());
191                
192                // recursive call to save/update
193                createOrUpdateTags(jcrTag, tag.getTags(), contextualParameters);
194                
195                jcrTag.saveChanges();
196                
197                Map<String, Object> eventParams = new HashMap<>();
198                eventParams.put(ObservationConstants.ARGS_TAG_ID, jcrTag.getId());
199                eventParams.put(ObservationConstants.ARGS_TAG_NAME, jcrTag.getName());
200                
201                if (update)
202                {
203                    _observationManager.notify(new Event(ObservationConstants.EVENT_TAG_UPDATED, _getCurrentUser(), eventParams));
204                    _updatedTagsCount++;
205                }
206                else
207                {
208                    // Notify observers that the tag has been added.
209                    _observationManager.notify(new Event(ObservationConstants.EVENT_TAG_ADDED, _getCurrentUser(), eventParams));
210                    _createdTagsCount++;
211                }
212            }
213            catch  (Exception e)
214            {
215                getLogger().error("Unable to add tag " + tag.getName() + " to JCR tag category of id '" + parent.getId() + "'", e);
216                _errorCount++;
217            }
218        }
219    }
220    
221    /**
222     * Initialize a configuration tags from the tags file.
223     * @param path the path of the file to initialize tags.
224     * @return The configuration
225     * @throws Exception if an error occurs.
226     */
227    protected Configuration initializeTags(String path) throws Exception
228    {
229        try (InputStream is = new FileInputStream(path))
230        {
231            SAXParserFactory factory = SAXParserFactory.newInstance();
232            
233            XMLReader reader = factory.newSAXParser().getXMLReader();
234
235            DefaultConfigurationBuilder confBuilder = new DefaultConfigurationBuilder(reader);
236
237            return confBuilder.build(is);
238        }
239        catch (IOException e)
240        {
241            return null;
242        }
243    }
244    
245    /**
246     * Configure tag from the passed configuration
247     * @param configuration The configuration
248     * @param parent The parent tag if any
249     * @param defaultCatalogue The default catalogue for i18n
250     * @return a Set of {@link Tag}
251     * @throws ConfigurationException If an error occurred
252     */
253    protected Map<String, Tag> configureTags (Configuration configuration, Tag parent, String defaultCatalogue)  throws ConfigurationException
254    {
255        Map<String, Tag> tags = new HashMap<>();
256        
257        Configuration[] tagsConfiguration = configuration.getChildren("tag");
258        for (Configuration tagConfiguration : tagsConfiguration)
259        {
260            String id = tagConfiguration.getAttribute("id");
261            if (!Tag.NAME_PATTERN.matcher(id).matches())
262            {
263                throw new ConfigurationException("Invalid tag ID '" + id + "': it must match the pattern " + Tag.NAME_PATTERN.toString(), configuration);
264            }
265            
266            TagVisibility visibility = TagVisibility.PUBLIC;
267            if (tagConfiguration.getAttribute("private", "").equals("true"))
268            {
269                visibility = TagVisibility.PRIVATE;
270            }
271            
272            String typeName = tagConfiguration.getAttribute("target", "CONTENT");
273            TagTargetType targetType = _targetTypeEP.getTagTargetType(typeName);
274            
275            I18nizableText label = configureLabel (tagConfiguration, defaultCatalogue);
276            I18nizableText description = configureDescription (tagConfiguration, defaultCatalogue);
277            Tag tag = new Tag(id, id, parent, label, description, visibility, targetType);
278            tags.put(id, tag);
279            
280            // Recursive configuration
281            Map<String, Tag> childTags = configureTags(tagConfiguration, tag, defaultCatalogue);
282            tag.setTags(childTags);
283        }
284        
285        return tags;
286    }
287    
288    /**
289     * Configure label from the passed configuration
290     * @param configuration The configuration
291     * @param defaultCatalogue The default catalogue
292     * @return The label
293     * @throws ConfigurationException If an error occurred
294     */
295    protected I18nizableText configureLabel (Configuration configuration, String defaultCatalogue) throws ConfigurationException
296    {
297        Configuration labelConfiguration = configuration.getChild("label");
298        
299        if (labelConfiguration.getAttributeAsBoolean("i18n", false))
300        {
301            return new I18nizableText(defaultCatalogue, labelConfiguration.getValue(""));
302        }
303        else
304        {
305            return new I18nizableText(labelConfiguration.getValue(""));
306        }
307    }
308    
309    /**
310     * Configure description from the passed configuration
311     * @param configuration The configuration
312     * @param defaultCatalogue The default catalogue
313     * @return The description
314     * @throws ConfigurationException If an error occurred
315     */
316    protected I18nizableText configureDescription (Configuration configuration, String defaultCatalogue) throws ConfigurationException
317    {
318        Configuration descConfiguration = configuration.getChild("description");
319        
320        if (descConfiguration.getAttributeAsBoolean("i18n", false))
321        {
322            return new I18nizableText(defaultCatalogue, descConfiguration.getValue(""));
323        }
324        else
325        {
326            return new I18nizableText(descConfiguration.getValue(""));
327        }
328    }
329
330    private String _findUniqueName(String originalName, Map<String, Object> contextualParameters)
331    {
332        Set<TagProvider> providers = _getTagProviders();
333        
334        // Find an unique name
335        int index = 2;
336        String name = originalName;
337        while (_hasTag(providers, name, contextualParameters))
338        {
339            name = originalName + "_" + (index++);
340        }
341        
342        return name;
343    }
344    
345    private boolean _hasTag(Set<TagProvider> providers, String name, Map<String, Object> contextualParameters)
346    {
347        for (TagProvider provider : providers)
348        {
349            if (provider.hasTag(name, contextualParameters))
350            {
351                return true;
352            }
353        }
354        return false;
355    }
356    
357    private Set<TagProvider> _getTagProviders ()
358    {
359        Set<TagProvider> providers = new HashSet<>();
360        
361        Set<String> ids = _tagProviderExtPt.getExtensionsIds();
362        for (String id : ids)
363        {
364            TagProvider provider = _tagProviderExtPt.getExtension(id);
365            providers.add(provider);
366        }
367        
368        return providers;
369    }
370}