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<Object, Object> 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.error("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(), attribute.get());
097                        }
098                        result.put(idValue, values);
099                    }
100                    catch (Exception e)
101                    {
102                        _nbError++;
103                        logger.error("Failed to import the content '{}'", idValue, e);
104                    }
105                }
106                else
107                {
108                    logger.error("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    /**
123     * Returns the number of errors which occured {@link #search(String, int, String, String, String, int, int, Map, String, Logger)}
124     * @return the number of errors which occured {@link #search(String, int, String, String, String, int, int, Map, String, Logger)}
125     */
126    public int getNbErrors()
127    {
128        return _nbError;
129    }
130    
131    /**
132     * Returns true if the a global error occured during {@link #search(String, int, String, String, String, int, int, Map, String, Logger)}
133     * @return true if the a global error occured during {@link #search(String, int, String, String, String, int, int, Map, String, Logger)}
134     */
135    public boolean hasGlobalError()
136    {
137        return _hasGlobalError;
138    }
139    
140    /**
141     * Get the LDAP search controls.
142     * @param mapping The mapping
143     * @param searchScope The search scope
144     * @return the search controls.
145     * @throws ProcessingException if the scope is not valid
146     */
147    protected SearchControls _getSearchControls(Map<String, List<String>> mapping, String searchScope) throws ProcessingException
148    {
149        SearchControls controls = new SearchControls();
150        
151        List<String> attributes = new ArrayList<>();
152        for (List<String> attribute : mapping.values())
153        {
154            attributes.addAll(attribute);
155        }
156        String[] attrArray = attributes.toArray(new String[attributes.size()]);
157        
158        controls.setReturningAttributes(attrArray);
159        
160        controls.setSearchScope(_getScope(searchScope));
161        
162        return controls;
163    }
164    
165    /**
166     * Get the scope as an integer (handlable by the SearchControls) from the scope string.
167     * @param scopeStr the scope string.
168     * @return the scope as an integer.
169     * @throws ProcessingException if the given scope is not valid
170     */
171    protected int _getScope(String scopeStr) throws ProcessingException
172    {
173        try
174        {
175            return ScopeEnumerator.parseScope(scopeStr);
176        }
177        catch (IllegalArgumentException e)
178        {
179            throw new ProcessingException("Unable to parse scope", e);
180        }
181    }
182    
183    /**
184     * Gets id value from a ldap entry
185     * @param idKey The key where to search the id value
186     * @param entry The ldap entry
187     * @param logger The logger
188     * @return The attribute value
189     * @throws NamingException if a ldap query error occurred
190     */
191    protected Object _getIdValue(String idKey, SearchResult entry, Logger logger) throws NamingException
192    {
193        Attribute ldapAttr = entry.getAttributes().get(idKey);
194        
195        if (ldapAttr == null)
196        {
197            logger.error("LDAP attribute not found: '{}'", idKey);
198        }
199        return ldapAttr != null ? ldapAttr.get() : null;
200    }
201    
202    /**
203     * Clean a connection to a ldap server.
204     * 
205     * @param context The connection to the database to close.
206     * @param result The result to close.
207     * @param logger The logger
208     */
209    protected void _cleanup(Context context, NamingEnumeration result, Logger logger)
210    {
211        if (result != null)
212        {
213            try
214            {
215                result.close();
216            }
217            catch (NamingException e)
218            {
219                _nbError++;
220                logger.error("Error while closing ldap result", e);
221            }
222        }
223        if (context != null)
224        {
225            try
226            {
227                context.close();
228            }
229            catch (NamingException e)
230            {
231                _nbError++;
232                logger.error("Error while closing ldap connection", e);
233            }
234        }
235    }
236}