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;
017
018import java.text.Normalizer;
019import java.util.ArrayList;
020import java.util.Collection;
021import java.util.Collections;
022import java.util.HashMap;
023import java.util.List;
024import java.util.Map;
025import java.util.Set;
026
027import javax.jcr.RepositoryException;
028
029import org.apache.avalon.framework.component.Component;
030import org.apache.avalon.framework.logger.AbstractLogEnabled;
031import org.apache.avalon.framework.service.ServiceException;
032import org.apache.avalon.framework.service.ServiceManager;
033import org.apache.avalon.framework.service.Serviceable;
034import org.apache.commons.lang3.StringUtils;
035
036import org.ametys.cms.tag.jcr.AbstractJCRTagProvider;
037import org.ametys.cms.tag.jcr.AbstractJCRTagsDAO;
038import org.ametys.cms.tag.jcr.JCRTag;
039import org.ametys.core.ui.Callable;
040import org.ametys.core.util.I18nUtils;
041import org.ametys.runtime.i18n.I18nizableText;
042
043/**
044 * DAO for manipulating tags
045 */
046public abstract class AbstractTagsDAO extends AbstractLogEnabled implements Serviceable, Component
047{
048    /** The tag provider extension point */
049    protected AbstractTagProviderExtensionPoint<? extends Tag> _tagProviderExtPt;
050    /** The I18n utils */
051    protected I18nUtils _i18nUtils;
052    
053    @SuppressWarnings("unchecked")
054    @Override
055    public void service(ServiceManager manager) throws ServiceException
056    {
057        _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE);
058        _tagProviderExtPt = (AbstractTagProviderExtensionPoint< ? extends Tag>) manager.lookup(getTagProviderEPRole());
059    }
060    
061    /**
062     * Get the tag provider extension point role
063     * @return the tag provider extension point role
064     */
065    public abstract String getTagProviderEPRole();
066    
067    /**
068     * Get the paths of given tags
069     * @param tagNames The name of tags
070     * @param contextualParameters Contextual parameters
071     * @return {String} path
072     */
073    @Callable
074    public List<String> getTagPaths(List<String> tagNames, Map<String, Object> contextualParameters)
075    {
076        List<String> paths = new ArrayList<>();
077        if (tagNames != null)
078        {
079            for (String tagName : tagNames)
080            {
081                if (tagName.startsWith("provider_"))
082                {
083                    paths.add(tagName);
084                }
085                else
086                {
087                    String tagPath = getFullPath(tagName, contextualParameters);
088                    if (tagPath != null)
089                    {
090                        paths.add(tagPath);
091                    }
092                    
093                }
094            }
095        }
096        
097        return paths;
098    }
099
100    /**
101     * Get the full path of a tag, with its provider
102     * @param tagName The tag name
103     * @param contextualParameters Contextual parameters
104     * @return the full path of tag or null if not found
105     */
106    protected String getFullPath (String tagName, Map<String, Object> contextualParameters)
107    {
108        Set<String> tagProviders = _tagProviderExtPt.getExtensionsIds();
109        
110        for (String id : tagProviders)
111        {
112            TagProvider<? extends Tag> tagProvider = _tagProviderExtPt.getExtension(id);
113            if (tagProvider.hasTag(tagName, contextualParameters))
114            {
115                Tag tag = tagProvider.getTag(tagName, contextualParameters);
116                return "provider_" + tagProvider.getId() + getPath(tag);
117            }
118        }
119        
120        // Not found
121        return null;
122    }
123    
124    /**
125     * Get a tag by its name
126     * @param tagName The tag name
127     * @param contextualParameters Contextual parameters
128     * @return The tag or null if not found
129     */
130    public Tag getTag (String tagName, Map<String, Object> contextualParameters)
131    {
132        Set<String> tagProviders = _tagProviderExtPt.getExtensionsIds();
133        
134        for (String id : tagProviders)
135        {
136            TagProvider<? extends Tag> tagProvider = _tagProviderExtPt.getExtension(id);
137            if (tagProvider.hasTag(tagName, contextualParameters))
138            {
139                return tagProvider.getTag(tagName, contextualParameters);
140            }
141        }
142        return null;
143    }
144    
145    /**
146     * Get the path of a tag inside its provider.
147     * The path is composed of tags name and '/' as a separator
148     * @param tag The tag
149     * @return The path
150     */
151    protected String getPath (Tag tag)
152    {
153        return tag == null ? "" : getPath(tag.getParent()) + "/" + tag.getName();
154    }
155    
156    /**
157     * Get the title of given tags
158     * @param tagNames The name of tags
159     * @param contextualParameters Contextual parameters
160     * @return the labels
161     */
162    @Callable
163    public Map<String, Object> getTagsTitle(List<String> tagNames, Map<String, Object> contextualParameters)
164    {
165        Map<String, Object> result = new HashMap<>();
166        
167        Set<String> tagProviders = _tagProviderExtPt.getExtensionsIds();
168        
169        List<I18nizableText> labels = new ArrayList<>();
170        
171        for (String tagName : tagNames)
172        {
173            if (tagName.startsWith("provider_"))
174            {
175                TagProvider<? extends Tag> tagProvider = _tagProviderExtPt.getExtension(tagName.substring("provider_".length()));
176                labels.add(tagProvider.getLabel());
177            }
178            else
179            {
180                for (String id : tagProviders)
181                {
182                    TagProvider<? extends Tag> tagProvider = _tagProviderExtPt.getExtension(id);
183                    if (tagProvider.hasTag(tagName, contextualParameters))
184                    {
185                        Tag tag = tagProvider.getTag(tagName, contextualParameters);
186                        labels.add(tag.getTitle());
187                    }
188                }
189            }
190        }
191        
192        result.put("titles", labels);
193        return result;
194    }
195
196    /**
197     * Get the path of node which match filter regexp
198     * @param value the value to match
199     * @param contextualParameters Contextual parameters
200     * @return the matching paths
201     */
202    @Callable
203    public List<String> filterTagsByRegExp(String value, Map<String, Object> contextualParameters)
204    {
205        List<String> matchingPaths = new ArrayList<>();
206
207        String toMatch = Normalizer.normalize(value.toLowerCase(), Normalizer.Form.NFD).replaceAll("[\\p{InCombiningDiacriticalMarks}]", "").trim();
208        
209        Set<String> extensionsIds = _tagProviderExtPt.getExtensionsIds();
210        for (String id : extensionsIds)
211        {
212            TagProvider<? extends Tag> tagProvider = _tagProviderExtPt.getExtension(id);
213            for (Tag tag : tagProvider.getTags(contextualParameters).values())
214            {
215                _getMatchingTag(toMatch, tagProvider, tag, matchingPaths);
216            }
217        }
218        
219        return matchingPaths;
220    }
221
222    /**
223     * Get the path of node which match filter regexp
224     * @param filter the value to match
225     * @param tagNames the list of tag's name to search inside
226     * @param contextualParameters Contextual parameters
227     * @return the matching paths
228     */
229    @Callable
230    public List<String> filterTagsFromListByRegExp(String filter, List<String> tagNames, Map<String, Object> contextualParameters)
231    {
232        if (StringUtils.isEmpty(filter))
233        {
234            return getTagPaths(tagNames, contextualParameters);
235        }
236        
237        if (tagNames != null && tagNames.size() > 0)
238        {
239            List<String> matchingPaths = new ArrayList<>();
240            
241            String toMatch = Normalizer.normalize(filter.toLowerCase(), Normalizer.Form.NFD).replaceAll("[\\p{InCombiningDiacriticalMarks}]", "").trim();
242            
243            for (String tagName : tagNames)
244            {
245                Tag tag = getTag(tagName, contextualParameters);
246                if (tag != null)
247                {
248                    String title = _i18nUtils.translate(tag.getTitle());
249                    String normalizedName = Normalizer.normalize(title.toLowerCase(), Normalizer.Form.NFD).replaceAll("[\\p{InCombiningDiacriticalMarks}]", "");
250                    if (normalizedName.contains(toMatch))
251                    {
252                        matchingPaths.add(getFullPath(tagName, contextualParameters));
253                    }
254                }
255            }
256            
257            return matchingPaths;
258        }
259        else
260        {
261            return filterTagsByRegExp(filter, contextualParameters);
262        }
263    }
264
265    /**
266     * Get paths of tag which match filter regexp
267     * @param value the value to match
268     * @param tagProvider the tag provider
269     * @param tag the current tag
270     * @param matchingPaths the matching paths
271     */
272    private void _getMatchingTag(String value, TagProvider<? extends Tag> tagProvider, Tag tag, List<String> matchingPaths)
273    {
274        String title = _i18nUtils.translate(tag.getTitle());
275        
276        if (title != null)
277        {
278            String normalizedName = Normalizer.normalize(title.toLowerCase(), Normalizer.Form.NFD).replaceAll("[\\p{InCombiningDiacriticalMarks}]", "");
279            
280            if (normalizedName.contains(value))
281            {
282                matchingPaths.add("provider_" + tagProvider.getId() + getPath(tag));
283            }
284        }
285        
286        for (Tag child : tag.getTags().values())
287        {
288            _getMatchingTag(value, tagProvider, child, matchingPaths);
289        }
290    }
291    
292    /**
293     * Test if tags exists
294     * @param tagNames The tag names to test
295     * @param onlyCustomTags If true, return only custom tags
296     * @param otherParameters the other parameters
297     * @param contextualParameters Contextual parameters
298     * @return The list of tag names without the invalid values. 
299     */
300    @Callable
301    public List<String> checkTags(List<String> tagNames, boolean onlyCustomTags, Map<String, Object> otherParameters, Map<String, Object> contextualParameters)
302    {
303        List<String> existingTagNames = new ArrayList<>();
304        List<TagProvider<? extends Tag>> tagProviders = new ArrayList<>();
305        
306        if (onlyCustomTags)
307        {
308            tagProviders.addAll(getCustomTagProvider());
309        }
310        else
311        {
312            Set<String> extensionsIds = _tagProviderExtPt.getExtensionsIds();
313            for (String id : extensionsIds)
314            {
315                tagProviders.add(_tagProviderExtPt.getExtension(id));
316            }
317        }
318        
319        for (String tagName : tagNames)
320        {
321            if (tagName.startsWith("provider_"))
322            {
323                String providerId = tagName.substring("provider_".length());
324                TagProvider<? extends Tag> tagProvider = _tagProviderExtPt.getExtension(providerId);
325                
326                if (tagProvider != null)
327                {
328                    existingTagNames.add(tagName);
329                }
330            }
331            else
332            {
333                for (TagProvider<? extends Tag> tagProvider : tagProviders)
334                {
335                    if (tagProvider.hasTag(tagName, contextualParameters))
336                    {
337                        String filteredTagName = _getFilteredTagName(tagProvider, tagName, otherParameters, contextualParameters);
338                        if (StringUtils.isNotBlank(filteredTagName))
339                        {
340                            existingTagNames.add(filteredTagName);
341                            break;
342                        }
343                    }
344                }
345            }
346        }
347        
348        return existingTagNames;
349    }
350    
351    /**
352     * Get filtered tag name
353     * @param tagProvider the tag provider
354     * @param tagName the tag name
355     * @param otherParameters the other parameters
356     * @param contextualParameters the contextual parameters
357     * @return the tag name if it match
358     */
359    protected String _getFilteredTagName(TagProvider<? extends Tag> tagProvider, String tagName, Map<String, Object> otherParameters, Map<String, Object> contextualParameters)
360    {
361        return tagName;
362    }
363    
364    /**
365     * Get the list of custom tag provider
366     * @return the list of custom tag provider
367     */
368    protected abstract List<TagProvider<? extends Tag>> getCustomTagProvider();
369    
370    /**
371     * Add tags from FO
372     * @param tagNames The name of tags to add
373     * @return The new tags descriptions
374     */
375    public List<Map<String, Object>> addTags(String[] tagNames)
376    {
377        try
378        {
379            List<Map<String, Object>> tags = new ArrayList<>();
380    
381            for (String tagName : tagNames)
382            {
383                String filteredName = Normalizer.normalize(tagName.toLowerCase(), Normalizer.Form.NFD).replaceAll("[\\p{InCombiningDiacriticalMarks}]", "").trim(); 
384                filteredName = filteredName.replaceAll("œ", "oe").replaceAll("æ", "ae").replaceAll("[^a-z0-9]", "_").replaceAll("_+", "_");
385    
386                JCRTag tag = _getTagJCRDAO().addTag(null, filteredName.toUpperCase(), tagName, "", Collections.emptyMap(), Collections.emptyMap());
387                tags.add(_tagToJSON(tag));
388            }
389            
390            // Clear the request cache after tag added
391            for (String id : _tagProviderExtPt.getExtensionsIds())
392            {
393                TagProvider tagProvider = _tagProviderExtPt.getExtension(id);
394                if (tagProvider instanceof AbstractJCRTagProvider)
395                {
396                    ((AbstractJCRTagProvider) tagProvider).clearCache();
397                }
398            }
399            
400            return tags;
401        }
402        catch (RepositoryException e)
403        {
404            throw new IllegalStateException("Cannot create tags", e);
405        }
406    }
407    
408    /**
409     * Get the tag JCR DAO
410     * @return the tag JCR DAO
411     */
412    protected abstract AbstractJCRTagsDAO _getTagJCRDAO();
413    
414    /**
415     * Get tags to json
416     * @param tags the tags
417     * @return the tags to json
418     */
419    protected List<Map<String, Object>> _tagsToJSON(Collection< ? extends Tag> tags)
420    {
421        List<Map<String, Object>> result = new ArrayList<>();
422
423        for (Tag tag : tags)
424        {
425            result.add(tagToJSON(tag));
426            
427            Map<String, ? extends Tag> subTags = tag.getTags();
428            if (subTags != null)
429            {
430                result.addAll(_tagsToJSON(subTags.values()));
431            }
432        }
433        
434        return result;
435    }
436    
437    /**
438     * JCR tag to json
439     * @param tag the jcr tag
440     * @return the jcr tag to json
441     */
442    protected Map<String, Object> _tagToJSON(JCRTag tag)
443    {
444        Map<String, Object> tagInfo = new HashMap<>();
445        tagInfo.put("id", tag.getId());
446        tagInfo.put("name", tag.getName());
447        tagInfo.put("text", tag.getTitle());
448        return tagInfo;
449    }
450    
451    /**
452     * Tag to json
453     * @param tag the tag
454     * @return the tag to json
455     */
456    public Map<String, Object> tagToJSON(Tag tag)
457    {
458        Map<String, Object> tagInfo = new HashMap<>();
459        tagInfo.put("id", tag.getId());
460        tagInfo.put("name", tag.getName());
461        tagInfo.put("text", tag.getTitle());
462        return tagInfo;
463    }
464}