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