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