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