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}