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}