001/*
002 *  Copyright 2016 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.contentio.synchronize.impl;
017
018import java.util.ArrayList;
019import java.util.HashMap;
020import java.util.LinkedHashMap;
021import java.util.List;
022import java.util.Map;
023import java.util.stream.Collectors;
024
025import javax.naming.Context;
026import javax.naming.NamingEnumeration;
027import javax.naming.NamingException;
028import javax.naming.directory.Attribute;
029import javax.naming.directory.Attributes;
030import javax.naming.directory.SearchControls;
031import javax.naming.directory.SearchResult;
032
033import org.apache.avalon.framework.component.Component;
034import org.apache.cocoon.ProcessingException;
035import org.slf4j.Logger;
036
037import org.ametys.core.util.LambdaUtils;
038import org.ametys.core.util.ldap.AbstractLDAPConnector;
039import org.ametys.core.util.ldap.ScopeEnumerator;
040import org.ametys.plugins.contentio.synchronize.SynchronizableContentsCollection;
041
042/**
043 * Helper component for {@link SynchronizableContentsCollection}s which need to access a LDAP
044 */
045public class LDAPCollectionHelper extends AbstractLDAPConnector implements Component
046{
047    /** Avalon Role */
048    public static final String ROLE = LDAPCollectionHelper.class.getName();
049    
050    private int _nbError;
051    private boolean _hasGlobalError;
052    
053    @Override
054    public void _delayedInitialize(String dataSourceId) throws Exception
055    {
056        super._delayedInitialize(dataSourceId);
057    }
058    
059    /**
060     * Search over the LDAP the contents to import.
061     * After calling this method, call the methods {@link #getNbErrors()} and {@link #hasGlobalError()} to know about error which occured.
062     * @param collectionId The id of the collection being synchronized
063     * @param pageSize The page size for the search
064     * @param relativeDN the name of the context or object to search
065     * @param filter the filter expression to use for the search
066     * @param searchScope The search scope
067     * @param mapping The mapping for retrieving the remote values (keys are metadata paths, )
068     * @param idKey The key where to search the id value of the content
069     * @param logger The logger
070     * @return A map containing the content ids (keys) to import with their metadata remote values (key is metadata path, value is the list of remote values for this metadata).
071     */
072    public Map<String, Map<String, List<Object>>> search(String collectionId, int pageSize, String relativeDN, String filter, String searchScope, Map<String, List<String>> mapping, String idKey, Logger logger)
073    {
074        Map<String, Map<String, List<Object>>> result = new LinkedHashMap<>();
075        _nbError = 0;
076        _hasGlobalError = false;
077        
078        try
079        {
080            for (SearchResult searchResult : _search(pageSize, relativeDN, filter, _getSearchControls(mapping, searchScope)))
081            {
082                String idValue = (String) _getIdValue(idKey, searchResult, logger);
083                if (idValue == null)
084                {
085                    _nbError++;
086                    logger.error("The id value '{}' for '{}' was null ", idKey, searchResult.getName());
087                }
088                else if (!result.keySet().contains(idValue))
089                {
090                    try
091                    {
092                        Map<String, List<Object>> attributes = _getAttributes(searchResult, mapping);
093                        result.put(idValue, attributes);
094                    }
095                    catch (Exception e)
096                    {
097                        _nbError++;
098                        logger.error(String.format("Failed to import the content '%s'", idValue), e);
099                    }
100                }
101                else
102                {
103                    logger.error("Cannot import '{}' because its id value '{}={}' is already an id value for another content", searchResult.getName(), idKey, idValue);
104                }
105            }
106        }
107        catch (Exception e)
108        {
109            _hasGlobalError = true;
110            _nbError++;
111            logger.error(String.format("Failed to populate contents from synchronizable collection of id '%s'", collectionId), e);
112        }
113        
114        return result;
115    }
116    
117    /**
118     * Returns the number of errors which occured {@link #search(String, int, String, String, String, Map, String, Logger)}
119     * @return the number of errors which occured {@link #search(String, int, String, String, String, Map, String, Logger)}
120     */
121    public int getNbErrors()
122    {
123        return _nbError;
124    }
125    
126    /**
127     * Returns true if the a global error occured during {@link #search(String, int, String, String, String, Map, String, Logger)}
128     * @return true if the a global error occured during {@link #search(String, int, String, String, String, Map, String, Logger)}
129     */
130    public boolean hasGlobalError()
131    {
132        return _hasGlobalError;
133    }
134    
135    /**
136     * Get the LDAP search controls.
137     * @param mapping The mapping
138     * @param searchScope The search scope
139     * @return the search controls.
140     * @throws ProcessingException if the scope is not valid
141     */
142    protected SearchControls _getSearchControls(Map<String, List<String>> mapping, String searchScope) throws ProcessingException
143    {
144        SearchControls controls = new SearchControls();
145        
146        List<String> attributes = new ArrayList<>();
147        for (List<String> attribute : mapping.values())
148        {
149            attributes.addAll(attribute);
150        }
151        String[] attrArray = attributes.toArray(new String[attributes.size()]);
152        
153        controls.setReturningAttributes(attrArray);
154        
155        controls.setSearchScope(_getScope(searchScope));
156        
157        return controls;
158    }
159    
160    /**
161     * Get the scope as an integer (handlable by the SearchControls) from the scope string.
162     * @param scopeStr the scope string.
163     * @return the scope as an integer.
164     * @throws ProcessingException if the given scope is not valid
165     */
166    protected int _getScope(String scopeStr) throws ProcessingException
167    {
168        try
169        {
170            return ScopeEnumerator.parseScope(scopeStr);
171        }
172        catch (IllegalArgumentException e)
173        {
174            throw new ProcessingException("Unable to parse scope", e);
175        }
176    }
177    
178    /**
179     * Gets id value from a ldap entry
180     * @param idKey The key where to search the id value
181     * @param entry The ldap entry
182     * @param logger The logger
183     * @return The attribute value
184     * @throws NamingException if a ldap query error occurred
185     */
186    protected Object _getIdValue(String idKey, SearchResult entry, Logger logger) throws NamingException
187    {
188        Attribute ldapAttr = entry.getAttributes().get(idKey);
189        
190        if (ldapAttr == null)
191        {
192            logger.error("LDAP attribute not found: '{}'", idKey);
193        }
194        return ldapAttr != null ? ldapAttr.get() : null;
195    }
196    
197    /**
198     * Gets attributes from a ldap entry.
199     * @param entry The ldap entry
200     * @param mapping The mapping
201     * @return The attributes in a map
202     * @throws NamingException if a ldap query error occured
203     */
204    protected Map<String, List<Object>> _getAttributes(SearchResult entry, Map<String, List<String>> mapping) throws NamingException
205    {
206        Map<String, List<Object>> result = new HashMap<>();
207        Attributes attrs = entry.getAttributes();
208        
209        for (String metadataRef : mapping.keySet())
210        {
211            List<String> ldapAttributes = mapping.get(metadataRef);
212            List<Object> ldapValues = ldapAttributes.stream()
213                            .map(ldapAttribute -> attrs.get(ldapAttribute))
214                            .filter(val -> val != null)
215                            .map(LambdaUtils.wrap(val -> val.get()))
216                            .collect(Collectors.toList());
217            result.put(metadataRef, ldapValues);
218        }
219        
220        return result;
221    }
222    
223    /**
224     * Clean a connection to a ldap server.
225     * 
226     * @param context The connection to the database to close.
227     * @param result The result to close.
228     * @param logger The logger
229     */
230    protected void _cleanup(Context context, NamingEnumeration result, Logger logger)
231    {
232        if (result != null)
233        {
234            try
235            {
236                result.close();
237            }
238            catch (NamingException e)
239            {
240                _nbError++;
241                logger.error("Error while closing ldap result", e);
242            }
243        }
244        if (context != null)
245        {
246            try
247            {
248                context.close();
249            }
250            catch (NamingException e)
251            {
252                _nbError++;
253                logger.error("Error while closing ldap connection", e);
254            }
255        }
256    }
257}