001/*
002 *  Copyright 2015 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.glossary;
017
018import java.text.Normalizer;
019import java.text.Normalizer.Form;
020import java.util.ArrayList;
021import java.util.Collections;
022import java.util.HashMap;
023import java.util.List;
024import java.util.Map;
025import java.util.Set;
026
027import org.apache.avalon.framework.component.Component;
028import org.apache.avalon.framework.logger.AbstractLogEnabled;
029import org.apache.avalon.framework.service.ServiceException;
030import org.apache.avalon.framework.service.ServiceManager;
031import org.apache.avalon.framework.service.Serviceable;
032import org.apache.commons.lang.StringUtils;
033
034import org.ametys.core.ui.Callable;
035import org.ametys.plugins.glossary.theme.ThemesDAO;
036import org.ametys.plugins.repository.AmetysObjectIterable;
037import org.ametys.plugins.repository.AmetysObjectResolver;
038import org.ametys.plugins.repository.AmetysRepositoryException;
039import org.ametys.plugins.repository.ModifiableAmetysObject;
040import org.ametys.plugins.repository.ModifiableTraversableAmetysObject;
041import org.ametys.plugins.repository.TraversableAmetysObject;
042import org.ametys.plugins.repository.UnknownAmetysObjectException;
043import org.ametys.plugins.repository.jcr.NameHelper;
044import org.ametys.runtime.i18n.I18nizableText;
045import org.ametys.web.repository.site.SiteManager;
046
047/**
048 * DAO for manipulating glossary.
049 *
050 */
051public class GlossaryDAO extends AbstractLogEnabled implements Serviceable, Component
052{
053    /** The Avalon role */
054    public static final String ROLE = GlossaryDAO.class.getName();
055    
056    /** The site manager */
057    protected SiteManager _siteManager;
058    
059    /** Ametys object resolver */
060    protected AmetysObjectResolver _resolver;
061    
062    /** The themes DAO */
063    ThemesDAO _themeDAO;
064    
065    @Override
066    public void service(ServiceManager manager) throws ServiceException
067    {
068        _siteManager = (SiteManager) manager.lookup(SiteManager.ROLE);
069        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
070        _themeDAO = (ThemesDAO) manager.lookup(ThemesDAO.ROLE);
071    }
072    
073    /**
074     * Get the word definitions 
075     * @param siteName The site name
076     * @param lang The language
077     * @return The word definitions
078     */
079    @Callable (rights = "Glossary_Rights_Definitions_Handle")
080    public Map<String, Object> getDefinitions(String siteName, String lang)
081    {
082        Map<String, Object> result = new HashMap<>();
083        
084        List<Map<String, Object>> definitions = new ArrayList<>();
085        
086        TraversableAmetysObject definitionsNode = GlossaryHelper.getDefinitionsNode(_siteManager.getSite(siteName), lang);
087        
088        try (AmetysObjectIterable<DefaultDefinition> wordDefinitions = definitionsNode.getChildren())
089        {
090            for (DefaultDefinition wordDefinition : wordDefinitions)
091            {
092                definitions.add(getDefinition(wordDefinition));
093            }
094        }
095        
096        result.put("definitions", definitions);
097        
098        return result;
099    }
100    
101    /**
102     * Get properties of a word definition
103     * @param id the id of definition
104     * @return the properties
105     */
106    @Callable (rights = "Glossary_Rights_Definitions_Handle")
107    public Map<String, Object> getDefinition (String id)
108    {
109        DefaultDefinition wordDefinition = _resolver.resolveById(id);
110        
111        return getDefinition(wordDefinition);
112    }
113    
114    /**
115     * Get the definition in JSON 
116     * @param wordDefinition the word definition
117     * @return the JSON for the word definition
118     */
119    public Map<String, Object> getDefinition (DefaultDefinition wordDefinition)
120    {
121        Map<String, Object> properties = new HashMap<>();
122        
123        properties.put("id", wordDefinition.getId());
124        properties.put("lang", wordDefinition.getLanguage());
125        properties.put("siteName", wordDefinition.getSiteName());
126        properties.put("word", wordDefinition.getWord());
127        String firstLetter = Normalizer.normalize(wordDefinition.getWord().substring(0, 1), Form.NFD).replaceAll("\\p{InCombiningDiacriticalMarks}+", "");
128        properties.put("firstLetter", firstLetter);
129        properties.put("variants", wordDefinition.getVariants());
130        properties.put("content", wordDefinition.getContent());
131        properties.put("displayOnText", wordDefinition.displayOnText());
132        
133        // Themes
134        List<Map<String, Object>> themesList = new ArrayList<>();
135        for (String themeId : wordDefinition.getThemes())
136        {
137            try
138            {
139                I18nizableText themeTitle = _themeDAO.getThemeTitle(themeId, wordDefinition.getSiteName(), wordDefinition.getLanguage());
140                Map<String, Object> themeData = new HashMap<>();
141                themeData.put("id", themeId);
142                themeData.put("label", themeTitle);
143                themesList.add(themeData);
144            }
145            catch (UnknownAmetysObjectException e)
146            {
147                // Theme does not exist anymore
148            }
149        }
150        
151        properties.put("themes", themesList);
152        
153        return properties;
154    }
155    
156    /**
157     * Creates a word definition.
158     * @param word The word
159     * @param variants The variants
160     * @param content The content
161     * @param themes the themes
162     * @param display The value of displayOnText
163     * @param siteName The site name
164     * @param language The language
165     * @return The id of the created word, or an error
166     */
167    @Callable (rights = "Glossary_Rights_Definitions_Handle")
168    public Map<String, String> createDefinition (String word, String variants, String content,  List<String> themes, Boolean display, String siteName, String language)
169    {
170        Map<String, String> result = new HashMap<>();
171        
172        String capitalizedWord = StringUtils.capitalize(word);
173        List<String> variantList = new ArrayList<>();
174        if (StringUtils.isNotBlank(variants))
175        {
176            for (String variant : StringUtils.split(variants, ","))
177            {
178                String trimVariant = variant.trim();
179                if (StringUtils.isNotEmpty(trimVariant))
180                {
181                    variantList.add(StringUtils.capitalize(trimVariant));
182                }
183            }
184        }
185        
186        // Check that the word doesn't already exist.
187        if (_wordExists(capitalizedWord, siteName, language))
188        {
189            result.put("error", "word-already-exists");
190            return result;
191        }
192        
193        // Check that none of the variant already exists.
194        for (String variant : variantList)
195        {
196            if (_wordExists(variant, siteName, language))
197            {
198                result.put("error", "word-already-exists");
199                return result;
200            }
201        }
202        
203        ModifiableTraversableAmetysObject rootNode = GlossaryHelper.getDefinitionsNode(_siteManager.getSite(siteName), language);
204        
205        String originalName = NameHelper.filterName(capitalizedWord);
206        
207        // Find unique name
208        String name = originalName;
209        int index = 2;
210        while (rootNode.hasChild(name))
211        {
212            name = originalName + "-" + (index++);
213        }
214        
215        DefaultDefinition def = rootNode.createChild(name, DefaultDefinitionFactory.DEFINITION_NODE_TYPE);
216        def.setWord(capitalizedWord);
217        def.setVariants(variantList);
218        def.setContent(content);
219        def.setThemes(themes.toArray(new String[themes.size()]));
220        def.setDisplayOnText(display);
221        
222        rootNode.saveChanges();
223        
224        result.put("id", def.getId());
225        
226        return result;
227    }
228    
229    /**
230     * Edits a word definition.
231     * @param id The id of the word to edit
232     * @param word The new word
233     * @param variants The new variants
234     * @param content The new content
235     * @param themes the themes
236     * @param display The new value of displayOnText
237     * @param siteName The site name
238     * @param language The language
239     * @return The id of the edited word, or an error
240     */
241    @Callable (rights = "Glossary_Rights_Definitions_Handle")
242    public Map<String, String> editDefinition (String id, String word, String variants, String content, List<String> themes, Boolean display, String siteName, String language)
243    {
244        Map<String, String> result = new HashMap<>();
245        
246        String capitalizedWord = StringUtils.capitalize(word);
247        List<String> variantList = new ArrayList<>();
248        if (StringUtils.isNotBlank(variants))
249        {
250            for (String variant : StringUtils.split(variants, ","))
251            {
252                String trimVariant = variant.trim();
253                if (StringUtils.isNotEmpty(trimVariant))
254                {
255                    variantList.add(StringUtils.capitalize(variant.trim()));
256                }
257            }
258        }
259        
260        try
261        {
262            DefaultDefinition definition = _resolver.resolveById(id);
263            
264            Set<String> allForms = definition.getAllForms();
265            
266            // If the word was changed, check that the new word doesn't already exist.
267            if (!allForms.contains(word) && _wordExists(word, siteName, language))
268            {
269                return Collections.singletonMap("error", "word-already-exists");
270            }
271            
272            // If the word was changed, check that none of the variant already exists.
273            for (String variant : variantList)
274            {
275                if (!allForms.contains(variant) && _wordExists(variant, siteName, language))
276                {
277                    return Collections.singletonMap("error", "word-already-exists");
278                }
279            }
280            
281            definition.setWord(capitalizedWord.trim());
282            definition.setVariants(variantList);
283            definition.setContent(content);
284            definition.setThemes(themes.toArray(new String[themes.size()]));
285            definition.setDisplayOnText(display);
286            
287            definition.saveChanges();
288            
289            result.put("id", definition.getId());
290        }
291        catch (UnknownAmetysObjectException e)
292        {
293            result.put("error", "unknown-definition");
294            getLogger().error("Unable to edit definition of id '" + id + ", because it does not not exist.", e);
295        }
296        
297        return result;
298    }
299    
300    /**
301     * Deletes word definitions.
302     * @param ids The ids of definitions to delete
303     * @return The ids of the deleted words, the ids of the unknown words, and maybe an error
304     */
305    @Callable (rights = "Glossary_Rights_Definitions_Handle")
306    public Map<String, Object> deleteDefinitions (ArrayList<String> ids)
307    {
308        Map<String, Object> result = new HashMap<>();
309        List<String> deletedDefinitions = new ArrayList<>();
310        List<String> unknownIds = new ArrayList<>();
311        
312        for (String id : ids)
313        {
314            try
315            {
316                DefaultDefinition definition = _resolver.resolveById(id);
317                
318                ModifiableAmetysObject parent = definition.getParent();
319                definition.remove();
320                
321                parent.saveChanges();
322                
323                deletedDefinitions.add(id);
324            }
325            catch (UnknownAmetysObjectException e)
326            {
327                result.put("error", "unknown-definition");
328                getLogger().error("Unable to delete the definition of id '" + id + ", because it does not exist.", e);
329                unknownIds.add(id);
330            }
331        }
332        
333        result.put("ids", deletedDefinitions);
334        result.put("unknown-ids", unknownIds);
335        
336        return result;
337    }
338    
339    /**
340     * Tests if the specified word exists in the glossary.
341     * @param word The word to test.
342     * @param siteName The site name.
343     * @param language The language.
344     * @return true if the word exists.
345     * @throws AmetysRepositoryException if a repository error occurs.
346     */
347    protected boolean _wordExists(String word, String siteName, String language) throws AmetysRepositoryException
348    {
349        String xpathQuery = GlossaryHelper.getWordExistsQuery(siteName, language, word);
350        
351        try (AmetysObjectIterable<DefaultDefinition> defs = _resolver.query(xpathQuery))
352        {
353            return defs.iterator().hasNext();
354        }
355    }
356}