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.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(), attribute.get()); 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 /** 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.warn("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}