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.time.LocalDate; 019import java.time.ZonedDateTime; 020import java.time.format.DateTimeFormatter; 021import java.time.format.DateTimeFormatterBuilder; 022import java.time.temporal.ChronoField; 023import java.util.ArrayList; 024import java.util.HashMap; 025import java.util.HashSet; 026import java.util.LinkedHashMap; 027import java.util.List; 028import java.util.Map; 029import java.util.Optional; 030import java.util.Set; 031import java.util.function.Function; 032import java.util.stream.Collectors; 033import java.util.stream.Stream; 034 035import javax.naming.Context; 036import javax.naming.NamingEnumeration; 037import javax.naming.NamingException; 038import javax.naming.directory.Attribute; 039import javax.naming.directory.SearchControls; 040import javax.naming.directory.SearchResult; 041 042import org.apache.avalon.framework.component.Component; 043import org.apache.avalon.framework.service.ServiceException; 044import org.apache.avalon.framework.service.ServiceManager; 045import org.apache.cocoon.ProcessingException; 046import org.slf4j.Logger; 047 048import org.ametys.cms.contenttype.ContentType; 049import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 050import org.ametys.core.util.ldap.AbstractLDAPConnector; 051import org.ametys.core.util.ldap.ScopeEnumerator; 052import org.ametys.plugins.contentio.synchronize.SynchronizableContentsCollection; 053import org.ametys.runtime.model.type.ModelItemTypeConstants; 054 055/** 056 * Helper component for {@link SynchronizableContentsCollection}s which need to access a LDAP 057 */ 058public class LDAPCollectionHelper extends AbstractLDAPConnector implements Component 059{ 060 /** Avalon Role */ 061 public static final String ROLE = LDAPCollectionHelper.class.getName(); 062 063 private static final DateTimeFormatter __BASIC_DATE_TIME = new DateTimeFormatterBuilder() 064 .append(DateTimeFormatter.BASIC_ISO_DATE) 065 .appendValue(ChronoField.HOUR_OF_DAY, 2) 066 .appendValue(ChronoField.MINUTE_OF_HOUR, 2) 067 .appendValue(ChronoField.SECOND_OF_MINUTE, 2) 068 .appendFraction(ChronoField.MILLI_OF_SECOND, 1, 3, true) 069 .appendOffsetId() 070 .toFormatter(); 071 072 /** The content type extension point */ 073 protected ContentTypeExtensionPoint _contentTypeEP; 074 075 private int _nbError; 076 private boolean _hasGlobalError; 077 078 @Override 079 public void service(ServiceManager serviceManager) throws ServiceException 080 { 081 _contentTypeEP = (ContentTypeExtensionPoint) serviceManager.lookup(ContentTypeExtensionPoint.ROLE); 082 super.service(serviceManager); 083 } 084 085 @Override 086 public void _delayedInitialize(String dataSourceId) throws Exception 087 { 088 super._delayedInitialize(dataSourceId); 089 } 090 091 /** 092 * Search over the LDAP the data from the filter. 093 * After calling this method, call the methods {@link #getNbErrors()} and {@link #hasGlobalError()} to know about error which occured. 094 * @param collectionId The id of the collection being synchronized 095 * @param pageSize The page size for the search 096 * @param relativeDN the name of the context or object to search 097 * @param filter the filter expression to use for the search 098 * @param searchScope The search scope 099 * @param offset Begin of the search 100 * @param limit Number of results 101 * @param mapping The mapping for retrieving the remote values (keys are metadata paths) 102 * @param idKey The key where to search the id value of the content 103 * @param logger The logger 104 * @return A map containing the content ids (keys) to import with their remote values (key is attribute, value is the remote value). 105 */ 106 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) 107 { 108 Map<String, Map<String, Object>> result = new LinkedHashMap<>(); 109 _nbError = 0; 110 _hasGlobalError = false; 111 112 try 113 { 114 for (SearchResult searchResult : _search(pageSize, relativeDN, filter, null, _getSearchControls(mapping, searchScope), offset, limit)) 115 { 116 String idValue = (String) _getIdValue(idKey, searchResult, logger); 117 if (idValue == null) 118 { 119 _nbError++; 120 logger.warn("The id value '{}' for '{}' was null ", idKey, searchResult.getName()); 121 } 122 else if (!result.keySet().contains(idValue)) 123 { 124 try 125 { 126 Map<String, Object> values = new HashMap<>(); 127 NamingEnumeration<? extends Attribute> attributes = searchResult.getAttributes().getAll(); 128 while (attributes.hasMoreElements()) 129 { 130 Attribute attribute = attributes.nextElement(); 131 values.put(attribute.getID(), _getLDAPValues(attribute)); 132 } 133 result.put(idValue, values); 134 } 135 catch (Exception e) 136 { 137 _nbError++; 138 logger.warn("Failed to import the content '{}'", idValue, e); 139 } 140 } 141 else 142 { 143 logger.warn("Cannot import '{}' because its id value '{}={}' is already an id value for another content", searchResult.getName(), idKey, idValue); 144 } 145 } 146 } 147 catch (Exception e) 148 { 149 _hasGlobalError = true; 150 _nbError++; 151 logger.error("Failed to populate contents from synchronizable collection of id '{}'", collectionId, e); 152 } 153 154 return result; 155 } 156 157 private List<Object> _getLDAPValues(Attribute attribute) throws NamingException 158 { 159 List<Object> ldapValues = new ArrayList<>(); 160 161 NamingEnumeration< ? > values = attribute.getAll(); 162 while (values.hasMoreElements()) 163 { 164 ldapValues.add(values.nextElement()); 165 } 166 167 return ldapValues; 168 } 169 170 /** 171 * Returns the number of errors which occured {@link #search(String, int, String, String, String, int, int, Map, String, Logger)} 172 * @return the number of errors which occured {@link #search(String, int, String, String, String, int, int, Map, String, Logger)} 173 */ 174 public int getNbErrors() 175 { 176 return _nbError; 177 } 178 179 /** 180 * Returns true if the a global error occured during {@link #search(String, int, String, String, String, int, int, Map, String, Logger)} 181 * @return true if the a global error occured during {@link #search(String, int, String, String, String, int, int, Map, String, Logger)} 182 */ 183 public boolean hasGlobalError() 184 { 185 return _hasGlobalError; 186 } 187 188 /** 189 * Get the LDAP search controls. 190 * @param mapping The mapping 191 * @param searchScope The search scope 192 * @return the search controls. 193 * @throws ProcessingException if the scope is not valid 194 */ 195 protected SearchControls _getSearchControls(Map<String, List<String>> mapping, String searchScope) throws ProcessingException 196 { 197 SearchControls controls = new SearchControls(); 198 199 List<String> attributes = new ArrayList<>(); 200 for (List<String> attribute : mapping.values()) 201 { 202 attributes.addAll(attribute); 203 } 204 String[] attrArray = attributes.toArray(new String[attributes.size()]); 205 206 controls.setReturningAttributes(attrArray); 207 208 controls.setSearchScope(_getScope(searchScope)); 209 210 return controls; 211 } 212 213 /** 214 * Get the scope as an integer (handlable by the SearchControls) from the scope string. 215 * @param scopeStr the scope string. 216 * @return the scope as an integer. 217 * @throws ProcessingException if the given scope is not valid 218 */ 219 protected int _getScope(String scopeStr) throws ProcessingException 220 { 221 try 222 { 223 return ScopeEnumerator.parseScope(scopeStr); 224 } 225 catch (IllegalArgumentException e) 226 { 227 throw new ProcessingException("Unable to parse scope", e); 228 } 229 } 230 231 /** 232 * Gets id value from a ldap entry 233 * @param idKey The key where to search the id value 234 * @param entry The ldap entry 235 * @param logger The logger 236 * @return The attribute value 237 * @throws NamingException if a ldap query error occurred 238 */ 239 protected Object _getIdValue(String idKey, SearchResult entry, Logger logger) throws NamingException 240 { 241 Attribute ldapAttr = entry.getAttributes().get(idKey); 242 243 if (ldapAttr == null) 244 { 245 logger.warn("LDAP attribute not found: '{}'", idKey); 246 } 247 return ldapAttr != null ? ldapAttr.get() : null; 248 } 249 250 /** 251 * Clean a connection to a ldap server. 252 * 253 * @param context The connection to the database to close. 254 * @param result The result to close. 255 * @param logger The logger 256 */ 257 protected void _cleanup(Context context, NamingEnumeration result, Logger logger) 258 { 259 if (result != null) 260 { 261 try 262 { 263 result.close(); 264 } 265 catch (NamingException e) 266 { 267 _nbError++; 268 logger.error("Error while closing ldap result", e); 269 } 270 } 271 if (context != null) 272 { 273 try 274 { 275 context.close(); 276 } 277 catch (NamingException e) 278 { 279 _nbError++; 280 logger.error("Error while closing ldap connection", e); 281 } 282 } 283 } 284 285 /** 286 * Transform date and datetime attributes on each result line from timestamp to LocalDate (date) or ZonedDateTime (datetime) 287 * @param results The results from LDAP source 288 * @param contentTypeId Content type ID from which attributes come from 289 * @param allAttributes All mapped attributes 290 */ 291 public void transformTypedAttributes(Map<String, Map<String, List<Object>>> results, String contentTypeId, Set<String> allAttributes) 292 { 293 // Define date and datetime attributes 294 Set<String> dateAttributes = new HashSet<>(); 295 Set<String> datetimeAttributes = new HashSet<>(); 296 297 ContentType contentType = _contentTypeEP.getExtension(contentTypeId); 298 for (String attributePath : allAttributes) 299 { 300 // Some synchronized attributes are not necessarily defined in the model 301 if (contentType.hasModelItem(attributePath)) 302 { 303 String attributeType = contentType.getModelItem(attributePath).getType().getId(); 304 switch (attributeType) 305 { 306 case ModelItemTypeConstants.DATE_TYPE_ID: 307 dateAttributes.add(attributePath); 308 break; 309 case ModelItemTypeConstants.DATETIME_TYPE_ID: 310 datetimeAttributes.add(attributePath); 311 break; 312 default: 313 // Nothing to do 314 break; 315 } 316 } 317 } 318 319 // Transform values to typed values 320 if (!dateAttributes.isEmpty() || !datetimeAttributes.isEmpty()) 321 { 322 for (Map<String, List<Object>> resultLine : results.values()) 323 { 324 _transformValuesAsTypedValues(resultLine, dateAttributes, s -> LocalDate.parse(s, __BASIC_DATE_TIME)); 325 _transformValuesAsTypedValues(resultLine, datetimeAttributes, s -> ZonedDateTime.parse(s, __BASIC_DATE_TIME)); 326 } 327 } 328 } 329 330 private <R> void _transformValuesAsTypedValues(Map<String, List<Object>> resultLine, Set<String> attributeNames, Function<String, R> typedFunction) 331 { 332 for (String attributeName : attributeNames) 333 { 334 _transformValueAsTypedValue(resultLine, attributeName, typedFunction); 335 } 336 } 337 338 private <R> void _transformValueAsTypedValue(Map<String, List<Object>> resultLine, String attributeName, Function<String, R> typedFunction) 339 { 340 List<Object> newValues = Optional.of(attributeName) 341 // Get the date attribute for the current result line 342 .map(resultLine::get) 343 // Stream the list 344 .map(List::stream) 345 .orElseGet(Stream::empty) 346 // Transform each element of the list to a String 347 .map(Object::toString) 348 // Transform it to LocalDate 349 .map(typedFunction) 350 // Collect 351 .collect(Collectors.toList()); 352 353 // If there are values, update the line 354 if (!newValues.isEmpty()) 355 { 356 resultLine.put(attributeName, newValues); 357 } 358 } 359}