View Javadoc
1   /*
2    * Copyright 2012-2017 CodeLibs Project and the Others.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
13   * either express or implied. See the License for the specific language
14   * governing permissions and limitations under the License.
15   */
16  package org.codelibs.fess.ldap;
17  
18  import static org.codelibs.core.stream.StreamUtil.stream;
19  
20  import java.util.ArrayList;
21  import java.util.Base64;
22  import java.util.Collections;
23  import java.util.Hashtable;
24  import java.util.List;
25  import java.util.Locale;
26  import java.util.function.BiConsumer;
27  import java.util.function.Consumer;
28  import java.util.function.Supplier;
29  import java.util.stream.Collectors;
30  
31  import javax.naming.Context;
32  import javax.naming.NamingException;
33  import javax.naming.directory.Attribute;
34  import javax.naming.directory.Attributes;
35  import javax.naming.directory.BasicAttribute;
36  import javax.naming.directory.BasicAttributes;
37  import javax.naming.directory.DirContext;
38  import javax.naming.directory.InitialDirContext;
39  import javax.naming.directory.ModificationItem;
40  import javax.naming.directory.SearchControls;
41  import javax.naming.directory.SearchResult;
42  
43  import org.codelibs.core.lang.StringUtil;
44  import org.codelibs.fess.Constants;
45  import org.codelibs.fess.entity.FessUser;
46  import org.codelibs.fess.es.user.exentity.Group;
47  import org.codelibs.fess.es.user.exentity.Role;
48  import org.codelibs.fess.es.user.exentity.User;
49  import org.codelibs.fess.exception.LdapOperationException;
50  import org.codelibs.fess.helper.SystemHelper;
51  import org.codelibs.fess.mylasta.direction.FessConfig;
52  import org.codelibs.fess.util.ComponentUtil;
53  import org.codelibs.fess.util.OptionalUtil;
54  import org.dbflute.optional.OptionalEntity;
55  import org.dbflute.util.DfTypeUtil;
56  import org.slf4j.Logger;
57  import org.slf4j.LoggerFactory;
58  
59  public class LdapManager {
60      private static final Logger logger = LoggerFactory.getLogger(LdapManager.class);
61  
62      protected ThreadLocal<DirContextHolder> contextLocal = new ThreadLocal<>();
63  
64      protected volatile boolean isBind = false;
65  
66      protected Hashtable<String, String> createEnvironment(final String initialContextFactory, final String securityAuthentication,
67              final String providerUrl, final String principal, final String credntials) {
68          final Hashtable<String, String> env = new Hashtable<>();
69          env.put(Context.INITIAL_CONTEXT_FACTORY, initialContextFactory);
70          env.put(Context.SECURITY_AUTHENTICATION, securityAuthentication);
71          env.put(Context.PROVIDER_URL, providerUrl);
72          env.put(Context.SECURITY_PRINCIPAL, principal);
73          env.put(Context.SECURITY_CREDENTIALS, credntials);
74          if (providerUrl != null && providerUrl.startsWith("ldaps://")) {
75              env.put(Context.SECURITY_PROTOCOL, "ssl");
76          }
77          return env;
78      }
79  
80      protected Hashtable<String, String> createAdminEnv() {
81          final FessConfig fessConfig = ComponentUtil.getFessConfig();
82          return createEnvironment(//
83                  fessConfig.getLdapInitialContextFactory(), //
84                  fessConfig.getLdapSecurityAuthentication(), fessConfig.getLdapProviderUrl(), //
85                  fessConfig.getLdapAdminSecurityPrincipal(), //
86                  fessConfig.getLdapAdminSecurityCredentials());
87      }
88  
89      protected Hashtable<String, String> createSearchEnv(final String username, final String password) {
90          final FessConfig fessConfig = ComponentUtil.getFessConfig();
91          return createEnvironment(//
92                  fessConfig.getLdapInitialContextFactory(), //
93                  fessConfig.getLdapSecurityAuthentication(), //
94                  fessConfig.getLdapProviderUrl(), //
95                  fessConfig.getLdapSecurityPrincipal(username), password);
96      }
97  
98      protected Hashtable<String, String> createSearchEnv() {
99          final FessConfig fessConfig = ComponentUtil.getFessConfig();
100         return createEnvironment(//
101                 fessConfig.getLdapInitialContextFactory(), //
102                 fessConfig.getLdapSecurityAuthentication(), fessConfig.getLdapProviderUrl(), //
103                 fessConfig.getLdapAdminSecurityPrincipal(), //
104                 fessConfig.getLdapAdminSecurityCredentials());
105     }
106 
107     public void updateConfig() {
108         isBind = false;
109     }
110 
111     protected boolean validate() {
112         if (!isBind) {
113             final Hashtable<String, String> env = createAdminEnv();
114             try (DirContextHolder holder = getDirContext(() -> env)) {
115                 final DirContext context = holder.get();
116                 if (logger.isDebugEnabled()) {
117                     logger.debug("Logged in as Bind DN.", context);
118                 }
119                 isBind = true;
120             } catch (final Exception e) {
121                 logger.warn("LDAP configuration is wrong.", e);
122             }
123         }
124         return isBind;
125     }
126 
127     public OptionalEntity<FessUser> login(final String username, final String password) {
128         final FessConfig fessConfig = ComponentUtil.getFessConfig();
129 
130         if (StringUtil.isBlank(fessConfig.getLdapProviderUrl())) {
131             return OptionalEntity.empty();
132         }
133 
134         if (!validate()) {
135             return OptionalEntity.empty();
136         }
137 
138         final Hashtable<String, String> env = createSearchEnv(username, password);
139         try (DirContextHolder holder = getDirContext(() -> env)) {
140             final DirContext context = holder.get();
141             if (logger.isDebugEnabled()) {
142                 logger.debug("Logged in.", context);
143             }
144             return OptionalEntity.of(createLdapUser(username, env));
145         } catch (final Exception e) {
146             logger.debug("Login failed.", e);
147         }
148         return OptionalEntity.empty();
149     }
150 
151     public OptionalEntity<FessUser> login(final String username) {
152         final Hashtable<String, String> env = createSearchEnv();
153         try (DirContextHolder holder = getDirContext(() -> env)) {
154             final DirContext context = holder.get();
155             if (logger.isDebugEnabled()) {
156                 logger.debug("Logged in.", context);
157             }
158             return OptionalEntity.of(createLdapUser(username, env));
159         } catch (final Exception e) {
160             logger.debug("Login failed.", e);
161         }
162         return OptionalEntity.empty();
163     }
164 
165     protected LdapUser createLdapUser(final String username, final Hashtable<String, String> env) {
166         return new LdapUser(env, username);
167     }
168 
169     public String[] getRoles(final LdapUser ldapUser, final String bindDn, final String accountFilter) {
170         final SystemHelper systemHelper = ComponentUtil.getSystemHelper();
171         final FessConfig fessConfig = ComponentUtil.getFessConfig();
172         final List<String> roleList = new ArrayList<>();
173 
174         if (fessConfig.isLdapRoleSearchUserEnabled()) {
175             roleList.add(systemHelper.getSearchRoleByUser(ldapUser.getName()));
176         }
177 
178         // LDAP: cn=%s
179         // AD: (&(objectClass=user)(sAMAccountName=%s))
180         final String filter = String.format(accountFilter, ldapUser.getName());
181         search(bindDn, filter, new String[] { fessConfig.getLdapMemberofAttribute() }, () -> ldapUser.getEnvironment(), result -> {
182             processSearchRoles(result, (entryDn, name) -> {
183                 final boolean isRole = entryDn.toLowerCase(Locale.ROOT).indexOf("ou=role") != -1;
184                 if (isRole) {
185                     if (fessConfig.isLdapRoleSearchRoleEnabled()) {
186                         roleList.add(systemHelper.getSearchRoleByRole(name));
187                     }
188                 } else if (fessConfig.isLdapRoleSearchGroupEnabled()) {
189                     roleList.add(systemHelper.getSearchRoleByGroup(name));
190                 }
191             });
192         });
193 
194         return roleList.toArray(new String[roleList.size()]);
195     }
196 
197     protected void processSearchRoles(final List<SearchResult> result, final BiConsumer<String, String> consumer) throws NamingException {
198         final FessConfig fessConfig = ComponentUtil.getFessConfig();
199         for (final SearchResult srcrslt : result) {
200             final Attributes attrs = srcrslt.getAttributes();
201 
202             //get group attr
203             final Attribute attr = attrs.get(fessConfig.getLdapMemberofAttribute());
204             if (attr == null) {
205                 continue;
206             }
207 
208             for (int i = 0; i < attr.size(); i++) {
209                 final Object attrValue = attr.get(i);
210                 if (attrValue != null) {
211                     final String entryDn = attrValue.toString();
212 
213                     int start = 0;
214                     int end = 0;
215 
216                     start = entryDn.indexOf("CN=");
217                     if (start < 0) {
218                         start = entryDn.indexOf("cn=");
219                     }
220                     if (start == -1) {
221                         continue;
222                     }
223                     start += 3;
224                     end = entryDn.indexOf(',');
225 
226                     String name;
227                     if (end == -1) {
228                         name = entryDn.substring(start);
229                     } else {
230                         name = entryDn.substring(start, end);
231                     }
232 
233                     consumer.accept(entryDn, name);
234                 }
235             }
236         }
237     }
238 
239     protected void setAttributeValue(final List<SearchResult> result, final String name, final Consumer<Object> consumer) {
240         final List<Object> attrList = getAttributeValueList(result, name);
241         if (!attrList.isEmpty()) {
242             consumer.accept(attrList.get(0));
243         }
244     }
245 
246     protected List<Object> getAttributeValueList(final List<SearchResult> result, final String name) {
247         try {
248             for (final SearchResult srcrslt : result) {
249                 final Attributes attrs = srcrslt.getAttributes();
250 
251                 final Attribute attr = attrs.get(name);
252                 if (attr == null) {
253                     continue;
254                 }
255 
256                 final List<Object> attrList = new ArrayList<>();
257                 for (int i = 0; i < attr.size(); i++) {
258                     final Object attrValue = attr.get(i);
259                     if (attrValue != null) {
260                         attrList.add(attrValue);
261                     }
262                 }
263                 return attrList;
264             }
265             return Collections.emptyList();
266         } catch (final NamingException e) {
267             throw new LdapOperationException("Failed to parse attribute values for " + name, e);
268         }
269     }
270 
271     public void apply(final User user) {
272         final FessConfig fessConfig = ComponentUtil.getFessConfig();
273         if (!fessConfig.isLdapAdminEnabled(user.getName())) {
274             return;
275         }
276 
277         final Supplier<Hashtable<String, String>> adminEnv = () -> createAdminEnv();
278         search(fessConfig.getLdapAdminUserBaseDn(),
279                 fessConfig.getLdapAdminUserFilter(user.getName()),
280                 null,
281                 adminEnv,
282                 result -> {
283                     if (!result.isEmpty()) {
284                         setAttributeValue(result, fessConfig.getLdapAttrSurname(), o -> user.setSurname(o.toString()));
285                         setAttributeValue(result, fessConfig.getLdapAttrGivenName(), o -> user.setGivenName(o.toString()));
286                         setAttributeValue(result, fessConfig.getLdapAttrMail(), o -> user.setMail(o.toString()));
287                         setAttributeValue(result, fessConfig.getLdapAttrEmployeeNumber(), o -> user.setEmployeeNumber(o.toString()));
288                         setAttributeValue(result, fessConfig.getLdapAttrTelephoneNumber(), o -> user.setTelephoneNumber(o.toString()));
289                         setAttributeValue(result, fessConfig.getLdapAttrHomePhone(), o -> user.setHomePhone(o.toString()));
290                         setAttributeValue(result, fessConfig.getLdapAttrHomePostalAddress(), o -> user.setHomePostalAddress(o.toString()));
291                         setAttributeValue(result, fessConfig.getLdapAttrLabeleduri(), o -> user.setLabeledURI(o.toString()));
292                         setAttributeValue(result, fessConfig.getLdapAttrRoomNumber(), o -> user.setRoomNumber(o.toString()));
293                         setAttributeValue(result, fessConfig.getLdapAttrDescription(), o -> user.setDescription(o.toString()));
294                         setAttributeValue(result, fessConfig.getLdapAttrTitle(), o -> user.setTitle(o.toString()));
295                         setAttributeValue(result, fessConfig.getLdapAttrPager(), o -> user.setPager(o.toString()));
296                         setAttributeValue(result, fessConfig.getLdapAttrStreet(), o -> user.setStreet(o.toString()));
297                         setAttributeValue(result, fessConfig.getLdapAttrPostalCode(), o -> user.setPostalCode(o.toString()));
298                         setAttributeValue(result, fessConfig.getLdapAttrPhysicalDeliveryOfficeName(),
299                                 o -> user.setPhysicalDeliveryOfficeName(o.toString()));
300                         setAttributeValue(result, fessConfig.getLdapAttrDestinationIndicator(),
301                                 o -> user.setDestinationIndicator(o.toString()));
302                         setAttributeValue(result, fessConfig.getLdapAttrInternationalisdnNumber(),
303                                 o -> user.setInternationaliSDNNumber(o.toString()));
304                         setAttributeValue(result, fessConfig.getLdapAttrState(), o -> user.setState(o.toString()));
305                         setAttributeValue(result, fessConfig.getLdapAttrEmployeeType(), o -> user.setEmployeeType(o.toString()));
306                         setAttributeValue(result, fessConfig.getLdapAttrFacsimileTelephoneNumber(),
307                                 o -> user.setFacsimileTelephoneNumber(o.toString()));
308                         setAttributeValue(result, fessConfig.getLdapAttrPostOfficeBox(), o -> user.setPostOfficeBox(o.toString()));
309                         setAttributeValue(result, fessConfig.getLdapAttrInitials(), o -> user.setInitials(o.toString()));
310                         setAttributeValue(result, fessConfig.getLdapAttrCarLicense(), o -> user.setCarLicense(o.toString()));
311                         setAttributeValue(result, fessConfig.getLdapAttrMobile(), o -> user.setMobile(o.toString()));
312                         setAttributeValue(result, fessConfig.getLdapAttrPostalAddress(), o -> user.setPostalAddress(o.toString()));
313                         setAttributeValue(result, fessConfig.getLdapAttrCity(), o -> user.setCity(o.toString()));
314                         setAttributeValue(result, fessConfig.getLdapAttrTeletexTerminalIdentifier(),
315                                 o -> user.setTeletexTerminalIdentifier(o.toString()));
316                         setAttributeValue(result, fessConfig.getLdapAttrX121Address(), o -> user.setX121Address(o.toString()));
317                         setAttributeValue(result, fessConfig.getLdapAttrBusinessCategory(), o -> user.setBusinessCategory(o.toString()));
318                         setAttributeValue(result, fessConfig.getLdapAttrRegisteredAddress(), o -> user.setRegisteredAddress(o.toString()));
319                         setAttributeValue(result, fessConfig.getLdapAttrDisplayName(), o -> user.setDisplayName(o.toString()));
320                         setAttributeValue(result, fessConfig.getLdapAttrPreferredLanguage(), o -> user.setPreferredLanguage(o.toString()));
321                         setAttributeValue(result, fessConfig.getLdapAttrDepartmentNumber(), o -> user.setDepartmentNumber(o.toString()));
322                         setAttributeValue(result, fessConfig.getLdapAttrUidNumber(), o -> user.setUidNumber(DfTypeUtil.toLong(o)));
323                         setAttributeValue(result, fessConfig.getLdapAttrGidNumber(), o -> user.setGidNumber(DfTypeUtil.toLong(o)));
324                         setAttributeValue(result, fessConfig.getLdapAttrHomeDirectory(), o -> user.setHomeDirectory(o.toString()));
325                     }
326                 });
327 
328         // groups and roles
329         search(fessConfig.getLdapAdminUserBaseDn(), fessConfig.getLdapAdminUserFilter(user.getName()),
330                 new String[] { fessConfig.getLdapMemberofAttribute() }, adminEnv, result -> {
331                     if (!result.isEmpty()) {
332                         final List<String> groupList = new ArrayList<>();
333                         final List<String> roleList = new ArrayList<>();
334                         final String lowerGroupDn = fessConfig.getLdapAdminGroupBaseDn().toLowerCase(Locale.ROOT);
335                         final String lowerRoleDn = fessConfig.getLdapAdminRoleBaseDn().toLowerCase(Locale.ROOT);
336                         processSearchRoles(result, (entryDn, name) -> {
337                             final String lowerEntryDn = entryDn.toLowerCase(Locale.ROOT);
338                             if (lowerEntryDn.indexOf(lowerGroupDn) != -1) {
339                                 groupList.add(Base64.getUrlEncoder().encodeToString(name.getBytes(Constants.CHARSET_UTF_8)));
340                             } else if (lowerEntryDn.indexOf(lowerRoleDn) != -1) {
341                                 roleList.add(Base64.getUrlEncoder().encodeToString(name.getBytes(Constants.CHARSET_UTF_8)));
342                             }
343                         });
344                         user.setGroups(groupList.toArray(new String[groupList.size()]));
345                         user.setRoles(roleList.toArray(new String[roleList.size()]));
346                     }
347                 });
348 
349     }
350 
351     public void insert(final User user) {
352         final FessConfig fessConfig = ComponentUtil.getFessConfig();
353         if (!fessConfig.isLdapAdminEnabled(user.getName())) {
354             return;
355         }
356 
357         final Supplier<Hashtable<String, String>> adminEnv = () -> createAdminEnv();
358         final String userDN = fessConfig.getLdapAdminUserSecurityPrincipal(user.getName());
359         // attributes
360         search(fessConfig.getLdapAdminUserBaseDn(), fessConfig.getLdapAdminUserFilter(user.getName()), null, adminEnv, result -> {
361             if (!result.isEmpty()) {
362                 modifyUserAttributes(user, adminEnv, userDN, result, fessConfig);
363             } else {
364                 final BasicAttributes entry = new BasicAttributes();
365                 addUserAttributes(entry, user, fessConfig);
366                 final Attribute oc = fessConfig.getLdapAdminUserObjectClassAttribute();
367                 entry.put(oc);
368                 insert(userDN, entry, adminEnv);
369             }
370         });
371 
372         // groups and roles
373         search(fessConfig.getLdapAdminUserBaseDn(),
374                 fessConfig.getLdapAdminUserFilter(user.getName()),
375                 new String[] { fessConfig.getLdapMemberofAttribute() },
376                 adminEnv,
377                 result -> {
378                     if (!result.isEmpty()) {
379                         final List<String> oldGroupList = new ArrayList<>();
380                         final List<String> oldRoleList = new ArrayList<>();
381                         final String lowerGroupDn = fessConfig.getLdapAdminGroupBaseDn().toLowerCase(Locale.ROOT);
382                         final String lowerRoleDn = fessConfig.getLdapAdminRoleBaseDn().toLowerCase(Locale.ROOT);
383                         processSearchRoles(result, (entryDn, name) -> {
384                             final String lowerEntryDn = entryDn.toLowerCase(Locale.ROOT);
385                             if (lowerEntryDn.indexOf(lowerGroupDn) != -1) {
386                                 oldGroupList.add(name);
387                             } else if (lowerEntryDn.indexOf(lowerRoleDn) != -1) {
388                                 oldRoleList.add(name);
389                             }
390                         });
391                         final List<String> newGroupList = stream(user.getGroupNames()).get(stream -> stream.collect(Collectors.toList()));
392                         stream(user.getGroupNames()).of(stream -> stream.forEach(name -> {
393                             if (oldGroupList.contains(name)) {
394                                 oldGroupList.remove(name);
395                                 newGroupList.remove(name);
396                             }
397                         }));
398                         oldGroupList.stream().forEach(
399                                 name -> {
400                                     search(fessConfig.getLdapAdminGroupBaseDn(), fessConfig.getLdapAdminGroupFilter(name), null, adminEnv,
401                                             subResult -> {
402                                                 if (!subResult.isEmpty()) {
403                                                     final List<ModificationItem> modifyList = new ArrayList<>();
404                                                     modifyDeleteEntry(modifyList, "member", userDN);
405                                                     modify(fessConfig.getLdapAdminGroupSecurityPrincipal(name), modifyList, adminEnv);
406                                                 }
407                                             });
408                                 });
409                         newGroupList.stream().forEach(
410                                 name -> {
411                                     search(fessConfig.getLdapAdminGroupBaseDn(), fessConfig.getLdapAdminGroupFilter(name), null, adminEnv,
412                                             subResult -> {
413                                                 if (!!subResult.isEmpty()) {
414                                                     final Group group = new Group();
415                                                     group.setName(name);
416                                                     insert(group);
417                                                 }
418                                                 final List<ModificationItem> modifyList = new ArrayList<>();
419                                                 modifyAddEntry(modifyList, "member", userDN);
420                                                 modify(fessConfig.getLdapAdminGroupSecurityPrincipal(name), modifyList, adminEnv);
421                                             });
422                                 });
423 
424                         final List<String> newRoleList = stream(user.getRoleNames()).get(stream -> stream.collect(Collectors.toList()));
425                         stream(user.getRoleNames()).of(stream -> stream.forEach(name -> {
426                             if (oldRoleList.contains(name)) {
427                                 oldRoleList.remove(name);
428                                 newRoleList.remove(name);
429                             }
430                         }));
431                         oldRoleList.stream().forEach(
432                                 name -> {
433                                     search(fessConfig.getLdapAdminRoleBaseDn(), fessConfig.getLdapAdminRoleFilter(name), null, adminEnv,
434                                             subResult -> {
435                                                 if (!subResult.isEmpty()) {
436                                                     final List<ModificationItem> modifyList = new ArrayList<>();
437                                                     modifyDeleteEntry(modifyList, "member", userDN);
438                                                     modify(fessConfig.getLdapAdminRoleSecurityPrincipal(name), modifyList, adminEnv);
439                                                 }
440                                             });
441                                 });
442                         newRoleList.stream().forEach(
443                                 name -> {
444                                     search(fessConfig.getLdapAdminRoleBaseDn(), fessConfig.getLdapAdminRoleFilter(name), null, adminEnv,
445                                             subResult -> {
446                                                 if (!!subResult.isEmpty()) {
447                                                     final Role role = new Role();
448                                                     role.setName(name);
449                                                     insert(role);
450                                                 }
451                                                 final List<ModificationItem> modifyList = new ArrayList<>();
452                                                 modifyAddEntry(modifyList, "member", userDN);
453                                                 modify(fessConfig.getLdapAdminRoleSecurityPrincipal(name), modifyList, adminEnv);
454                                             });
455                                 });
456                     } else {
457                         stream(user.getGroupNames()).of(
458                                 stream -> stream.forEach(name -> {
459                                     search(fessConfig.getLdapAdminGroupBaseDn(), fessConfig.getLdapAdminGroupFilter(name), null, adminEnv,
460                                             subResult -> {
461                                                 if (!!subResult.isEmpty()) {
462                                                     final Group group = new Group();
463                                                     group.setName(name);
464                                                     insert(group);
465                                                 }
466                                                 final List<ModificationItem> modifyList = new ArrayList<>();
467                                                 modifyAddEntry(modifyList, "member", userDN);
468                                                 modify(fessConfig.getLdapAdminGroupSecurityPrincipal(name), modifyList, adminEnv);
469                                             });
470                                 }));
471 
472                         stream(user.getRoleNames()).of(
473                                 stream -> stream.forEach(name -> {
474                                     search(fessConfig.getLdapAdminRoleBaseDn(), fessConfig.getLdapAdminRoleFilter(name), null, adminEnv,
475                                             subResult -> {
476                                                 if (!!subResult.isEmpty()) {
477                                                     final Role role = new Role();
478                                                     role.setName(name);
479                                                     insert(role);
480                                                 }
481                                                 final List<ModificationItem> modifyList = new ArrayList<>();
482                                                 modifyAddEntry(modifyList, "member", userDN);
483                                                 modify(fessConfig.getLdapAdminRoleSecurityPrincipal(name), modifyList, adminEnv);
484                                             });
485                                 }));
486                     }
487                 });
488 
489     }
490 
491     protected void modifyUserAttributes(final User user, final Supplier<Hashtable<String, String>> adminEnv, final String userDN,
492             final List<SearchResult> result, final FessConfig fessConfig) {
493         final List<ModificationItem> modifyList = new ArrayList<>();
494         if (user.getOriginalPassword() != null) {
495             modifyReplaceEntry(modifyList, "userPassword", user.getOriginalPassword());
496         }
497 
498         final String attrSurname = fessConfig.getLdapAttrSurname();
499         OptionalUtil
500                 .ofNullable(user.getSurname())
501                 .filter(StringUtil::isNotBlank)
502                 .ifPresent(s -> modifyReplaceEntry(modifyList, attrSurname, s))
503                 .orElse(() -> getAttributeValueList(result, attrSurname).stream().forEach(
504                         v -> modifyDeleteEntry(modifyList, attrSurname, v)));
505         final String attrGivenName = fessConfig.getLdapAttrGivenName();
506         OptionalUtil
507                 .ofNullable(user.getGivenName())
508                 .filter(StringUtil::isNotBlank)
509                 .ifPresent(s -> modifyReplaceEntry(modifyList, attrGivenName, s))
510                 .orElse(() -> getAttributeValueList(result, attrGivenName).stream().forEach(
511                         v -> modifyDeleteEntry(modifyList, attrGivenName, v)));
512         final String attrMail = fessConfig.getLdapAttrMail();
513         OptionalUtil.ofNullable(user.getMail()).filter(StringUtil::isNotBlank).ifPresent(s -> modifyReplaceEntry(modifyList, attrMail, s))
514                 .orElse(() -> getAttributeValueList(result, attrMail).stream().forEach(v -> modifyDeleteEntry(modifyList, attrMail, v)));
515         final String attrEmployeeNumber = fessConfig.getLdapAttrEmployeeNumber();
516         OptionalUtil
517                 .ofNullable(user.getEmployeeNumber())
518                 .filter(StringUtil::isNotBlank)
519                 .ifPresent(s -> modifyReplaceEntry(modifyList, attrEmployeeNumber, s))
520                 .orElse(() -> getAttributeValueList(result, attrEmployeeNumber).stream().forEach(
521                         v -> modifyDeleteEntry(modifyList, attrEmployeeNumber, v)));
522         final String attrTelephoneNumber = fessConfig.getLdapAttrTelephoneNumber();
523         OptionalUtil
524                 .ofNullable(user.getTelephoneNumber())
525                 .filter(StringUtil::isNotBlank)
526                 .ifPresent(s -> modifyReplaceEntry(modifyList, attrTelephoneNumber, s))
527                 .orElse(() -> getAttributeValueList(result, attrTelephoneNumber).stream().forEach(
528                         v -> modifyDeleteEntry(modifyList, attrTelephoneNumber, v)));
529         final String attrHomePhone = fessConfig.getLdapAttrHomePhone();
530         OptionalUtil
531                 .ofNullable(user.getHomePhone())
532                 .filter(StringUtil::isNotBlank)
533                 .ifPresent(s -> modifyReplaceEntry(modifyList, attrHomePhone, s))
534                 .orElse(() -> getAttributeValueList(result, attrHomePhone).stream().forEach(
535                         v -> modifyDeleteEntry(modifyList, attrHomePhone, v)));
536         final String attrHomePostalAddress = fessConfig.getLdapAttrHomePostalAddress();
537         OptionalUtil
538                 .ofNullable(user.getHomePostalAddress())
539                 .filter(StringUtil::isNotBlank)
540                 .ifPresent(s -> modifyReplaceEntry(modifyList, attrHomePostalAddress, s))
541                 .orElse(() -> getAttributeValueList(result, attrHomePostalAddress).stream().forEach(
542                         v -> modifyDeleteEntry(modifyList, attrHomePostalAddress, v)));
543         final String attrLabeledURI = fessConfig.getLdapAttrLabeleduri();
544         OptionalUtil
545                 .ofNullable(user.getLabeledURI())
546                 .filter(StringUtil::isNotBlank)
547                 .ifPresent(s -> modifyReplaceEntry(modifyList, attrLabeledURI, s))
548                 .orElse(() -> getAttributeValueList(result, attrLabeledURI).stream().forEach(
549                         v -> modifyDeleteEntry(modifyList, attrLabeledURI, v)));
550         final String attrRoomNumber = fessConfig.getLdapAttrRoomNumber();
551         OptionalUtil
552                 .ofNullable(user.getRoomNumber())
553                 .filter(StringUtil::isNotBlank)
554                 .ifPresent(s -> modifyReplaceEntry(modifyList, attrRoomNumber, s))
555                 .orElse(() -> getAttributeValueList(result, attrRoomNumber).stream().forEach(
556                         v -> modifyDeleteEntry(modifyList, attrRoomNumber, v)));
557         final String attrDescription = fessConfig.getLdapAttrDescription();
558         OptionalUtil
559                 .ofNullable(user.getDescription())
560                 .filter(StringUtil::isNotBlank)
561                 .ifPresent(s -> modifyReplaceEntry(modifyList, attrDescription, s))
562                 .orElse(() -> getAttributeValueList(result, attrDescription).stream().forEach(
563                         v -> modifyDeleteEntry(modifyList, attrDescription, v)));
564         final String attrTitle = fessConfig.getLdapAttrTitle();
565         OptionalUtil.ofNullable(user.getTitle()).filter(StringUtil::isNotBlank)
566                 .ifPresent(s -> modifyReplaceEntry(modifyList, attrTitle, s))
567                 .orElse(() -> getAttributeValueList(result, attrTitle).stream().forEach(v -> modifyDeleteEntry(modifyList, attrTitle, v)));
568         final String attrPager = fessConfig.getLdapAttrPager();
569         OptionalUtil.ofNullable(user.getPager()).filter(StringUtil::isNotBlank)
570                 .ifPresent(s -> modifyReplaceEntry(modifyList, attrPager, s))
571                 .orElse(() -> getAttributeValueList(result, attrPager).stream().forEach(v -> modifyDeleteEntry(modifyList, attrPager, v)));
572         final String attrStreet = fessConfig.getLdapAttrStreet();
573         OptionalUtil
574                 .ofNullable(user.getStreet())
575                 .filter(StringUtil::isNotBlank)
576                 .ifPresent(s -> modifyReplaceEntry(modifyList, attrStreet, s))
577                 .orElse(() -> getAttributeValueList(result, attrStreet).stream().forEach(v -> modifyDeleteEntry(modifyList, attrStreet, v)));
578         final String attrPostalCode = fessConfig.getLdapAttrPostalCode();
579         OptionalUtil
580                 .ofNullable(user.getPostalCode())
581                 .filter(StringUtil::isNotBlank)
582                 .ifPresent(s -> modifyReplaceEntry(modifyList, attrPostalCode, s))
583                 .orElse(() -> getAttributeValueList(result, attrPostalCode).stream().forEach(
584                         v -> modifyDeleteEntry(modifyList, attrPostalCode, v)));
585         final String attrPhysicalDeliveryOfficeName = fessConfig.getLdapAttrPhysicalDeliveryOfficeName();
586         OptionalUtil
587                 .ofNullable(user.getPhysicalDeliveryOfficeName())
588                 .filter(StringUtil::isNotBlank)
589                 .ifPresent(s -> modifyReplaceEntry(modifyList, attrPhysicalDeliveryOfficeName, s))
590                 .orElse(() -> getAttributeValueList(result, attrPhysicalDeliveryOfficeName).stream().forEach(
591                         v -> modifyDeleteEntry(modifyList, attrPhysicalDeliveryOfficeName, v)));
592         final String attrDestinationIndicator = fessConfig.getLdapAttrDestinationIndicator();
593         OptionalUtil
594                 .ofNullable(user.getDestinationIndicator())
595                 .filter(StringUtil::isNotBlank)
596                 .ifPresent(s -> modifyReplaceEntry(modifyList, attrDestinationIndicator, s))
597                 .orElse(() -> getAttributeValueList(result, attrDestinationIndicator).stream().forEach(
598                         v -> modifyDeleteEntry(modifyList, attrDestinationIndicator, v)));
599         final String attrInternationaliSDNNumber = fessConfig.getLdapAttrInternationalisdnNumber();
600         OptionalUtil
601                 .ofNullable(user.getInternationaliSDNNumber())
602                 .filter(StringUtil::isNotBlank)
603                 .ifPresent(s -> modifyReplaceEntry(modifyList, attrInternationaliSDNNumber, s))
604                 .orElse(() -> getAttributeValueList(result, attrInternationaliSDNNumber).stream().forEach(
605                         v -> modifyDeleteEntry(modifyList, attrInternationaliSDNNumber, v)));
606         final String attrState = fessConfig.getLdapAttrState();
607         OptionalUtil.ofNullable(user.getState()).filter(StringUtil::isNotBlank)
608                 .ifPresent(s -> modifyReplaceEntry(modifyList, attrState, s))
609                 .orElse(() -> getAttributeValueList(result, attrState).stream().forEach(v -> modifyDeleteEntry(modifyList, attrState, v)));
610         final String attrEmployeeType = fessConfig.getLdapAttrEmployeeType();
611         OptionalUtil
612                 .ofNullable(user.getEmployeeType())
613                 .filter(StringUtil::isNotBlank)
614                 .ifPresent(s -> modifyReplaceEntry(modifyList, attrEmployeeType, s))
615                 .orElse(() -> getAttributeValueList(result, attrEmployeeType).stream().forEach(
616                         v -> modifyDeleteEntry(modifyList, attrEmployeeType, v)));
617         final String attrFacsimileTelephoneNumber = fessConfig.getLdapAttrFacsimileTelephoneNumber();
618         OptionalUtil
619                 .ofNullable(user.getFacsimileTelephoneNumber())
620                 .filter(StringUtil::isNotBlank)
621                 .ifPresent(s -> modifyReplaceEntry(modifyList, attrFacsimileTelephoneNumber, s))
622                 .orElse(() -> getAttributeValueList(result, attrFacsimileTelephoneNumber).stream().forEach(
623                         v -> modifyDeleteEntry(modifyList, attrFacsimileTelephoneNumber, v)));
624         final String attrPostOfficeBox = fessConfig.getLdapAttrPostOfficeBox();
625         OptionalUtil
626                 .ofNullable(user.getPostOfficeBox())
627                 .filter(StringUtil::isNotBlank)
628                 .ifPresent(s -> modifyReplaceEntry(modifyList, attrPostOfficeBox, s))
629                 .orElse(() -> getAttributeValueList(result, attrPostOfficeBox).stream().forEach(
630                         v -> modifyDeleteEntry(modifyList, attrPostOfficeBox, v)));
631         final String attrInitials = fessConfig.getLdapAttrInitials();
632         OptionalUtil
633                 .ofNullable(user.getInitials())
634                 .filter(StringUtil::isNotBlank)
635                 .ifPresent(s -> modifyReplaceEntry(modifyList, attrInitials, s))
636                 .orElse(() -> getAttributeValueList(result, attrInitials).stream().forEach(
637                         v -> modifyDeleteEntry(modifyList, attrInitials, v)));
638         final String attrCarLicense = fessConfig.getLdapAttrCarLicense();
639         OptionalUtil
640                 .ofNullable(user.getCarLicense())
641                 .filter(StringUtil::isNotBlank)
642                 .ifPresent(s -> modifyReplaceEntry(modifyList, attrCarLicense, s))
643                 .orElse(() -> getAttributeValueList(result, attrCarLicense).stream().forEach(
644                         v -> modifyDeleteEntry(modifyList, attrCarLicense, v)));
645         final String attrMobile = fessConfig.getLdapAttrMobile();
646         OptionalUtil
647                 .ofNullable(user.getMobile())
648                 .filter(StringUtil::isNotBlank)
649                 .ifPresent(s -> modifyReplaceEntry(modifyList, attrMobile, s))
650                 .orElse(() -> getAttributeValueList(result, attrMobile).stream().forEach(v -> modifyDeleteEntry(modifyList, attrMobile, v)));
651         final String attrPostalAddress = fessConfig.getLdapAttrPostalAddress();
652         OptionalUtil
653                 .ofNullable(user.getPostalAddress())
654                 .filter(StringUtil::isNotBlank)
655                 .ifPresent(s -> modifyReplaceEntry(modifyList, attrPostalAddress, s))
656                 .orElse(() -> getAttributeValueList(result, attrPostalAddress).stream().forEach(
657                         v -> modifyDeleteEntry(modifyList, attrPostalAddress, v)));
658         final String attrCity = fessConfig.getLdapAttrCity();
659         OptionalUtil.ofNullable(user.getCity()).filter(StringUtil::isNotBlank).ifPresent(s -> modifyReplaceEntry(modifyList, attrCity, s))
660                 .orElse(() -> getAttributeValueList(result, attrCity).stream().forEach(v -> modifyDeleteEntry(modifyList, attrCity, v)));
661         final String attrTeletexTerminalIdentifier = fessConfig.getLdapAttrTeletexTerminalIdentifier();
662         OptionalUtil
663                 .ofNullable(user.getTeletexTerminalIdentifier())
664                 .filter(StringUtil::isNotBlank)
665                 .ifPresent(s -> modifyReplaceEntry(modifyList, attrTeletexTerminalIdentifier, s))
666                 .orElse(() -> getAttributeValueList(result, attrTeletexTerminalIdentifier).stream().forEach(
667                         v -> modifyDeleteEntry(modifyList, attrTeletexTerminalIdentifier, v)));
668         final String attrX121Address = fessConfig.getLdapAttrX121Address();
669         OptionalUtil
670                 .ofNullable(user.getX121Address())
671                 .filter(StringUtil::isNotBlank)
672                 .ifPresent(s -> modifyReplaceEntry(modifyList, attrX121Address, s))
673                 .orElse(() -> getAttributeValueList(result, attrX121Address).stream().forEach(
674                         v -> modifyDeleteEntry(modifyList, attrX121Address, v)));
675         final String attrBusinessCategory = fessConfig.getLdapAttrBusinessCategory();
676         OptionalUtil
677                 .ofNullable(user.getBusinessCategory())
678                 .filter(StringUtil::isNotBlank)
679                 .ifPresent(s -> modifyReplaceEntry(modifyList, attrBusinessCategory, s))
680                 .orElse(() -> getAttributeValueList(result, attrBusinessCategory).stream().forEach(
681                         v -> modifyDeleteEntry(modifyList, attrBusinessCategory, v)));
682         final String attrRegisteredAddress = fessConfig.getLdapAttrRegisteredAddress();
683         OptionalUtil
684                 .ofNullable(user.getRegisteredAddress())
685                 .filter(StringUtil::isNotBlank)
686                 .ifPresent(s -> modifyReplaceEntry(modifyList, attrRegisteredAddress, s))
687                 .orElse(() -> getAttributeValueList(result, attrRegisteredAddress).stream().forEach(
688                         v -> modifyDeleteEntry(modifyList, attrRegisteredAddress, v)));
689         final String attrDisplayName = fessConfig.getLdapAttrDisplayName();
690         OptionalUtil
691                 .ofNullable(user.getDisplayName())
692                 .filter(StringUtil::isNotBlank)
693                 .ifPresent(s -> modifyReplaceEntry(modifyList, attrDisplayName, s))
694                 .orElse(() -> getAttributeValueList(result, attrDisplayName).stream().forEach(
695                         v -> modifyDeleteEntry(modifyList, attrDisplayName, v)));
696         final String attrPreferredLanguage = fessConfig.getLdapAttrPreferredLanguage();
697         OptionalUtil
698                 .ofNullable(user.getPreferredLanguage())
699                 .filter(StringUtil::isNotBlank)
700                 .ifPresent(s -> modifyReplaceEntry(modifyList, attrPreferredLanguage, s))
701                 .orElse(() -> getAttributeValueList(result, attrPreferredLanguage).stream().forEach(
702                         v -> modifyDeleteEntry(modifyList, attrPreferredLanguage, v)));
703         final String attrDepartmentNumber = fessConfig.getLdapAttrDepartmentNumber();
704         OptionalUtil
705                 .ofNullable(user.getDepartmentNumber())
706                 .filter(StringUtil::isNotBlank)
707                 .ifPresent(s -> modifyReplaceEntry(modifyList, attrDepartmentNumber, s))
708                 .orElse(() -> getAttributeValueList(result, attrDepartmentNumber).stream().forEach(
709                         v -> modifyDeleteEntry(modifyList, attrDepartmentNumber, v)));
710         final String attrUidNumber = fessConfig.getLdapAttrUidNumber();
711         OptionalUtil
712                 .ofNullable(user.getUidNumber())
713                 .filter(s -> StringUtil.isNotBlank(s.toString()))
714                 .ifPresent(s -> modifyReplaceEntry(modifyList, attrUidNumber, s.toString()))
715                 .orElse(() -> getAttributeValueList(result, attrUidNumber).stream().forEach(
716                         v -> modifyDeleteEntry(modifyList, attrUidNumber, v)));
717         final String attrGidNumber = fessConfig.getLdapAttrGidNumber();
718         OptionalUtil
719                 .ofNullable(user.getGidNumber())
720                 .filter(s -> StringUtil.isNotBlank(s.toString()))
721                 .ifPresent(s -> modifyReplaceEntry(modifyList, attrGidNumber, s.toString()))
722                 .orElse(() -> getAttributeValueList(result, attrGidNumber).stream().forEach(
723                         v -> modifyDeleteEntry(modifyList, attrGidNumber, v)));
724         final String attrHomeDirectory = fessConfig.getLdapAttrHomeDirectory();
725         OptionalUtil
726                 .ofNullable(user.getHomeDirectory())
727                 .filter(StringUtil::isNotBlank)
728                 .ifPresent(s -> modifyReplaceEntry(modifyList, attrHomeDirectory, s))
729                 .orElse(() -> getAttributeValueList(result, attrHomeDirectory).stream().forEach(
730                         v -> modifyDeleteEntry(modifyList, attrHomeDirectory, v)));
731 
732         modify(userDN, modifyList, adminEnv);
733     }
734 
735     protected void addUserAttributes(final BasicAttributes entry, final User user, final FessConfig fessConfig) {
736         entry.put(new BasicAttribute("cn", user.getName()));
737         entry.put(new BasicAttribute("userPassword", user.getOriginalPassword()));
738 
739         OptionalUtil.ofNullable(user.getSurname()).filter(StringUtil::isNotBlank)
740                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrSurname(), s)));
741         OptionalUtil.ofNullable(user.getGivenName()).filter(StringUtil::isNotBlank)
742                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrGivenName(), s)));
743         OptionalUtil.ofNullable(user.getMail()).filter(StringUtil::isNotBlank)
744                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrMail(), s)));
745         OptionalUtil.ofNullable(user.getEmployeeNumber()).filter(StringUtil::isNotBlank)
746                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrEmployeeNumber(), s)));
747         OptionalUtil.ofNullable(user.getTelephoneNumber()).filter(StringUtil::isNotBlank)
748                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrTelephoneNumber(), s)));
749         OptionalUtil.ofNullable(user.getHomePhone()).filter(StringUtil::isNotBlank)
750                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrHomePhone(), s)));
751         OptionalUtil.ofNullable(user.getHomePostalAddress()).filter(StringUtil::isNotBlank)
752                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrHomePostalAddress(), s)));
753         OptionalUtil.ofNullable(user.getLabeledURI()).filter(StringUtil::isNotBlank)
754                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrLabeleduri(), s)));
755         OptionalUtil.ofNullable(user.getRoomNumber()).filter(StringUtil::isNotBlank)
756                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrRoomNumber(), s)));
757         OptionalUtil.ofNullable(user.getDescription()).filter(StringUtil::isNotBlank)
758                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrDescription(), s)));
759         OptionalUtil.ofNullable(user.getTitle()).filter(StringUtil::isNotBlank)
760                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrTitle(), s)));
761         OptionalUtil.ofNullable(user.getPager()).filter(StringUtil::isNotBlank)
762                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrPager(), s)));
763         OptionalUtil.ofNullable(user.getStreet()).filter(StringUtil::isNotBlank)
764                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrStreet(), s)));
765         OptionalUtil.ofNullable(user.getPostalCode()).filter(StringUtil::isNotBlank)
766                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrPostalCode(), s)));
767         OptionalUtil.ofNullable(user.getPhysicalDeliveryOfficeName()).filter(StringUtil::isNotBlank)
768                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrPhysicalDeliveryOfficeName(), s)));
769         OptionalUtil.ofNullable(user.getDestinationIndicator()).filter(StringUtil::isNotBlank)
770                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrDestinationIndicator(), s)));
771         OptionalUtil.ofNullable(user.getInternationaliSDNNumber()).filter(StringUtil::isNotBlank)
772                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrInternationalisdnNumber(), s)));
773         OptionalUtil.ofNullable(user.getState()).filter(StringUtil::isNotBlank)
774                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrState(), s)));
775         OptionalUtil.ofNullable(user.getEmployeeType()).filter(StringUtil::isNotBlank)
776                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrEmployeeType(), s)));
777         OptionalUtil.ofNullable(user.getFacsimileTelephoneNumber()).filter(StringUtil::isNotBlank)
778                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrFacsimileTelephoneNumber(), s)));
779         OptionalUtil.ofNullable(user.getPostOfficeBox()).filter(StringUtil::isNotBlank)
780                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrPostOfficeBox(), s)));
781         OptionalUtil.ofNullable(user.getInitials()).filter(StringUtil::isNotBlank)
782                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrInitials(), s)));
783         OptionalUtil.ofNullable(user.getCarLicense()).filter(StringUtil::isNotBlank)
784                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrCarLicense(), s)));
785         OptionalUtil.ofNullable(user.getMobile()).filter(StringUtil::isNotBlank)
786                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrMobile(), s)));
787         OptionalUtil.ofNullable(user.getPostalAddress()).filter(StringUtil::isNotBlank)
788                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrPostalAddress(), s)));
789         OptionalUtil.ofNullable(user.getCity()).filter(StringUtil::isNotBlank)
790                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrCity(), s)));
791         OptionalUtil.ofNullable(user.getTeletexTerminalIdentifier()).filter(StringUtil::isNotBlank)
792                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrTeletexTerminalIdentifier(), s)));
793         OptionalUtil.ofNullable(user.getX121Address()).filter(StringUtil::isNotBlank)
794                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrX121Address(), s)));
795         OptionalUtil.ofNullable(user.getBusinessCategory()).filter(StringUtil::isNotBlank)
796                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrBusinessCategory(), s)));
797         OptionalUtil.ofNullable(user.getRegisteredAddress()).filter(StringUtil::isNotBlank)
798                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrRegisteredAddress(), s)));
799         OptionalUtil.ofNullable(user.getDisplayName()).filter(StringUtil::isNotBlank)
800                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrDisplayName(), s)));
801         OptionalUtil.ofNullable(user.getPreferredLanguage()).filter(StringUtil::isNotBlank)
802                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrPreferredLanguage(), s)));
803         OptionalUtil.ofNullable(user.getDepartmentNumber()).filter(StringUtil::isNotBlank)
804                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrDepartmentNumber(), s)));
805         OptionalUtil.ofNullable(user.getUidNumber()).filter(s -> StringUtil.isNotBlank(s.toString()))
806                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrUidNumber(), s)));
807         OptionalUtil.ofNullable(user.getGidNumber()).filter(s -> StringUtil.isNotBlank(s.toString()))
808                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrGidNumber(), s)));
809         OptionalUtil.ofNullable(user.getHomeDirectory()).filter(StringUtil::isNotBlank)
810                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrHomeDirectory(), s)));
811     }
812 
813     public void delete(final User user) {
814         final FessConfig fessConfig = ComponentUtil.getFessConfig();
815         if (!fessConfig.isLdapAdminEnabled(user.getName())) {
816             return;
817         }
818 
819         final Supplier<Hashtable<String, String>> adminEnv = () -> createAdminEnv();
820         final String userDN = fessConfig.getLdapAdminUserSecurityPrincipal(user.getName());
821 
822         stream(user.getGroupNames()).of(stream -> stream.forEach(name -> {
823             search(fessConfig.getLdapAdminGroupBaseDn(), fessConfig.getLdapAdminGroupFilter(name), null, adminEnv, subResult -> {
824                 if (!!subResult.isEmpty()) {
825                     final Group group = new Group();
826                     group.setName(name);
827                     insert(group);
828                 }
829                 final List<ModificationItem> modifyList = new ArrayList<>();
830                 modifyDeleteEntry(modifyList, "member", userDN);
831                 modify(fessConfig.getLdapAdminGroupSecurityPrincipal(name), modifyList, adminEnv);
832             });
833         }));
834         stream(user.getRoleNames()).of(stream -> stream.forEach(name -> {
835             search(fessConfig.getLdapAdminRoleBaseDn(), fessConfig.getLdapAdminRoleFilter(name), null, adminEnv, subResult -> {
836                 if (!!subResult.isEmpty()) {
837                     final Role role = new Role();
838                     role.setName(name);
839                     insert(role);
840                 }
841                 final List<ModificationItem> modifyList = new ArrayList<>();
842                 modifyDeleteEntry(modifyList, "member", userDN);
843                 modify(fessConfig.getLdapAdminRoleSecurityPrincipal(name), modifyList, adminEnv);
844             });
845         }));
846 
847         search(fessConfig.getLdapAdminUserBaseDn(), fessConfig.getLdapAdminUserFilter(user.getName()), null, adminEnv, result -> {
848             if (!result.isEmpty()) {
849                 delete(userDN, adminEnv);
850             } else {
851                 logger.info("{} does not exist in LDAP server.", user.getName());
852             }
853         });
854 
855     }
856 
857     public void insert(final Role role) {
858         final FessConfig fessConfig = ComponentUtil.getFessConfig();
859         if (!fessConfig.isLdapAdminEnabled()) {
860             return;
861         }
862 
863         final Supplier<Hashtable<String, String>> adminEnv = () -> createAdminEnv();
864         search(fessConfig.getLdapAdminRoleBaseDn(), fessConfig.getLdapAdminRoleFilter(role.getName()), null, adminEnv, result -> {
865             if (!result.isEmpty()) {
866                 logger.info("{} exists in LDAP server.", role.getName());
867             } else {
868                 final String entryDN = fessConfig.getLdapAdminRoleSecurityPrincipal(role.getName());
869                 final BasicAttributes entry = new BasicAttributes();
870                 addRoleAttributes(entry, role, fessConfig);
871                 final Attribute oc = fessConfig.getLdapAdminRoleObjectClassAttribute();
872                 entry.put(oc);
873                 insert(entryDN, entry, adminEnv);
874             }
875         });
876 
877     }
878 
879     protected void addRoleAttributes(final BasicAttributes entry, final Role user, final FessConfig fessConfig) {
880         // nothing
881     }
882 
883     public void delete(final Role role) {
884         final FessConfig fessConfig = ComponentUtil.getFessConfig();
885         if (!fessConfig.isLdapAdminEnabled()) {
886             return;
887         }
888 
889         final Supplier<Hashtable<String, String>> adminEnv = () -> createAdminEnv();
890         search(fessConfig.getLdapAdminRoleBaseDn(), fessConfig.getLdapAdminRoleFilter(role.getName()), null, adminEnv, result -> {
891             if (!result.isEmpty()) {
892                 final String entryDN = fessConfig.getLdapAdminRoleSecurityPrincipal(role.getName());
893                 delete(entryDN, adminEnv);
894             } else {
895                 logger.info("{} does not exist in LDAP server.", role.getName());
896             }
897         });
898 
899     }
900 
901     public void apply(final Group group) {
902         final FessConfig fessConfig = ComponentUtil.getFessConfig();
903         if (!fessConfig.isLdapAdminEnabled()) {
904             return;
905         }
906 
907         final Supplier<Hashtable<String, String>> adminEnv = () -> createAdminEnv();
908         search(fessConfig.getLdapAdminGroupBaseDn(), fessConfig.getLdapAdminGroupFilter(group.getName()), null, adminEnv, result -> {
909             if (!result.isEmpty()) {
910                 setAttributeValue(result, fessConfig.getLdapAttrGidNumber(), o -> group.setGidNumber(DfTypeUtil.toLong(o)));
911             }
912         });
913     }
914 
915     public void insert(final Group group) {
916         final FessConfig fessConfig = ComponentUtil.getFessConfig();
917         if (!fessConfig.isLdapAdminEnabled()) {
918             return;
919         }
920 
921         final Supplier<Hashtable<String, String>> adminEnv = () -> createAdminEnv();
922         final String entryDN = fessConfig.getLdapAdminGroupSecurityPrincipal(group.getName());
923         search(fessConfig.getLdapAdminGroupBaseDn(), fessConfig.getLdapAdminGroupFilter(group.getName()), null, adminEnv, result -> {
924             if (!result.isEmpty()) {
925                 logger.info("{} exists in LDAP server.", group.getName());
926                 modifyGroupAttributes(group, adminEnv, entryDN, result, fessConfig);
927             } else {
928                 final BasicAttributes entry = new BasicAttributes();
929                 addGroupAttributes(entry, group, fessConfig);
930                 final Attribute oc = fessConfig.getLdapAdminGroupObjectClassAttribute();
931                 entry.put(oc);
932                 insert(entryDN, entry, adminEnv);
933             }
934         });
935     }
936 
937     protected void modifyGroupAttributes(final Group group, final Supplier<Hashtable<String, String>> adminEnv, final String entryDN,
938             final List<SearchResult> result, final FessConfig fessConfig) {
939         final List<ModificationItem> modifyList = new ArrayList<>();
940 
941         final String attrGidNumber = fessConfig.getLdapAttrGidNumber();
942         OptionalUtil
943                 .ofNullable(group.getGidNumber())
944                 .filter(s -> StringUtil.isNotBlank(s.toString()))
945                 .ifPresent(s -> modifyReplaceEntry(modifyList, attrGidNumber, s.toString()))
946                 .orElse(() -> getAttributeValueList(result, attrGidNumber).stream().forEach(
947                         v -> modifyDeleteEntry(modifyList, attrGidNumber, v)));
948 
949         modify(entryDN, modifyList, adminEnv);
950     }
951 
952     protected void addGroupAttributes(final BasicAttributes entry, final Group group, final FessConfig fessConfig) {
953         OptionalUtil.ofNullable(group.getGidNumber()).filter(s -> StringUtil.isNotBlank(s.toString()))
954                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrGidNumber(), s)));
955     }
956 
957     public void delete(final Group group) {
958         final FessConfig fessConfig = ComponentUtil.getFessConfig();
959         if (!fessConfig.isLdapAdminEnabled()) {
960             return;
961         }
962 
963         final Supplier<Hashtable<String, String>> adminEnv = () -> createAdminEnv();
964         search(fessConfig.getLdapAdminGroupBaseDn(), fessConfig.getLdapAdminGroupFilter(group.getName()), null, adminEnv, result -> {
965             if (!result.isEmpty()) {
966                 final String entryDN = fessConfig.getLdapAdminGroupSecurityPrincipal(group.getName());
967                 delete(entryDN, adminEnv);
968             } else {
969                 logger.info("{} does not exist in LDAP server.", group.getName());
970             }
971         });
972     }
973 
974     public boolean changePassword(final String username, final String password) {
975         final FessConfig fessConfig = ComponentUtil.getFessConfig();
976         if (!fessConfig.isLdapAdminEnabled(username)) {
977             return false;
978         }
979 
980         final Supplier<Hashtable<String, String>> adminEnv = () -> createAdminEnv();
981         final String userDN = fessConfig.getLdapAdminUserSecurityPrincipal(username);
982         search(fessConfig.getLdapAdminUserBaseDn(), fessConfig.getLdapAdminUserFilter(username), null, adminEnv, result -> {
983             if (!result.isEmpty()) {
984                 final List<ModificationItem> modifyList = new ArrayList<>();
985                 modifyReplaceEntry(modifyList, "userPassword", password);
986                 modify(userDN, modifyList, adminEnv);
987             } else {
988                 throw new LdapOperationException("User is not found: " + username);
989             }
990         });
991         return true;
992     }
993 
994     protected void insert(final String entryDN, final Attributes entry, final Supplier<Hashtable<String, String>> envSupplier) {
995         try (DirContextHolder holder = getDirContext(envSupplier)) {
996             logger.debug("Inserting {}", entryDN);
997             holder.get().createSubcontext(entryDN, entry);
998         } catch (final NamingException e) {
999             throw new LdapOperationException("Failed to add " + entryDN, e);
1000         }
1001     }
1002 
1003     protected void delete(final String entryDN, final Supplier<Hashtable<String, String>> envSupplier) {
1004         try (DirContextHolder holder = getDirContext(envSupplier)) {
1005             logger.debug("Deleting {}", entryDN);
1006             holder.get().destroySubcontext(entryDN);
1007         } catch (final NamingException e) {
1008             throw new LdapOperationException("Failed to delete " + entryDN, e);
1009         }
1010     }
1011 
1012     protected void search(final String baseDn, final String filter, final String[] returningAttrs,
1013             final Supplier<Hashtable<String, String>> envSupplier, final SearcConsumer consumer) {
1014         try (DirContextHolder holder = getDirContext(envSupplier)) {
1015             final SearchControls controls = new SearchControls();
1016             controls.setSearchScope(SearchControls.SUBTREE_SCOPE);
1017             if (returningAttrs != null) {
1018                 controls.setReturningAttributes(returningAttrs);
1019             }
1020 
1021             consumer.accept(Collections.list(holder.get().search(baseDn, filter, controls)));
1022         } catch (final NamingException e) {
1023             throw new LdapOperationException("Failed to search " + baseDn + " with " + filter, e);
1024         }
1025     }
1026 
1027     protected void modifyAddEntry(final List<ModificationItem> modifyList, final String name, final String value) {
1028         final Attribute attr = new BasicAttribute(name, value);
1029         final ModificationItem mod = new ModificationItem(DirContext.ADD_ATTRIBUTE, attr);
1030         modifyList.add(mod);
1031     }
1032 
1033     protected void modifyReplaceEntry(final List<ModificationItem> modifyList, final String name, final String value) {
1034         final Attribute attr = new BasicAttribute(name, value);
1035         final ModificationItem mod = new ModificationItem(DirContext.REPLACE_ATTRIBUTE, attr);
1036         modifyList.add(mod);
1037     }
1038 
1039     protected void modifyDeleteEntry(final List<ModificationItem> modifyList, final String name, final Object value) {
1040         final Attribute attr = new BasicAttribute(name, value);
1041         final ModificationItem mod = new ModificationItem(DirContext.REMOVE_ATTRIBUTE, attr);
1042         modifyList.add(mod);
1043     }
1044 
1045     protected void modify(final String dn, final List<ModificationItem> modifyList, final Supplier<Hashtable<String, String>> envSupplier) {
1046         if (modifyList.isEmpty()) {
1047             return;
1048         }
1049         try (DirContextHolder holder = getDirContext(envSupplier)) {
1050             holder.get().modifyAttributes(dn, modifyList.toArray(new ModificationItem[modifyList.size()]));
1051         } catch (final NamingException e) {
1052             throw new LdapOperationException("Failed to search " + dn, e);
1053         }
1054     }
1055 
1056     interface SearcConsumer {
1057         void accept(List<SearchResult> t) throws NamingException;
1058     }
1059 
1060     protected DirContextHolder getDirContext(final Supplier<Hashtable<String, String>> envSupplier) {
1061         DirContextHolder holder = contextLocal.get();
1062         if (holder == null) {
1063             final Hashtable<String, String> env = envSupplier.get();
1064             try {
1065                 holder = new DirContextHolder(new InitialDirContext(env));
1066                 contextLocal.set(holder);
1067                 return holder;
1068             } catch (final NamingException e) {
1069                 throw new LdapOperationException("Failed to create DirContext.", e);
1070             }
1071         } else {
1072             holder.inc();
1073             return holder;
1074         }
1075     }
1076 
1077     protected class DirContextHolder implements AutoCloseable {
1078         private final DirContext context;
1079 
1080         private int counter = 1;
1081 
1082         protected DirContextHolder(final DirContext context) {
1083             this.context = context;
1084         }
1085 
1086         public DirContext get() {
1087             return context;
1088         }
1089 
1090         public void inc() {
1091             counter++;
1092         }
1093 
1094         @Override
1095         public void close() {
1096             if (counter > 1) {
1097                 counter--;
1098             } else {
1099                 contextLocal.remove();
1100                 if (context != null) {
1101                     try {
1102                         context.close();
1103                     } catch (final NamingException e) {
1104                         // ignored
1105                     }
1106                 }
1107             }
1108         }
1109     }
1110 }