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.plugins.thesaurus;
017
018import java.io.IOException;
019import java.io.InputStream;
020import java.nio.charset.StandardCharsets;
021import java.text.Normalizer;
022import java.util.ArrayList;
023import java.util.Collection;
024import java.util.HashMap;
025import java.util.LinkedHashSet;
026import java.util.List;
027import java.util.Map;
028import java.util.Set;
029import java.util.regex.Matcher;
030import java.util.regex.Pattern;
031
032import javax.jcr.Node;
033import javax.jcr.RepositoryException;
034import javax.jcr.Session;
035
036import org.apache.avalon.framework.component.Component;
037import org.apache.avalon.framework.logger.AbstractLogEnabled;
038import org.apache.avalon.framework.service.ServiceException;
039import org.apache.avalon.framework.service.ServiceManager;
040import org.apache.avalon.framework.service.Serviceable;
041import org.apache.commons.io.IOUtils;
042import org.apache.commons.lang3.ArrayUtils;
043import org.apache.commons.lang3.StringUtils;
044import org.apache.excalibur.source.Source;
045import org.apache.excalibur.source.SourceResolver;
046
047import org.ametys.cms.FilterNameHelper;
048import org.ametys.cms.repository.Content;
049import org.ametys.cms.repository.ModifiableContent;
050import org.ametys.cms.repository.WorkflowAwareContent;
051import org.ametys.cms.workflow.AbstractContentWorkflowComponent;
052import org.ametys.cms.workflow.ContentWorkflowHelper;
053import org.ametys.core.right.RightManager;
054import org.ametys.core.right.RightManager.RightResult;
055import org.ametys.core.ui.Callable;
056import org.ametys.core.user.CurrentUserProvider;
057import org.ametys.plugins.repository.AmetysObject;
058import org.ametys.plugins.repository.AmetysObjectIterable;
059import org.ametys.plugins.repository.AmetysObjectIterator;
060import org.ametys.plugins.repository.AmetysObjectResolver;
061import org.ametys.plugins.repository.AmetysRepositoryException;
062import org.ametys.plugins.repository.CollectionIterable;
063import org.ametys.plugins.repository.EmptyIterable;
064import org.ametys.plugins.repository.ModifiableTraversableAmetysObject;
065import org.ametys.plugins.repository.RepositoryConstants;
066import org.ametys.plugins.repository.TraversableAmetysObject;
067import org.ametys.plugins.repository.UnknownAmetysObjectException;
068import org.ametys.plugins.repository.jcr.JCRAmetysObject;
069import org.ametys.plugins.repository.lock.LockableAmetysObject;
070import org.ametys.plugins.repository.metadata.RichText;
071import org.ametys.plugins.thesaurus.content.ThesaurusItemContentType;
072import org.ametys.plugins.thesaurus.workflow.CreateTermFunction;
073
074import com.opensymphony.workflow.WorkflowException;
075
076/**
077 * DAO for manipulating thesaurus
078 *
079 */
080public class ThesaurusDAO extends AbstractLogEnabled implements Serviceable, Component
081{
082    /** The Avalon role */
083    public static final String ROLE = ThesaurusDAO.class.getName();
084    
085    /** Metadata for related terms */
086    public static final String METADATA_RELATED_TERMS = "relatedTerms";
087    /** Metadata for specific terms */
088    public static final String METADATA_SPECIFIC_TERMS = "specificTerms";
089    /** Metadata for synonyms */
090    public static final String METADATA_SYNONYMS = "synonyms";
091    /** Metadata for applicationNote */
092    public static final String METADATA_APPLICATION_NOTE = "applicationNote";
093    /** Metadata for explanatoryNote */
094    public static final String METADATA_EXPLANATORY_NOTE = "explanatoryNote";
095    /** Name of root candidate node */
096    public static final String ROOT_CANDIDATES_NODENAME = RepositoryConstants.NAMESPACE_PREFIX + ":candidates";
097    
098    private static final String __PLUGIN_NODE_NAME = "thesaurus";
099    private static final String __ROOT_THESAURII_NODETYPE = RepositoryConstants.NAMESPACE_PREFIX + ":thesaurii";
100    private static final String __TERM_WORKFLOW_NAME = "thesaurus";
101    
102    private static final Pattern __NUMERIC_PATTERN = Pattern.compile("^[0-9-_]+.*");
103    
104    /** Ametys resolver */
105    protected AmetysObjectResolver _resolver;
106    /** Content workflow helper */
107    protected ContentWorkflowHelper _contentWorkflowHelper;
108    /** Avalon source resolver */
109    protected SourceResolver _srcResolver;
110    /** The right manager */
111    protected RightManager _rightManager;
112    /** The current user provider */
113    protected CurrentUserProvider _currentUserProvider;
114    
115    @Override
116    public void service(ServiceManager manager) throws ServiceException
117    {
118        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
119        _contentWorkflowHelper = (ContentWorkflowHelper) manager.lookup(ContentWorkflowHelper.ROLE);
120        _srcResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE);
121        _rightManager = (RightManager) manager.lookup(RightManager.ROLE);
122        _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE);
123    }
124    
125    /**
126     * Get the list of thesaurus
127     * @return the thesaurus
128     */
129    public AmetysObjectIterable<Thesaurus> getThesaurii ()
130    {
131        ModifiableTraversableAmetysObject rootNode = getRootNode();
132        return rootNode.getChildren();
133    }
134    
135    /**
136     * Get microthesaurii of a thesaurus
137     * @param thesaurusId The id of thesaurus
138     * @return the microthesaurii
139     */
140    public AmetysObjectIterable<MicroThesaurus> getMicrothesaurii (String thesaurusId)
141    {
142        try
143        {
144            Thesaurus thesaurus = _resolver.resolveById(thesaurusId);
145            return thesaurus.getChildren();
146        }
147        catch (UnknownAmetysObjectException e)
148        {
149            getLogger().error("Unable to retrieve thesaurus of id '" + thesaurusId);
150            return new EmptyIterable<>();
151        }
152    }
153    
154    /**
155     * Get the parent microthesaurus
156     * @param term The term
157     * @return the parent microthesaurus
158     */
159    public MicroThesaurus getParentMicrothesaurus (Content term)
160    {
161        AmetysObject parent = term.getParent();
162        while (parent != null)
163        {
164            if (parent instanceof MicroThesaurus)
165            {
166                return (MicroThesaurus) parent;
167            }
168            parent = parent.getParent();
169        }
170        return null;
171    }
172    
173    /**
174     * Get a thesaurus by its unique name
175     * @param name The thesaurus' name
176     * @return the thesaurus or <code>null</code> if not found
177     */
178    public Thesaurus getThesaurusByName (String name)
179    {
180        String xPathQuery = "//element(" + name + ", ametys:thesaurus)";
181        
182        AmetysObjectIterable<Thesaurus> contents = _resolver.query(xPathQuery);
183        AmetysObjectIterator<Thesaurus> it = contents.iterator();
184        
185        if (it.hasNext())
186        {
187            return it.next();
188        }
189        
190        return null;
191    }
192    
193    /**
194     * Get a microthesaurus by its unique name
195     * @param thesaurusName The thesaurusName' name
196     * @param microthesaurusName The microthesaurus' name
197     * @return the microthesaurus or <code>null</code> if not found
198     */
199    public MicroThesaurus getMicrothesaurusByName (String thesaurusName, String microthesaurusName)
200    {
201        String xPathQuery = "//element(" + thesaurusName + ", ametys:thesaurus)/element(" + microthesaurusName + ", ametys:microthesaurus)";
202        
203        AmetysObjectIterable<MicroThesaurus> contents = _resolver.query(xPathQuery);
204        AmetysObjectIterator<MicroThesaurus> it = contents.iterator();
205        
206        if (it.hasNext())
207        {
208            return it.next();
209        }
210        
211        return null;
212    }
213    
214    /**
215     * Get the id of a microthesaurus by its unique name
216     * @param thesaurusName The thesaurusName' name
217     * @param microthesaurusName The microthesaurus' name
218     * @return the id of microthesaurus if found
219     */
220    @Callable
221    public String getMicrothesaurusIdByName (String thesaurusName, String microthesaurusName)
222    {
223        MicroThesaurus microthesaurus = getMicrothesaurusByName(thesaurusName, microthesaurusName);
224        return microthesaurus != null ? microthesaurus.getId() : null;
225    }
226    
227    /**
228     * Get the id of parent microthesaurus 
229     * @param termId The id of term
230     * @return the id of parent microthesaurus
231     */
232    @Callable
233    public Map<String, Object> getParentMicrothesaurus (String termId)
234    {
235        Content term = _resolver.resolveById(termId);
236        
237        MicroThesaurus parentMicrothesaurus = getParentMicrothesaurus(term);
238        
239        Map<String, Object> result = new HashMap<>();
240        result.put("microthesaurusId", parentMicrothesaurus.getId());
241        return result;
242    }
243    
244    /**
245     * Get the parent thesaurus
246     * @param microThesaurus The microthesaurus
247     * @return the parent thesaurus
248     */
249    public Thesaurus getParentThesaurus (MicroThesaurus microThesaurus)
250    {
251        return microThesaurus.getParent();
252    }
253    
254    /**
255     * Get the generic term or null
256     * @param term The term
257     * @return the parent term or null
258     */
259    public Content getGenericTerm (Content term)
260    {
261        AmetysObject parent = term.getParent();
262        while (!(parent instanceof MicroThesaurus))
263        {
264            if (parent instanceof Content)
265            {
266                return (Content) parent;
267            }
268            parent = parent.getParent();
269        }
270        return null;
271    }
272    
273    /**
274     * Get direct child terms of a microthesaurus or a term
275     * @param parentId The parent Id
276     * @return The child terms
277     */
278    public AmetysObjectIterable<Content> getChildTerms(String parentId)
279    {
280        TraversableAmetysObject parent = _resolver.resolveById(parentId);
281        return getChildTerms(parent);
282    }
283    
284    /**
285     * Get child terms of a microthesaurus or a term
286     * @param parent The parent
287     * @return The child terms
288     */
289    public AmetysObjectIterable<Content> getChildTerms(TraversableAmetysObject parent)
290    {
291        if (parent instanceof MicroThesaurus)
292        {
293            List<Content> contents = new ArrayList<>();
294            for (AmetysObject child : parent.getChildren())
295            {
296                if (child instanceof Content)
297                {
298                    contents.add((Content) child);
299                }
300            }
301            return new CollectionIterable<>(contents);
302        }
303        else if (parent instanceof Content)
304        {
305            if (((Content) parent).getMetadataHolder().hasMetadata(METADATA_SPECIFIC_TERMS))
306            {
307                TraversableAmetysObject specificTerms = ((Content) parent).getMetadataHolder().getObjectCollection(METADATA_SPECIFIC_TERMS);
308                return specificTerms.getChildren();
309            }
310            
311            return new EmptyIterable<>();
312        }
313        else if (parent.getName().equals(ROOT_CANDIDATES_NODENAME))
314        {
315            return parent.getChildren();
316        }
317
318        throw new IllegalArgumentException("Object of id '" + parent.getId() + "' can not have child terms");
319    }
320    
321    /**
322     * Get the contents indexed with this term
323     * @param termId The term id
324     * @return The indexed contents
325     */
326    public Set<Content> getIndexedContents (String termId)
327    {
328        Set<Content> indexedContents = new LinkedHashSet<>();
329        
330        Content term = _resolver.resolveById(termId);
331        
332        Collection<Content> referencingContents = term.getReferencingContents();
333        for (Content content : referencingContents)
334        {
335            String cType = content.getTypes()[0];
336            if (!ThesaurusItemContentType.TERM_CONTENT_TYPE_ID.equals(cType))
337            {
338                indexedContents.add(content);
339            }
340        }
341        
342        return indexedContents;
343    }
344    
345    /**
346     * Get the number of contents indexed with this term
347     * @param termId The term id
348     * @return The number of indexed contents
349     */
350    public int getNumberOfIndexedContents (String termId)
351    {
352        Collection<Content> indexedContents = getIndexedContents(termId);
353        return indexedContents.size();
354    }
355    
356    /**
357     * Get the JSON representation of terms
358     * @param termIds The id of a terms
359     * @return A JSON map with informations properties
360     * @throws IOException when an error occurs while getting the term information
361     */
362    @SuppressWarnings("unchecked")
363    @Callable
364    public Map<String, Object> getTermsInformation (List<String> termIds) throws IOException
365    {
366        Map<String, Object> terms = new HashMap<>();
367        terms.put("terms", new ArrayList<>());
368        
369        for (String termId : termIds)
370        {
371            ((List<Object>) terms.get("terms")).add(getTermInformation(termId));
372        }
373        return terms;
374    }
375    
376    /**
377     * Get the JSON representation of term
378     * @param termId The id of a term
379     * @return A JSON map with informations properties
380     * @throws IOException if an error occurs when creating a {@link Source}
381     */
382    @Callable
383    public Map<String, Object> getTermInformation (String termId) throws IOException
384    {
385        Map<String, Object> infos = new HashMap<>();
386        Content term = _resolver.resolveById(termId);
387        
388        infos.put("title", term.getTitle());
389        infos.put("thesaurus", this.getParentThesaurus(this.getParentMicrothesaurus(term)).getLabel());
390        infos.put("microthesaurus", this.getParentMicrothesaurus(term).getLabel());
391        
392        String cType = term.getTypes()[0];
393        if (ThesaurusItemContentType.TERM_CONTENT_TYPE_ID.equals(cType))
394        {
395            infos.put("type", "term");
396            
397            if (term.getMetadataHolder().hasMetadata(METADATA_SPECIFIC_TERMS))
398            {
399                List<Map<String, Object>> specificTerms = new ArrayList<>();
400                
401                TraversableAmetysObject ts = term.getMetadataHolder().getObjectCollection(METADATA_SPECIFIC_TERMS);
402                for (AmetysObject child : ts.getChildren())
403                {
404                    if (child instanceof Content)
405                    {
406                        Map<String, Object> specificTerm = new HashMap<>();
407                        specificTerm.put("title", ((Content) child).getTitle());
408                        specificTerm.put("path", this.getTermPath((Content) child));
409                        specificTerms.add(specificTerm);
410                    } 
411                }
412                infos.put("specificTerms", specificTerms);
413            }
414            
415            if (term.getMetadataHolder().hasMetadata(METADATA_RELATED_TERMS))
416            {
417                List<Map<String, Object>> relatedTerms = new ArrayList<>();
418                
419                String[] ids = term.getMetadataHolder().getStringArray(ThesaurusDAO.METADATA_RELATED_TERMS);
420                for (String id : ids)
421                {
422                    Content ta = _resolver.resolveById(id);
423                    Map<String, Object> relatedTerm = new HashMap<>();
424                    relatedTerm.put("title", ta.getTitle());
425                    relatedTerm.put("path", this.getTermPath(ta));
426                    relatedTerms.add(relatedTerm);
427                }
428                infos.put("relatedTerms", relatedTerms);
429            }
430            
431            if (term.getMetadataHolder().hasMetadata(ThesaurusDAO.METADATA_SYNONYMS))
432            {
433                String[] synonyms = term.getMetadataHolder().getStringArray(ThesaurusDAO.METADATA_SYNONYMS); 
434                infos.put("synonyms", StringUtils.join(synonyms, ", "));
435            }
436            
437            if (term.getMetadataHolder().hasMetadata(ThesaurusDAO.METADATA_EXPLANATORY_NOTE))
438            {
439                RichText explanatoryNote = term.getMetadataHolder().getRichText(ThesaurusDAO.METADATA_EXPLANATORY_NOTE); 
440                
441                InputStream docbookIS = null;
442                try
443                {
444                    docbookIS = explanatoryNote.getInputStream();
445                    HashMap<String, Object> params = new HashMap<>();
446                    params.put("source", docbookIS);
447                    
448                    Source source = _srcResolver.resolveURI("cocoon://_plugins/thesaurus/convert/docbook2html", null, params);
449                    
450                    try (InputStream htmlIS = source.getInputStream())
451                    {
452                        infos.put("explanatoryNote", IOUtils.toString(htmlIS, StandardCharsets.UTF_8));
453                    }
454                }
455                catch (IOException ex)
456                {
457                    throw new RuntimeException(ex);
458                }
459            }
460            
461            if (term.getMetadataHolder().hasMetadata(ThesaurusDAO.METADATA_APPLICATION_NOTE))
462            {
463                RichText applicationNote = term.getMetadataHolder().getRichText(ThesaurusDAO.METADATA_APPLICATION_NOTE); 
464                
465                InputStream docbookIS = null;
466                try
467                {
468                    docbookIS = applicationNote.getInputStream();
469                    HashMap<String, Object> params = new HashMap<>();
470                    params.put("source", docbookIS);
471                    
472                    Source source = _srcResolver.resolveURI("cocoon://_plugins/thesaurus/convert/docbook2html", null, params);
473                    
474                    try (InputStream htmlIS = source.getInputStream())
475                    {
476                        infos.put("applicationNote", IOUtils.toString(htmlIS, StandardCharsets.UTF_8));
477                    }
478                }
479                catch (IOException ex)
480                {
481                    throw new RuntimeException(ex);
482                }
483            }
484        }
485        else if (ThesaurusItemContentType.CANDIDAT_CONTENT_TYPE_ID.equals(cType))
486        {
487            infos.put("type", "candidate");
488            infos.put("comment", term.getMetadataHolder().getString("comment", ""));
489        }
490        
491        return infos;
492    }
493    
494    /**
495     * Get the JSON representation of terms
496     * @param ids The ids of terms
497     * @return A JSON map with term's properties
498     */
499    @SuppressWarnings("unchecked")
500    @Callable
501    public Map<String, Object> termsToJSON (List<String> ids)
502    {
503        Map<String, Object> terms = new HashMap<>();
504        terms.put("terms", new ArrayList<>());
505
506        for (String id : ids)
507        {
508            Map<String, Object> infos = new HashMap<>();
509            
510            Content term = _resolver.resolveById(id);
511            
512            infos.put("id", term.getId());
513            infos.put("name", term.getName());
514            infos.put("title", term.getTitle());
515            infos.put("lang", term.getLanguage());
516            infos.put("path", getTermPath(term));
517            infos.put("creator", term.getCreator());
518            infos.put("lastContributor", term.getLastContributor());
519            infos.put("creationDate", term.getCreationDate());
520            infos.put("lastModified", term.getLastModified());
521            
522            MicroThesaurus microthesaurus = getParentMicrothesaurus(term);
523            infos.put("microthesaurusId", microthesaurus.getId());
524            
525            Thesaurus thesaurus = getParentThesaurus(microthesaurus);
526            infos.put("thesaurusId", thesaurus.getId());
527            
528            if (term instanceof ModifiableContent)
529            {
530                infos.put("isModifiable", true);
531            }
532            
533            if (term instanceof LockableAmetysObject)
534            {
535                LockableAmetysObject lockableContent = (LockableAmetysObject) term;
536                if (lockableContent.isLocked())
537                {
538                    infos.put("locked", true);
539                    infos.put("lockOwner", lockableContent.getLockOwner());
540                }
541            }
542            
543            if (ThesaurusItemContentType.TERM_CONTENT_TYPE_ID.equals(term.getTypes()[0]))
544            {
545                Content genericTerm = getGenericTerm(term);
546                if (genericTerm != null)
547                {
548                    infos.put("parentId", genericTerm.getId());
549                }
550            }
551            else if (ThesaurusItemContentType.CANDIDAT_CONTENT_TYPE_ID.equals(term.getTypes()[0]))
552            {
553                infos.put("parentId", term.getParent().getId());
554                
555                String comment = term.getMetadataHolder().getString("comment", "");
556                infos.put("comment", comment);
557            }
558            
559            ((List<Object>) terms.get("terms")).add(infos);
560        }
561        
562        return terms;
563    }
564    
565    /**
566     * Creates a microthesaurus
567     * @param label The label
568     * @return The id and label of the created microthesaurus
569     */
570    @Callable
571    public Map<String, Object> createThesaurus (String label)
572    {
573        ModifiableTraversableAmetysObject rootNode = getRootNode();
574        
575        if (_rightManager.hasRight(_currentUserProvider.getUser(), "Thesaurus_Rights_EditThesaurus", rootNode) != RightResult.RIGHT_ALLOW)
576        {
577            String errorMessage = "User " + _currentUserProvider.getUser() + " try to create thesaurus with no sufficient rights"; 
578            getLogger().error(errorMessage);
579            throw new IllegalStateException(errorMessage);
580        }   
581        
582        String originalName = FilterNameHelper.filterName(label);
583        
584        // Find unique name
585        int index = 2;
586        String name = originalName;
587        while (rootNode.hasChild(name))
588        {
589            name = originalName + "_" + (index++);
590        }
591        
592        Thesaurus thesaurus = rootNode.createChild(name, ThesaurusFactory.THESAURUS_NODETYPE);
593        thesaurus.setLabel(label);
594        
595        rootNode.saveChanges();
596        
597        Map<String, Object> result = new HashMap<>();
598        result.put("id", thesaurus.getId());
599        result.put("label", thesaurus.getLabel());
600        
601        return result;
602    }
603    
604    /**
605     * Updates a thesaurus
606     * @param thesaurusId The id of microthesaurus
607     * @param label The new label
608     * @return The id and label of the updated microthesaurus
609     */
610    @Callable
611    public Map<String, Object> updateThesaurus (String thesaurusId, String label)
612    {
613        Thesaurus thesaurus = _resolver.resolveById(thesaurusId);
614        
615        if (_rightManager.hasRight(_currentUserProvider.getUser(), "Thesaurus_Rights_EditThesaurus", thesaurus) != RightResult.RIGHT_ALLOW)
616        {
617            String errorMessage = "User " + _currentUserProvider.getUser() + " try to edit thesaurus of id " + thesaurusId + " with no sufficient rights"; 
618            getLogger().error(errorMessage);
619            throw new IllegalStateException(errorMessage);
620        }   
621        
622        thesaurus.setLabel(label);
623        thesaurus.saveChanges();
624        
625        Map<String, Object> result = new HashMap<>();
626        result.put("id", thesaurus.getId());
627        result.put("label", thesaurus.getLabel());
628        
629        return result;
630    }
631    
632    /**
633     * Deletes a thesaurus
634     * @param thesaurusId The id of thesaurus
635     * @return The id of the deleted thesaurus
636     */
637    @Callable
638    public Map<String, Object> deleteThesaurus (String thesaurusId)
639    {
640        Map<String, Object> result = new HashMap<>();
641        
642        Thesaurus thesaurus = _resolver.resolveById(thesaurusId);
643        
644        if (_rightManager.hasRight(_currentUserProvider.getUser(), "Thesaurus_Rights_EditThesaurus", thesaurus) != RightResult.RIGHT_ALLOW)
645        {
646            String errorMessage = "User " + _currentUserProvider.getUser() + " try to delete thesaurus of id " + thesaurusId + " with no sufficient rights"; 
647            getLogger().error(errorMessage);
648            throw new IllegalStateException(errorMessage);
649        }   
650        
651        result.put("id", thesaurus.getId());
652        
653        // Microthesaurii must be deleted first.
654        AmetysObjectIterable<MicroThesaurus> microthesaurii = getMicrothesaurii(thesaurusId);
655        AmetysObjectIterator<MicroThesaurus> it = microthesaurii.iterator();
656        
657        while (it.hasNext())
658        {
659            Map<String, Object> sResult = deleteMicrothesaurus(it.next().getId());
660            if (sResult.containsKey("hasReferences"))
661            {
662                result.put("hasReferences", true);
663                return result;
664            }
665        }
666        
667        thesaurus.remove();
668        thesaurus.saveChanges();
669        
670        return result;
671    }
672    
673    /**
674     * Get the label of thesaurus
675     * @param thesaurusId The id of thesaurus
676     * @return The id and label of the thesaurus
677     */
678    @Callable
679    public Map<String, Object> getThesaurusLabel (String thesaurusId)
680    {
681        Map<String, Object> result = new HashMap<>();
682        try
683        {
684            Thesaurus thesaurus = _resolver.resolveById(thesaurusId);
685            
686            result.put("id", thesaurus.getId());
687            result.put("label", thesaurus.getLabel());
688        }
689        catch (UnknownAmetysObjectException e)
690        {
691            getLogger().error("Thesaurus of id '" + thesaurusId + "' does not exist anymore", e);
692            result.put("error", "not-found");
693        }
694        
695        return result;
696    }
697    
698    /**
699     * Creates a microthesaurus
700     * @param label The label
701     * @param thesaurusId The id of parent thesaurus
702     * @return The id and label of the created microthesaurus
703     */
704    @Callable
705    public Map<String, Object> createMicrothesaurus (String label, String thesaurusId)
706    {
707        ModifiableTraversableAmetysObject rootNode = getRootNode();
708        
709        Thesaurus thesaurus = _resolver.resolveById(thesaurusId);
710        
711        String originalName = FilterNameHelper.filterName(label);
712        
713        // Find unique name
714        int index = 2;
715        String name = originalName;
716        while (thesaurus.hasChild(name))
717        {
718            name = originalName + "_" + (index++);
719        }
720        
721        if (_rightManager.hasRight(_currentUserProvider.getUser(), "Thesaurus_Rights_EditMicrothesaurus", thesaurus) != RightResult.RIGHT_ALLOW)
722        {
723            String errorMessage = "User " + _currentUserProvider.getUser() + " try to create a microthesaurus with no sufficient rights"; 
724            getLogger().error(errorMessage);
725            throw new IllegalStateException(errorMessage);
726        }    
727        
728        MicroThesaurus mt = thesaurus.createChild(name, MicroThesaurusFactory.MICROTHESAURUS_NODETYPE);
729        mt.setLabel(label);
730        
731        rootNode.saveChanges();
732        
733        Map<String, Object> result = new HashMap<>();
734        result.put("id", mt.getId());
735        result.put("label", mt.getLabel());
736        result.put("thesaurusId", mt.getParent().getId());
737        
738        return result;
739    }
740    
741    /**
742     * Updates a microthesaurus
743     * @param microthesaurusId The id of microthesaurus
744     * @param label The label
745     * @return The id and label of the updated microthesaurus
746     */
747    @Callable
748    public Map<String, Object> updateMicrothesaurus (String microthesaurusId, String label)
749    {
750        MicroThesaurus microThesaurus = _resolver.resolveById(microthesaurusId);
751        
752        if (_rightManager.hasRight(_currentUserProvider.getUser(), "Thesaurus_Rights_EditMicrothesaurus", microThesaurus) != RightResult.RIGHT_ALLOW)
753        {
754            String errorMessage = "User " + _currentUserProvider.getUser() + " try to edit microthesaurus of id '" + microthesaurusId + "' with no sufficient rights"; 
755            getLogger().error(errorMessage);
756            throw new IllegalStateException(errorMessage);
757        }    
758        
759        microThesaurus.setLabel(label);
760        microThesaurus.saveChanges();
761        
762        Map<String, Object> result = new HashMap<>();
763        result.put("id", microThesaurus.getId());
764        result.put("label", microThesaurus.getLabel());
765        result.put("thesaurusId", microThesaurus.getParent().getId());
766        
767        return result;
768    }
769    
770    /**
771     * Deletes a microthesaurus
772     * @param microthesaurusId The id of microthesaurus
773     * @return The id of the deleted microthesaurus
774     */
775    @Callable
776    public Map<String, Object> deleteMicrothesaurus(String microthesaurusId)
777    {
778        Map<String, Object> result = new HashMap<>();
779        MicroThesaurus microThesaurus = _resolver.resolveById(microthesaurusId);
780        
781        if (_rightManager.hasRight(_currentUserProvider.getUser(), "Thesaurus_Rights_EditMicrothesaurus", microThesaurus) != RightResult.RIGHT_ALLOW)
782        {
783            String errorMessage = "User " + _currentUserProvider.getUser() + " try to delete microthesaurus of id '" + microthesaurusId + "' with no sufficient rights"; 
784            getLogger().error(errorMessage);
785            throw new IllegalStateException(errorMessage);
786        }          
787        
788        result.put("id", microThesaurus.getId());
789        result.put("thesaurusId", microThesaurus.getParent().getId());
790        
791        // First check references
792        AmetysObjectIterable<Content> terms = getChildTerms(microthesaurusId);
793        AmetysObjectIterator<Content> it = terms.iterator();
794        while (it.hasNext())
795        {
796            if (!_checkReferenced(it.next()))
797            {
798                result.put("hasReferences", true);
799                return result;
800            }
801        }
802        
803        if (microThesaurus.hasChild(ThesaurusDAO.ROOT_CANDIDATES_NODENAME))
804        {
805            TraversableAmetysObject rootCandidates = microThesaurus.getChild(ThesaurusDAO.ROOT_CANDIDATES_NODENAME);
806            AmetysObjectIterable<Content> candidates = getChildTerms(rootCandidates.getId());
807            AmetysObjectIterator<Content> candidatesIt = candidates.iterator();
808            
809            while (candidatesIt.hasNext())
810            {
811                if (!_checkReferenced(candidatesIt.next()))
812                {
813                    result.put("hasReferences", true);
814                    return result;
815                }
816            }
817        }
818        
819        microThesaurus.remove();
820        microThesaurus.saveChanges();
821        
822        return result;
823    }
824    
825    /**
826     * Creates a new candidate
827     * @param label The label of candidate
828     * @param comment Comment on candidate
829     * @param lang The language code
830     * @param microthesaurusId The parent microthesaurus
831     * @return The id of created content
832     * @throws WorkflowException if an error occurs in the created candidate's workflow
833     */
834    @Callable
835    public Map<String, Object> createCandidate (String label, String comment, String lang, String microthesaurusId) throws WorkflowException
836    {
837        return createCandidate(label, comment, lang, microthesaurusId, false);
838    }
839    
840    /**
841     * Creates a new candidate
842     * @param label The label of candidate
843     * @param comment Comment on candidate
844     * @param lang The language code
845     * @param microthesaurusId The parent microthesaurus
846     * @param disableIndexation When true, disable the indexation
847     * @return The id of created content
848     * @throws WorkflowException if an error occurs in the created candidate's workflow
849     */
850    @Callable
851    public Map<String, Object> createCandidate (String label, String comment, String lang, String microthesaurusId, boolean disableIndexation) throws WorkflowException
852    {
853        MicroThesaurus microthesaurus = _resolver.resolveById(microthesaurusId);
854        
855        ModifiableTraversableAmetysObject rootCandidates = _getOrCreateNode(microthesaurus, ROOT_CANDIDATES_NODENAME, "ametys:unstructured");
856        
857        Map<String, Object> inputs = new HashMap<>();
858        if (disableIndexation)
859        {
860            inputs.put(CreateTermFunction.DISABLE_INDEXATION, Boolean.TRUE);
861        }
862        inputs.put(CreateTermFunction.PARENT_MT_ID_KEY, rootCandidates.getId());
863        Map<String, Object> result = _contentWorkflowHelper.createContent(__TERM_WORKFLOW_NAME, 1, _getContentName(label), label, new String[]{ThesaurusItemContentType.CANDIDAT_CONTENT_TYPE_ID}, new String[0], lang, null, null, inputs);
864        
865        WorkflowAwareContent content = (WorkflowAwareContent) result.get(AbstractContentWorkflowComponent.CONTENT_KEY);
866        
867        if (StringUtils.isNotEmpty(comment))
868        {
869            content.getMetadataHolder().setMetadata("comment", comment);
870            content.saveChanges();
871        }
872        
873        Map<String, Object> jsonMap = new HashMap<>();
874        jsonMap.put("id", content.getId());
875        jsonMap.put("parentId", content.getParent().getId());
876        jsonMap.put("microthesaurusId", microthesaurusId);
877        jsonMap.put("thesaurusId", microthesaurus.getParent().getId());
878        
879        return jsonMap;
880    }
881    
882    /**
883     * Edits a candidate
884     * @param id The id of candidate
885     * @param label The label
886     * @param comment The comment
887     * @return The result
888     */
889    @Callable
890    public Map<String, Object> editCandidate (String id, String label, String comment)
891    {
892        ModifiableContent content = _resolver.resolveById(id);
893        content.setTitle(label);
894        content.getMetadataHolder().setMetadata("comment", comment);
895        
896        content.saveChanges();
897        
898        Map<String, Object> result = new HashMap<>();
899        result.put("id", content.getId());
900        result.put("label", content.getTitle());
901        
902        return result;
903    }
904    
905    /**
906     * Creates a new term of micro-thesaurus
907     * @param label The label
908     * @param lang The language code
909     * @param parentId The parent id in micro-thesaurus
910     * @return The id of created content
911     * @throws WorkflowException if an error occurs in the created terms's workflow
912     */
913    @Callable
914    public Map<String, Object> createTerm (String label, String lang, String parentId) throws WorkflowException
915    {
916        return createTerm(label, lang, parentId, false);
917    }
918    
919    /**
920     * Creates a new term of micro-thesaurus
921     * @param label The label
922     * @param lang The language code
923     * @param parentId The parent id in micro-thesaurus
924     * @param disableIndexation true not to index the term, false otherwise
925     * @return The id of created content
926     * @throws WorkflowException if an error occurs in the created terms's workflow
927     */
928    @Callable
929    public Map<String, Object> createTerm (String label, String lang, String parentId, boolean disableIndexation) throws WorkflowException
930    {
931        Map<String, Object> result = null;
932        
933        Map<String, Object> inputs = new HashMap<>();
934        if (disableIndexation)
935        {
936            inputs.put(CreateTermFunction.DISABLE_INDEXATION, Boolean.TRUE);
937        }
938        
939        AmetysObject parent = _resolver.resolveById(parentId);
940        if (parent instanceof MicroThesaurus)
941        {
942            inputs.put(CreateTermFunction.PARENT_MT_ID_KEY, parentId);
943            result = _contentWorkflowHelper.createContent(__TERM_WORKFLOW_NAME, 1, _getContentName(label), label, new String[]{ThesaurusItemContentType.TERM_CONTENT_TYPE_ID}, new String[0], lang, null, null, inputs);
944        }
945        else
946        {
947            result = _contentWorkflowHelper.createContent(__TERM_WORKFLOW_NAME, 1, _getContentName(label), label, new String[]{ThesaurusItemContentType.TERM_CONTENT_TYPE_ID}, new String[0], lang, parentId, METADATA_SPECIFIC_TERMS, inputs);
948        }
949        
950        WorkflowAwareContent content = (WorkflowAwareContent) result.get(AbstractContentWorkflowComponent.CONTENT_KEY);
951        MicroThesaurus parentMicrothesaurus = getParentMicrothesaurus(content);
952        Content genericTerm = getGenericTerm(content);
953        
954        Map<String, Object> jsonMap = new HashMap<>();
955        jsonMap.put("id", content.getId());
956        jsonMap.put("parentId", genericTerm != null ? genericTerm.getId() : parentMicrothesaurus.getId());
957        jsonMap.put("microthesaurusId", parentMicrothesaurus.getId());
958        jsonMap.put("thesaurusId", parentMicrothesaurus.getParent().getId());
959        
960        return jsonMap;
961    }
962    
963    /**
964     * Move a term
965     * @param term the term to move
966     * @param parent The parent ametys object, to where the term will be moved 
967     * @throws AmetysRepositoryException if an error occurs while manipulating the repository
968     */
969    public void moveTerm(Content term, TraversableAmetysObject parent) throws AmetysRepositoryException
970    {
971        try
972        {
973            Node termNode = null;
974            Node parentNode = null;
975            Session jcrSession = null;
976            
977            // Src
978            if (term instanceof JCRAmetysObject)
979            {
980                termNode = ((JCRAmetysObject) term).getNode();
981                jcrSession = termNode.getSession();
982            }
983            else
984            {
985                throw new AmetysRepositoryException("Unable to move the term '" + term.getId() + "'. A JCRAmetysObject is expected");
986            }
987            
988            // Dst
989            if (parent instanceof MicroThesaurus)
990            {
991                parentNode = ((JCRAmetysObject) parent).getNode();
992            }
993            else if (parent instanceof ModifiableContent)
994            {
995                TraversableAmetysObject specificTerms = ((ModifiableContent) parent).getMetadataHolder().getObjectCollection(METADATA_SPECIFIC_TERMS, true);
996                
997                if (specificTerms instanceof JCRAmetysObject)
998                {
999                    parentNode = ((JCRAmetysObject) specificTerms).getNode();
1000                }
1001                else
1002                {
1003                    throw new AmetysRepositoryException("Unable to move the term '" + term.getId() + "'. Parent specific term node should be a should be a JCRAmetysObject. Parent id : '" + parent.getId() + "'.");
1004                }
1005            }
1006            else
1007            {
1008                throw new AmetysRepositoryException("Unable to move the term '" + term.getId() + "'. Parent should be a MicroThesaurus or a ModifiableContent : '" + parent.getId() + "'.");
1009            }
1010            
1011            String dstAbsPath = parentNode.getPath();
1012            String originalName = termNode.getName();
1013            String name = originalName;
1014            
1015            // Find unused name on new parent node
1016            int index = 1;
1017            while (jcrSession.getRootNode().hasNode(dstAbsPath.substring(1) + "/" + name))
1018            {
1019                name = originalName + "-" + index++;
1020            }
1021            
1022            // Move node
1023            jcrSession.move(termNode.getPath(), dstAbsPath + "/" + name);
1024            
1025        }
1026        catch (RepositoryException e)
1027        {
1028            throw new AmetysRepositoryException(String.format("Unable to move term '%s' to parent '%s'", term.getId(), parent.getId()), e);
1029        }
1030    }
1031    
1032    /**
1033     * Retrieves the content name given the desired title
1034     * @param title the content's title
1035     * @return The content name
1036     */
1037    protected String _getContentName (String title)
1038    {
1039        Matcher m = __NUMERIC_PATTERN.matcher(title);
1040        if (m.matches())
1041        {
1042            return "x" + title;
1043        }
1044        return title;
1045    }
1046    
1047    /**
1048     * Determines if the content is a candidate
1049     * @param content The content
1050     * @return <code>true</code> if it is a candidate
1051     */
1052    public boolean isCandidate (Content content)
1053    {
1054        return ThesaurusItemContentType.CANDIDAT_CONTENT_TYPE_ID.equals(content.getTypes()[0]);
1055    }
1056    
1057    private boolean _checkReferenced (Content term)
1058    {
1059        if (term instanceof TraversableAmetysObject)
1060        {
1061            AmetysObjectIterable<Content> terms = getChildTerms((TraversableAmetysObject) term);
1062            AmetysObjectIterator<Content> it = terms.iterator();
1063            while (it.hasNext())
1064            {
1065                if (!_checkReferenced(it.next()))
1066                {
1067                    return false;
1068                }
1069            }
1070        }
1071        
1072        Collection<Content> referencingContents = term.getReferencingContents();
1073        for (Content content : referencingContents)
1074        {
1075            if (!ArrayUtils.contains(content.getTypes(), ThesaurusItemContentType.TERM_CONTENT_TYPE_ID))
1076            {
1077                // The term is referenced by a content
1078                return false;
1079            }
1080        }
1081        
1082        // The term is not referenced or referenced by other term(s)
1083        return true;
1084    }
1085    
1086    /**
1087     * Returns paths of terms ids
1088     * @param termIds the list of terms ids
1089     * @return paths
1090     */
1091    @Callable
1092    public Map<String, Object> getTermsPaths(List<String> termIds)
1093    {
1094        Map<String, Object> result = new HashMap<>();
1095        List<String> paths = new ArrayList<>();
1096        
1097        for (String termId : termIds)
1098        {
1099            ModifiableContent term = _resolver.resolveById(termId);
1100            paths.add(getTermPath(term));
1101        }
1102        
1103        result.put("paths", paths);
1104        
1105        return result;
1106    }
1107    
1108    /**
1109     * Returns the path of this term in its hierarchy<br>
1110     * @param term The content term
1111     * @return the path of this term in its hierarchy.
1112     * @throws AmetysRepositoryException if an error occurs.
1113     */
1114    public String getTermPath (Content term)
1115    {
1116        Content parent = getGenericTerm(term);
1117        
1118        if (parent != null)
1119        {
1120            return getTermPath(parent) + "/" + term.getName();
1121        }
1122        else
1123        {
1124            if (term.getTypes()[0].contains("candidate"))
1125            {
1126                return "ametys:candidates/" + term.getName();
1127            }
1128            return term.getName();
1129        }
1130    }
1131    
1132    /**
1133     * Get the root plugin storage object.
1134     * @return the root plugin storage object.
1135     * @throws AmetysRepositoryException if a repository error occurs.
1136     */
1137    public ModifiableTraversableAmetysObject getRootNode() throws AmetysRepositoryException
1138    {
1139        try
1140        {
1141            ModifiableTraversableAmetysObject pluginsNode = _resolver.resolveByPath("/ametys:plugins");
1142            
1143            ModifiableTraversableAmetysObject pluginNode = _getOrCreateNode(pluginsNode, __PLUGIN_NODE_NAME, "ametys:unstructured");
1144            
1145            return _getOrCreateNode(pluginNode, "ametys:thesaurii", __ROOT_THESAURII_NODETYPE);
1146        }
1147        catch (AmetysRepositoryException e)
1148        {
1149            throw new AmetysRepositoryException("Unable to get the thesaurus root node", e);
1150        }
1151    }
1152    
1153    /**
1154     * Get or create a node
1155     * @param parentNode the parent node
1156     * @param nodeName the name of the node
1157     * @param nodeType the type of the node
1158     * @return The retrieved or created node
1159     * @throws AmetysRepositoryException if an error occurs when manipulating the repository
1160     */
1161    protected ModifiableTraversableAmetysObject _getOrCreateNode(ModifiableTraversableAmetysObject parentNode, String nodeName, String nodeType) throws AmetysRepositoryException
1162    {
1163        ModifiableTraversableAmetysObject definitionsNode;
1164        if (parentNode.hasChild(nodeName))
1165        {
1166            definitionsNode = parentNode.getChild(nodeName);
1167        }
1168        else
1169        {
1170            definitionsNode = parentNode.createChild(nodeName, nodeType);
1171            parentNode.saveChanges();
1172        }
1173        return definitionsNode;
1174    }
1175    
1176    /**
1177     * Get the path of node which match filter regexp
1178     * @param value the value to match
1179     * @param parentId the parent node id
1180     * @return the matching paths
1181     */
1182    @Callable
1183    public Map<String, Object> filterTermsByRegExp (String value, String parentId)
1184    {
1185        TraversableAmetysObject parent = _resolver.resolveById(parentId);
1186        
1187        String toMatch = Normalizer.normalize(value.toLowerCase(), Normalizer.Form.NFD).replaceAll("[\\p{InCombiningDiacriticalMarks}]", "").trim();
1188        
1189        Map<String, Object> result = new HashMap<>();
1190         
1191        List<Map<String, Object>> matchingPaths = new ArrayList<>();
1192        _getMatchingTerm(toMatch, parent, matchingPaths);
1193        
1194        result.put("paths", matchingPaths);
1195        return result;
1196    }
1197    
1198    /**
1199     * Get paths of term which match filter regexp
1200     * @param value the value to match
1201     * @param ao the ametys object
1202     * @param matchingPaths the matching paths
1203     */
1204    private void _getMatchingTerm (String value, TraversableAmetysObject ao, List<Map<String, Object>> matchingPaths)
1205    {
1206        if (ao instanceof Content)
1207        {
1208            Content content = (Content) ao;
1209            
1210            String normalizedName = Normalizer.normalize(content.getTitle().toLowerCase(), Normalizer.Form.NFD).replaceAll("[\\p{InCombiningDiacriticalMarks}]", "");
1211            
1212            if (normalizedName.contains(value))
1213            {
1214                Map<String, Object> matchingPath = new HashMap<>();
1215                matchingPath.put("path", getTermPath(content));
1216                matchingPath.put("matchSynonyms", false);
1217                matchingPaths.add(matchingPath);
1218            }
1219            else if (content.getMetadataHolder().hasMetadata(METADATA_SYNONYMS)) 
1220            {
1221                String[] synonyms = content.getMetadataHolder().getStringArray(METADATA_SYNONYMS, new String[0]); 
1222                for (String synonym : synonyms)
1223                {
1224                    String normalizedSynonym = Normalizer.normalize(synonym.toLowerCase(), Normalizer.Form.NFD).replaceAll("[\\p{InCombiningDiacriticalMarks}]", "");
1225                
1226                    if (normalizedSynonym.contains(value))
1227                    {
1228                        Map<String, Object> matchingPath = new HashMap<>();
1229                        matchingPath.put("path", getTermPath(content));
1230                        matchingPath.put("matchSynonyms", true);
1231                        matchingPaths.add(matchingPath);
1232                        break;
1233                    }
1234                }
1235            }
1236            
1237            if (content.getMetadataHolder().hasMetadata(METADATA_SPECIFIC_TERMS))
1238            {
1239                TraversableAmetysObject specificTerms = content.getMetadataHolder().getObjectCollection(METADATA_SPECIFIC_TERMS);
1240                
1241                for (AmetysObject child : specificTerms.getChildren())
1242                {
1243                    _getMatchingTerm(value, (TraversableAmetysObject) child, matchingPaths);
1244                }
1245            }
1246        }
1247        else
1248        {
1249            AmetysObjectIterable< ? extends TraversableAmetysObject> children = ao.getChildren();
1250            for (TraversableAmetysObject child : children)
1251            { 
1252                _getMatchingTerm (value, child, matchingPaths);
1253            }
1254        }
1255    }
1256}