1   /**
2    * Copyright (c) 2000-2009 Liferay, Inc. All rights reserved.
3    *
4    *
5    *
6    *
7    * The contents of this file are subject to the terms of the Liferay Enterprise
8    * Subscription License ("License"). You may not use this file except in
9    * compliance with the License. You can obtain a copy of the License by
10   * contacting Liferay, Inc. See the License for the specific language governing
11   * permissions and limitations under the License, including but not limited to
12   * distribution rights of the Software.
13   *
14   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15   * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16   * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17   * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18   * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19   * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20   * SOFTWARE.
21   */
22  
23  package com.liferay.portal.security.ldap;
24  
25  import com.liferay.portal.NoSuchUserException;
26  import com.liferay.portal.NoSuchUserGroupException;
27  import com.liferay.portal.SystemException;
28  import com.liferay.portal.kernel.log.Log;
29  import com.liferay.portal.kernel.log.LogFactoryUtil;
30  import com.liferay.portal.kernel.log.LogUtil;
31  import com.liferay.portal.kernel.util.ArrayUtil;
32  import com.liferay.portal.kernel.util.CalendarFactoryUtil;
33  import com.liferay.portal.kernel.util.DateFormatFactoryUtil;
34  import com.liferay.portal.kernel.util.GetterUtil;
35  import com.liferay.portal.kernel.util.InstancePool;
36  import com.liferay.portal.kernel.util.PropertiesUtil;
37  import com.liferay.portal.kernel.util.PropsKeys;
38  import com.liferay.portal.kernel.util.StringPool;
39  import com.liferay.portal.kernel.util.StringUtil;
40  import com.liferay.portal.kernel.util.Validator;
41  import com.liferay.portal.model.Company;
42  import com.liferay.portal.model.CompanyConstants;
43  import com.liferay.portal.model.Contact;
44  import com.liferay.portal.model.User;
45  import com.liferay.portal.model.UserGroup;
46  import com.liferay.portal.model.UserGroupRole;
47  import com.liferay.portal.security.auth.ScreenNameGenerator;
48  import com.liferay.portal.service.CompanyLocalServiceUtil;
49  import com.liferay.portal.service.ServiceContext;
50  import com.liferay.portal.service.UserGroupLocalServiceUtil;
51  import com.liferay.portal.service.UserLocalServiceUtil;
52  import com.liferay.portal.util.PrefsPropsUtil;
53  import com.liferay.portal.util.PropsValues;
54  import com.liferay.util.ldap.LDAPUtil;
55  import com.liferay.util.ldap.Modifications;
56  
57  import java.text.DateFormat;
58  import java.text.ParseException;
59  
60  import java.util.ArrayList;
61  import java.util.Calendar;
62  import java.util.Date;
63  import java.util.List;
64  import java.util.Locale;
65  import java.util.Properties;
66  
67  import javax.naming.Binding;
68  import javax.naming.CompositeName;
69  import javax.naming.Context;
70  import javax.naming.Name;
71  import javax.naming.NameNotFoundException;
72  import javax.naming.NamingEnumeration;
73  import javax.naming.OperationNotSupportedException;
74  import javax.naming.directory.Attribute;
75  import javax.naming.directory.Attributes;
76  import javax.naming.directory.ModificationItem;
77  import javax.naming.directory.SearchControls;
78  import javax.naming.directory.SearchResult;
79  import javax.naming.ldap.Control;
80  import javax.naming.ldap.InitialLdapContext;
81  import javax.naming.ldap.LdapContext;
82  import javax.naming.ldap.PagedResultsControl;
83  import javax.naming.ldap.PagedResultsResponseControl;
84  
85  /**
86   * <a href="PortalLDAPUtil.java.html"><b><i>View Source</i></b></a>
87   *
88   * @author Michael Young
89   * @author Brian Wing Shun Chan
90   * @author Jerry Niu
91   * @author Scott Lee
92   * @author Hervé Ménage
93   * @author Samuel Kong
94   * @author Ryan Park
95   */
96  public class PortalLDAPUtil {
97  
98      public static final String IMPORT_BY_GROUP = "group";
99  
100     public static final String IMPORT_BY_USER = "user";
101 
102     public static void exportToLDAP(Contact contact) throws Exception {
103         long companyId = contact.getCompanyId();
104 
105         if (!isAuthEnabled(companyId) || !isExportEnabled(companyId)) {
106             return;
107         }
108 
109         User user = UserLocalServiceUtil.getUserByContactId(
110             contact.getContactId());
111 
112         long ldapServerId = getLdapServerId(companyId, user.getScreenName());
113 
114         LdapContext ctx = getContext(ldapServerId, companyId);
115 
116         try {
117             if (ctx == null) {
118                 return;
119             }
120 
121             Properties userMappings = getUserMappings(ldapServerId, companyId);
122             Binding binding = getUser(
123                 ldapServerId, contact.getCompanyId(), user.getScreenName());
124             Name name = new CompositeName();
125 
126             if (binding == null) {
127 
128                 // Create new user in LDAP
129 
130                 _getDNName(ldapServerId, companyId, user, userMappings, name);
131 
132                 LDAPUser ldapUser = (LDAPUser)Class.forName(
133                     PropsValues.LDAP_USER_IMPL).newInstance();
134 
135                 ldapUser.setUser(user, ldapServerId);
136 
137                 ctx.bind(name, ldapUser);
138             }
139             else {
140 
141                 // Modify existing LDAP user record
142 
143                 name.add(getNameInNamespace(ldapServerId, companyId, binding));
144 
145                 Modifications mods = Modifications.getInstance();
146 
147                 mods.addItem(
148                     userMappings.getProperty("firstName"),
149                     contact.getFirstName());
150                 mods.addItem(
151                     userMappings.getProperty("lastName"),
152                     contact.getLastName());
153 
154                 String fullNameMapping = userMappings.getProperty("fullName");
155 
156                 if (Validator.isNotNull(fullNameMapping)) {
157                     mods.addItem(fullNameMapping, contact.getFullName());
158                 }
159 
160                 String jobTitleMapping = userMappings.getProperty("jobTitle");
161 
162                 if (Validator.isNotNull(jobTitleMapping)) {
163                     mods.addItem(jobTitleMapping, contact.getJobTitle());
164                 }
165 
166                 ModificationItem[] modItems = mods.getItems();
167 
168                 ctx.modifyAttributes(name, modItems);
169             }
170         }
171         catch (Exception e) {
172             throw e;
173         }
174         finally {
175             if (ctx != null) {
176                 ctx.close();
177             }
178         }
179     }
180 
181     public static void exportToLDAP(User user) throws Exception {
182         long companyId = user.getCompanyId();
183 
184         if (!isAuthEnabled(companyId) || !isExportEnabled(companyId)) {
185             return;
186         }
187 
188         long ldapServerId = getLdapServerId(companyId, user.getScreenName());
189 
190         LdapContext ctx = getContext(ldapServerId, companyId);
191 
192         try {
193             if (ctx == null) {
194                 return;
195             }
196 
197             Properties userMappings = getUserMappings(ldapServerId, companyId);
198             Binding binding = getUser(
199                 ldapServerId, user.getCompanyId(), user.getScreenName());
200             Name name = new CompositeName();
201 
202             if (binding == null) {
203 
204                 // Create new user in LDAP
205 
206                 _getDNName(ldapServerId, companyId, user, userMappings, name);
207 
208                 LDAPUser ldapUser = (LDAPUser)Class.forName(
209                     PropsValues.LDAP_USER_IMPL).newInstance();
210 
211                 ldapUser.setUser(user, ldapServerId);
212 
213                 ctx.bind(name, ldapUser);
214 
215                 binding = getUser(ldapServerId, user.getCompanyId(),
216                     user.getScreenName());
217 
218                 name = new CompositeName();
219             }
220 
221             // Modify existing LDAP user record
222 
223             name.add(getNameInNamespace(ldapServerId, companyId, binding));
224 
225             Modifications mods = Modifications.getInstance();
226 
227             mods.addItem(
228                 userMappings.getProperty("firstName"), user.getFirstName());
229             mods.addItem(
230                 userMappings.getProperty("lastName"), user.getLastName());
231 
232             String fullNameMapping = userMappings.getProperty("fullName");
233 
234             if (Validator.isNotNull(fullNameMapping)) {
235                 mods.addItem(fullNameMapping, user.getFullName());
236             }
237 
238             if (user.isPasswordModified() &&
239                 Validator.isNotNull(user.getPasswordUnencrypted())) {
240 
241                 mods.addItem(
242                     userMappings.getProperty("password"),
243                     user.getPasswordUnencrypted());
244             }
245 
246             if (Validator.isNotNull(user.getEmailAddress())) {
247                 mods.addItem(
248                     userMappings.getProperty("emailAddress"),
249                     user.getEmailAddress());
250             }
251 
252             String jobTitleMapping = userMappings.getProperty("jobTitle");
253 
254             if (Validator.isNotNull(jobTitleMapping)) {
255                 mods.addItem(jobTitleMapping, user.getJobTitle());
256             }
257 
258             ModificationItem[] modItems = mods.getItems();
259 
260             ctx.modifyAttributes(name, modItems);
261         }
262         catch (Exception e) {
263             _log.error(e, e);
264         }
265         finally {
266             if (ctx != null) {
267                 ctx.close();
268             }
269         }
270     }
271 
272     public static String getAuthSearchFilter(
273             long ldapServerId, long companyId, String emailAddress,
274             String screenName, String userId)
275         throws SystemException {
276 
277         String postfix = getPropertyPostfix(ldapServerId);
278 
279         String filter = PrefsPropsUtil.getString(
280             companyId, PropsKeys.LDAP_AUTH_SEARCH_FILTER + postfix);
281 
282         if (_log.isDebugEnabled()) {
283             _log.debug("Search filter before transformation " + filter);
284         }
285 
286         filter = StringUtil.replace(
287             filter,
288             new String[] {
289                 "@company_id@", "@email_address@", "@screen_name@", "@user_id@"
290             },
291             new String[] {
292                 String.valueOf(companyId), emailAddress, screenName,
293                 userId
294             });
295 
296         if (_log.isDebugEnabled()) {
297             _log.debug("Search filter after transformation " + filter);
298         }
299 
300         return filter;
301     }
302 
303     public static LdapContext getContext(long ldapServerId, long companyId)
304         throws Exception {
305 
306         String postfix = getPropertyPostfix(ldapServerId);
307 
308         String baseProviderURL = PrefsPropsUtil.getString(
309             companyId, PropsKeys.LDAP_BASE_PROVIDER_URL + postfix);
310         String pricipal = PrefsPropsUtil.getString(
311             companyId, PropsKeys.LDAP_SECURITY_PRINCIPAL + postfix);
312         String credentials = PrefsPropsUtil.getString(
313             companyId, PropsKeys.LDAP_SECURITY_CREDENTIALS + postfix);
314 
315         return getContext(companyId, baseProviderURL, pricipal, credentials);
316     }
317 
318     public static LdapContext getContext(
319             long companyId, String providerURL, String pricipal,
320             String credentials)
321         throws Exception {
322 
323         Properties env = new Properties();
324 
325         env.put(
326             Context.INITIAL_CONTEXT_FACTORY,
327             PrefsPropsUtil.getString(
328                 companyId, PropsKeys.LDAP_FACTORY_INITIAL));
329         env.put(Context.PROVIDER_URL, providerURL);
330         env.put(Context.SECURITY_PRINCIPAL, pricipal);
331         env.put(Context.SECURITY_CREDENTIALS, credentials);
332         env.put(
333             Context.REFERRAL,
334             PrefsPropsUtil.getString(companyId, PropsKeys.LDAP_REFERRAL));
335 
336         // Enable pooling
337 
338         env.put("com.sun.jndi.ldap.connect.pool", "true");
339         env.put("com.sun.jndi.ldap.connect.pool.maxsize","50");
340         env.put("com.sun.jndi.ldap.connect.pool.timeout", "10000");
341 
342         LogUtil.debug(_log, env);
343 
344         LdapContext ctx = null;
345 
346         try {
347             ctx = new InitialLdapContext(env, null);
348         }
349         catch (Exception e) {
350             if (_log.isWarnEnabled()) {
351                 _log.warn("Failed to bind to the LDAP server");
352             }
353 
354             if (_log.isDebugEnabled()) {
355                 _log.debug(e);
356             }
357         }
358 
359         return ctx;
360     }
361 
362     public static Attributes getGroupAttributes(
363             long ldapServerId, long companyId, LdapContext ctx,
364             String fullDistinguishedName)
365         throws Exception {
366 
367         return getGroupAttributes(ldapServerId, companyId, ctx,
368             fullDistinguishedName, false);
369     }
370 
371     public static Attributes getGroupAttributes(
372             long ldapServerId, long companyId, LdapContext ctx,
373             String fullDistinguishedName, boolean includeReferenceAttributes)
374         throws Exception {
375 
376         Properties groupMappings = getGroupMappings(ldapServerId, companyId);
377 
378         List<String> mappedGroupAttributeIds = new ArrayList<String>();
379 
380         mappedGroupAttributeIds.add(groupMappings.getProperty("groupName"));
381         mappedGroupAttributeIds.add(groupMappings.getProperty("description"));
382 
383         if (includeReferenceAttributes) {
384             mappedGroupAttributeIds.add(groupMappings.getProperty("user"));
385         }
386 
387         return _getAttributes(
388             ctx, fullDistinguishedName,
389             mappedGroupAttributeIds.toArray(new String[0]));
390     }
391 
392     public static Properties getGroupMappings(long ldapServerId, long companyId)
393         throws Exception {
394 
395         String postfix = getPropertyPostfix(ldapServerId);
396 
397         Properties groupMappings = PropertiesUtil.load(
398             PrefsPropsUtil.getString(companyId,
399                 PropsKeys.LDAP_GROUP_MAPPINGS + postfix));
400 
401         LogUtil.debug(_log, groupMappings);
402 
403         return groupMappings;
404     }
405 
406     public static List<SearchResult> getGroups(
407             long companyId, LdapContext ctx, int maxResults, String baseDN,
408             String groupFilter)
409         throws Exception {
410 
411         return _searchLDAP(
412             companyId, ctx, maxResults, baseDN, groupFilter, null);
413     }
414 
415     public static List<SearchResult> getGroups(
416             long ldapServerId, long companyId, LdapContext ctx, int maxResults)
417         throws Exception {
418 
419         String postfix = getPropertyPostfix(ldapServerId);
420 
421         String baseDN = PrefsPropsUtil.getString(
422             companyId, PropsKeys.LDAP_BASE_DN + postfix);
423         String groupFilter = PrefsPropsUtil.getString(
424             companyId, PropsKeys.LDAP_IMPORT_GROUP_SEARCH_FILTER + postfix);
425 
426         return getGroups(companyId, ctx, maxResults, baseDN, groupFilter);
427     }
428 
429     public static long getLdapServerId(long companyId, String screenName)
430         throws Exception {
431 
432         long[] ldapServerIds = StringUtil.split(
433             PrefsPropsUtil.getString(companyId, "ldap.server.ids"), 0L);
434 
435         for (long ldapServerId : ldapServerIds) {
436             if (hasUser(ldapServerId, companyId, screenName)) {
437                 return ldapServerId;
438             }
439         }
440 
441         if (ldapServerIds.length > 0) {
442             return ldapServerIds[0];
443         }
444 
445         return 0;
446     }
447 
448     public static Attribute getMultivaluedAttribute(
449             long companyId, LdapContext ctx, String baseDN, String filter,
450             Attribute attribute)
451         throws Exception {
452 
453         if (attribute.size() > 0) {
454             return attribute;
455         }
456 
457         String[] attributeIds = {_getNextRange(attribute.getID())};
458 
459         while (true) {
460             List<SearchResult> results = _searchLDAP(
461                 companyId, ctx, 0, baseDN, filter, attributeIds);
462 
463             if (results.size() != 1) {
464                 break;
465             }
466 
467             SearchResult result = results.get(0);
468 
469             Attributes attributes = result.getAttributes();
470 
471             if (attributes.size() != 1) {
472                 break;
473             }
474 
475             NamingEnumeration<? extends Attribute> enu = attributes.getAll();
476 
477             if (!enu.hasMoreElements()) {
478                 break;
479             }
480 
481             Attribute curAttribute = enu.nextElement();
482 
483             for (int i = 0; i < curAttribute.size(); i++) {
484                 attribute.add(curAttribute.get(i));
485             }
486 
487             if (StringUtil.endsWith(curAttribute.getID(), StringPool.STAR) ||
488                 (curAttribute.size() < PropsValues.LDAP_RANGE_SIZE)) {
489 
490                 break;
491             }
492 
493             attributeIds[0] = _getNextRange(attributeIds[0]);
494         }
495 
496         return attribute;
497     }
498 
499     public static String getNameInNamespace(
500             long ldapServerId, long companyId, Binding binding)
501         throws Exception {
502 
503         String postfix = getPropertyPostfix(ldapServerId);
504 
505         String baseDN = PrefsPropsUtil.getString(
506             companyId, PropsKeys.LDAP_BASE_DN + postfix);
507 
508         String name = binding.getName();
509 
510         if (name.startsWith(StringPool.QUOTE) &&
511             name.endsWith(StringPool.QUOTE)) {
512 
513             name = name.substring(1, name.length() - 1);
514         }
515 
516         if (Validator.isNull(baseDN)) {
517             return name.toString();
518         }
519         else {
520             StringBuilder sb = new StringBuilder();
521 
522             sb.append(name);
523             sb.append(StringPool.COMMA);
524             sb.append(baseDN);
525 
526             return sb.toString();
527         }
528     }
529 
530     public static String getPropertyPostfix(long ldapServerId) {
531         if (ldapServerId > 0) {
532             return StringPool.PERIOD + ldapServerId;
533         }
534 
535         return StringPool.BLANK;
536     }
537 
538     public static Binding getUser(
539             long ldapServerId, long companyId, String screenName)
540         throws Exception {
541 
542         String postfix = getPropertyPostfix(ldapServerId);
543 
544         LdapContext ctx = getContext(ldapServerId, companyId);
545 
546         NamingEnumeration<SearchResult> enu = null;
547 
548         try {
549             if (ctx == null) {
550                 return null;
551             }
552 
553             String baseDN = PrefsPropsUtil.getString(
554                 companyId, PropsKeys.LDAP_BASE_DN + postfix);
555 
556             Properties userMappings = getUserMappings(ldapServerId, companyId);
557 
558             StringBuilder filter = new StringBuilder();
559 
560             filter.append(StringPool.OPEN_PARENTHESIS);
561             filter.append(userMappings.getProperty("screenName"));
562             filter.append(StringPool.EQUAL);
563             filter.append(screenName);
564             filter.append(StringPool.CLOSE_PARENTHESIS);
565 
566             SearchControls cons = new SearchControls(
567                 SearchControls.SUBTREE_SCOPE, 1, 0, null, false, false);
568 
569             enu = ctx.search(baseDN, filter.toString(), cons);
570         }
571         catch (Exception e) {
572             throw e;
573         }
574         finally {
575             if (ctx != null) {
576                 ctx.close();
577             }
578         }
579 
580         if (enu.hasMoreElements()) {
581             Binding binding = enu.nextElement();
582 
583             enu.close();
584 
585             return binding;
586         }
587         else {
588             return null;
589         }
590     }
591 
592     public static Attributes getUserAttributes(
593             long ldapServerId, long companyId, LdapContext ctx,
594             String fullDistinguishedName)
595         throws Exception {
596 
597         Properties userMappings = getUserMappings(ldapServerId, companyId);
598 
599         String[] mappedUserAttributeIds = {
600             userMappings.getProperty("screenName"),
601             userMappings.getProperty("emailAddress"),
602             userMappings.getProperty("fullName"),
603             userMappings.getProperty("firstName"),
604             userMappings.getProperty("middleName"),
605             userMappings.getProperty("lastName"),
606             userMappings.getProperty("jobTitle"),
607             userMappings.getProperty("group")
608         };
609 
610         return _getAttributes(
611             ctx, fullDistinguishedName, mappedUserAttributeIds);
612     }
613 
614     public static Properties getUserMappings(long ldapServerId, long companyId)
615         throws Exception {
616 
617         String postfix = getPropertyPostfix(ldapServerId);
618 
619         Properties userMappings = PropertiesUtil.load(
620             PrefsPropsUtil.getString(companyId,
621                 PropsKeys.LDAP_USER_MAPPINGS + postfix));
622 
623         LogUtil.debug(_log, userMappings);
624 
625         return userMappings;
626     }
627 
628     public static List<SearchResult> getUsers(
629             long companyId, LdapContext ctx, int maxResults, String baseDN,
630             String userFilter)
631         throws Exception {
632 
633         return _searchLDAP(
634             companyId, ctx, maxResults, baseDN, userFilter, null);
635     }
636 
637     public static List<SearchResult> getUsers(
638             long ldapServerId, long companyId, LdapContext ctx, int maxResults)
639         throws Exception {
640 
641         String postfix = getPropertyPostfix(ldapServerId);
642 
643         String baseDN = PrefsPropsUtil.getString(
644             companyId, PropsKeys.LDAP_BASE_DN + postfix);
645         String userFilter = PrefsPropsUtil.getString(
646             companyId, PropsKeys.LDAP_IMPORT_USER_SEARCH_FILTER + postfix);
647 
648         return getUsers(companyId, ctx, maxResults, baseDN, userFilter);
649     }
650 
651     public static String getUsersDN(long ldapServerId, long companyId)
652         throws Exception {
653 
654         String postfix = getPropertyPostfix(ldapServerId);
655 
656         return PrefsPropsUtil.getString(companyId,
657             PropsKeys.LDAP_USERS_DN + postfix);
658     }
659 
660     public static boolean hasUser(
661             long ldapServerId, long companyId, String screenName)
662         throws Exception {
663 
664         if (getUser(ldapServerId, companyId, screenName) != null) {
665             return true;
666         }
667         else {
668             return false;
669         }
670     }
671 
672     public static void importFromLDAP() throws Exception {
673         List<Company> companies = CompanyLocalServiceUtil.getCompanies(false);
674 
675         for (Company company : companies) {
676             importFromLDAP(company.getCompanyId());
677         }
678     }
679 
680     public static void importFromLDAP(long companyId) throws Exception {
681         if (!isImportEnabled(companyId)) {
682             return;
683         }
684 
685         long[] ldapServerIds = StringUtil.split(
686             PrefsPropsUtil.getString(companyId, "ldap.server.ids"), 0L);
687 
688         for (long ldapServerId : ldapServerIds) {
689             importFromLDAP(ldapServerId, companyId);
690         }
691     }
692 
693     public static void importFromLDAP(long ldapServerId, long companyId)
694         throws Exception {
695 
696         if (!isImportEnabled(companyId)) {
697             return;
698         }
699 
700         LdapContext ctx = getContext(ldapServerId, companyId);
701 
702         if (ctx == null) {
703             return;
704         }
705 
706         try {
707             String importMethod = PrefsPropsUtil.getString(
708                 companyId, PropsKeys.LDAP_IMPORT_METHOD);
709 
710             if (importMethod.equals(IMPORT_BY_USER)) {
711                 List<SearchResult> results = getUsers(
712                     ldapServerId, companyId, ctx, 0);
713 
714                 // Loop through all LDAP users
715 
716                 for (SearchResult result : results) {
717                     Attributes attributes = getUserAttributes(
718                         ldapServerId, companyId, ctx,
719                         getNameInNamespace(ldapServerId, companyId, result));
720 
721                     importLDAPUser(
722                         ldapServerId, companyId, ctx, attributes,
723                         StringPool.BLANK, true);
724                 }
725             }
726             else if (importMethod.equals(IMPORT_BY_GROUP)) {
727                 List<SearchResult> results = getGroups(
728                     ldapServerId, companyId, ctx, 0);
729 
730                 // Loop through all LDAP groups
731 
732                 for (SearchResult result : results) {
733                     Attributes attributes = getGroupAttributes(
734                         ldapServerId, companyId, ctx,
735                         getNameInNamespace(ldapServerId, companyId, result),
736                         true);
737 
738                     importLDAPGroup(ldapServerId, companyId, ctx, attributes,
739                         true);
740                 }
741             }
742         }
743         catch (Exception e) {
744             _log.error("Error importing LDAP users and groups", e);
745         }
746         finally {
747             if (ctx != null) {
748                 ctx.close();
749             }
750         }
751     }
752 
753     public static UserGroup importLDAPGroup(
754             long ldapServerId, long companyId, LdapContext ctx,
755             Attributes attributes, boolean importGroupMembership)
756         throws Exception {
757 
758         String postfix = getPropertyPostfix(ldapServerId);
759 
760         AttributesTransformer attributesTransformer =
761             AttributesTransformerFactory.getInstance();
762 
763         attributes = attributesTransformer.transformGroup(attributes);
764 
765         Properties groupMappings = getGroupMappings(ldapServerId, companyId);
766 
767         LogUtil.debug(_log, groupMappings);
768 
769         String groupName = LDAPUtil.getAttributeValue(
770             attributes, groupMappings.getProperty("groupName")).toLowerCase();
771         String description = LDAPUtil.getAttributeValue(
772             attributes, groupMappings.getProperty("description"));
773 
774         // Get or create user group
775 
776         UserGroup userGroup = null;
777 
778         try {
779             userGroup = UserGroupLocalServiceUtil.getUserGroup(
780                 companyId, groupName);
781 
782             UserGroupLocalServiceUtil.updateUserGroup(
783                 companyId, userGroup.getUserGroupId(), groupName, description);
784         }
785         catch (NoSuchUserGroupException nsuge) {
786             if (_log.isDebugEnabled()) {
787                 _log.debug("Adding user group to portal " + groupName);
788             }
789 
790             long defaultUserId = UserLocalServiceUtil.getDefaultUserId(
791                 companyId);
792 
793             try {
794                 userGroup = UserGroupLocalServiceUtil.addUserGroup(
795                     defaultUserId, companyId, groupName, description);
796             }
797             catch (Exception e) {
798                 if (_log.isWarnEnabled()) {
799                     _log.warn("Could not create user group " + groupName);
800                 }
801 
802                 if (_log.isDebugEnabled()) {
803                     _log.debug(e, e);
804                 }
805             }
806         }
807 
808         // Import users and membership
809 
810         if (importGroupMembership && (userGroup != null)) {
811             Attribute attribute = attributes.get(
812                 groupMappings.getProperty("user"));
813 
814             if (attribute != null) {
815                 String baseDN = PrefsPropsUtil.getString(
816                     companyId, PropsKeys.LDAP_BASE_DN + postfix);
817 
818                 StringBuilder sb = new StringBuilder();
819 
820                 sb.append("(&");
821                 sb.append(
822                     PrefsPropsUtil.getString(
823                         companyId,
824                         PropsKeys.LDAP_IMPORT_GROUP_SEARCH_FILTER + postfix));
825                 sb.append("(");
826                 sb.append(groupMappings.getProperty("groupName"));
827                 sb.append("=");
828                 sb.append(
829                     LDAPUtil.getAttributeValue(
830                         attributes, groupMappings.getProperty("groupName")));
831                 sb.append("))");
832 
833                 attribute = getMultivaluedAttribute(
834                     companyId, ctx, baseDN, sb.toString(), attribute);
835 
836                 _importUsersAndMembershipFromLDAPGroup(
837                     ldapServerId, companyId, ctx, userGroup.getUserGroupId(),
838                     attribute);
839             }
840         }
841 
842         return userGroup;
843     }
844 
845     public static User importLDAPUser(
846             long ldapServerId, long companyId, LdapContext ctx,
847             Attributes attributes, String password,
848             boolean importGroupMembership)
849         throws Exception {
850 
851         LDAPUserTransactionThreadLocal.setOriginatesFromLDAP(true);
852 
853         try {
854             return _importLDAPUser(
855                 ldapServerId, companyId, ctx, attributes, password,
856                 importGroupMembership);
857         }
858         finally {
859             LDAPUserTransactionThreadLocal.setOriginatesFromLDAP(false);
860         }
861     }
862 
863     public static boolean isAuthEnabled(long companyId) throws SystemException {
864         if (PrefsPropsUtil.getBoolean(
865                 companyId, PropsKeys.LDAP_AUTH_ENABLED,
866                 PropsValues.LDAP_AUTH_ENABLED)) {
867 
868             return true;
869         }
870         else {
871             return false;
872         }
873     }
874 
875     public static boolean isExportEnabled(long companyId)
876         throws SystemException {
877 
878         if (PrefsPropsUtil.getBoolean(
879                 companyId, PropsKeys.LDAP_EXPORT_ENABLED,
880                 PropsValues.LDAP_EXPORT_ENABLED)) {
881 
882             return true;
883         }
884         else {
885             return false;
886         }
887     }
888 
889     public static boolean isImportEnabled(long companyId)
890         throws SystemException {
891 
892         if (PrefsPropsUtil.getBoolean(
893                 companyId, PropsKeys.LDAP_IMPORT_ENABLED,
894                 PropsValues.LDAP_IMPORT_ENABLED)) {
895 
896             return true;
897         }
898         else {
899             return false;
900         }
901     }
902 
903     public static boolean isImportOnStartup(long companyId)
904         throws SystemException {
905 
906         if (PrefsPropsUtil.getBoolean(
907                 companyId, PropsKeys.LDAP_IMPORT_ON_STARTUP)) {
908 
909             return true;
910         }
911         else {
912             return false;
913         }
914     }
915 
916     public static boolean isNtlmEnabled(long companyId)
917         throws SystemException {
918 
919         if (!isAuthEnabled(companyId)) {
920             return false;
921         }
922 
923         if (PrefsPropsUtil.getBoolean(
924                 companyId, PropsKeys.NTLM_AUTH_ENABLED,
925                 PropsValues.NTLM_AUTH_ENABLED)) {
926 
927             return true;
928         }
929         else {
930             return false;
931         }
932     }
933 
934     public static boolean isPasswordPolicyEnabled(long companyId)
935         throws SystemException {
936 
937         if (PrefsPropsUtil.getBoolean(
938                 companyId, PropsKeys.LDAP_PASSWORD_POLICY_ENABLED,
939                 PropsValues.LDAP_PASSWORD_POLICY_ENABLED)) {
940 
941             return true;
942         }
943         else {
944             return false;
945         }
946     }
947 
948     public static boolean isSiteMinderEnabled(long companyId)
949         throws SystemException {
950 
951         if (!isAuthEnabled(companyId)) {
952             return false;
953         }
954 
955         if (PrefsPropsUtil.getBoolean(
956                 companyId, PropsKeys.SITEMINDER_AUTH_ENABLED,
957                 PropsValues.SITEMINDER_AUTH_ENABLED)) {
958 
959             return true;
960         }
961         else {
962             return false;
963         }
964     }
965 
966     private static Attributes _getAttributes(
967             LdapContext ctx, String fullDistinguishedName,
968             String[] attributeIds)
969         throws Exception {
970 
971         Name fullDN = new CompositeName().add(fullDistinguishedName);
972 
973         Attributes attributes = null;
974 
975         String[] auditAttributeIds = {
976             "creatorsName", "createTimestamp", "modifiersName",
977             "modifyTimestamp"
978         };
979 
980         if (attributeIds == null) {
981 
982             // Get complete listing of LDAP attributes (slow)
983 
984             attributes = ctx.getAttributes(fullDN);
985 
986             NamingEnumeration<? extends Attribute> enu = ctx.getAttributes(
987                 fullDN, auditAttributeIds).getAll();
988 
989             while (enu.hasMoreElements()) {
990                 attributes.put(enu.nextElement());
991             }
992 
993             enu.close();
994         }
995         else {
996 
997             // Get specified LDAP attributes
998 
999             int attributeCount = attributeIds.length + auditAttributeIds.length;
1000
1001            String[] allAttributeIds = new String[attributeCount];
1002
1003            System.arraycopy(
1004                attributeIds, 0, allAttributeIds, 0, attributeIds.length);
1005            System.arraycopy(
1006                auditAttributeIds, 0, allAttributeIds, attributeIds.length,
1007                auditAttributeIds.length);
1008
1009            attributes = ctx.getAttributes(fullDN, allAttributeIds);
1010        }
1011
1012        return attributes;
1013    }
1014
1015    private static byte[] _getCookie(Control[] controls) {
1016        if (controls == null) {
1017            return null;
1018        }
1019
1020        for (Control control : controls) {
1021            if (control instanceof PagedResultsResponseControl) {
1022                PagedResultsResponseControl pagedResultsResponseControl =
1023                    (PagedResultsResponseControl)control;
1024
1025                return pagedResultsResponseControl.getCookie();
1026            }
1027        }
1028
1029        return null;
1030    }
1031
1032    private static void _getDNName(
1033            long ldapServerId, long companyId, User user,
1034            Properties userMappings, Name name)
1035        throws Exception {
1036
1037        // Generate full DN based on user DN
1038
1039        StringBuilder sb = new StringBuilder();
1040
1041        sb.append(userMappings.getProperty("screenName"));
1042        sb.append(StringPool.EQUAL);
1043        sb.append(user.getScreenName());
1044        sb.append(StringPool.COMMA);
1045        sb.append(getUsersDN(ldapServerId, companyId));
1046
1047        name.add(sb.toString());
1048    }
1049
1050    private static String _getNextRange(String attributeId) {
1051        String originalAttributeId = null;
1052        int start = 0;
1053        int end = 0;
1054
1055        int x = attributeId.indexOf(StringPool.SEMICOLON);
1056
1057        if (x < 0) {
1058            originalAttributeId = attributeId;
1059            end = PropsValues.LDAP_RANGE_SIZE - 1;
1060        }
1061        else {
1062            int y = attributeId.indexOf(StringPool.EQUAL, x);
1063            int z = attributeId.indexOf(StringPool.DASH, y);
1064
1065            originalAttributeId = attributeId.substring(0, x);
1066            start = GetterUtil.getInteger(attributeId.substring(y + 1, z));
1067            end = GetterUtil.getInteger(attributeId.substring(z + 1));
1068
1069            start += PropsValues.LDAP_RANGE_SIZE;
1070            end += PropsValues.LDAP_RANGE_SIZE;
1071        }
1072
1073        StringBuilder sb = new StringBuilder();
1074
1075        sb.append(originalAttributeId);
1076        sb.append(StringPool.SEMICOLON);
1077        sb.append("range=");
1078        sb.append(start);
1079        sb.append(StringPool.DASH);
1080        sb.append(end);
1081
1082        return sb.toString();
1083    }
1084
1085    private static void _importGroupsAndMembershipFromLDAPUser(
1086            long ldapServerId, long companyId, LdapContext ctx, long userId,
1087            Attribute attr)
1088        throws Exception {
1089
1090        List<Long> newUserGroupIds = new ArrayList<Long>(attr.size());
1091
1092        for (int i = 0; i < attr.size(); i++) {
1093
1094            // Find group in LDAP
1095
1096            String fullGroupDN = (String)attr.get(i);
1097
1098            Attributes groupAttributes = null;
1099
1100            try {
1101                groupAttributes = getGroupAttributes(
1102                    ldapServerId, companyId, ctx, fullGroupDN);
1103            }
1104            catch (NameNotFoundException nnfe) {
1105                _log.error(
1106                    "LDAP group not found with fullGroupDN " + fullGroupDN);
1107
1108                _log.error(nnfe, nnfe);
1109
1110                continue;
1111            }
1112
1113            UserGroup userGroup = importLDAPGroup(
1114                ldapServerId, companyId, ctx, groupAttributes, false);
1115
1116            // Add user to user group
1117
1118            if (userGroup != null) {
1119                if (_log.isDebugEnabled()) {
1120                    _log.debug(
1121                        "Adding " + userId + " to group " +
1122                            userGroup.getUserGroupId());
1123                }
1124
1125                newUserGroupIds.add(userGroup.getUserGroupId());
1126            }
1127        }
1128
1129        UserGroupLocalServiceUtil.setUserUserGroups(
1130            userId,
1131            ArrayUtil.toArray(
1132                newUserGroupIds.toArray(new Long[newUserGroupIds.size()])));
1133    }
1134
1135    private static User _importLDAPUser(
1136            long ldapServerId, long companyId, LdapContext ctx,
1137            Attributes attributes, String password,
1138            boolean importGroupMembership)
1139        throws Exception {
1140
1141        AttributesTransformer attributesTransformer =
1142            AttributesTransformerFactory.getInstance();
1143
1144        attributes = attributesTransformer.transformUser(attributes);
1145
1146        Properties userMappings = getUserMappings(ldapServerId, companyId);
1147
1148        LogUtil.debug(_log, userMappings);
1149
1150        User defaultUser = UserLocalServiceUtil.getDefaultUser(companyId);
1151
1152        boolean autoPassword = false;
1153        boolean updatePassword = true;
1154
1155        if (password.equals(StringPool.BLANK)) {
1156            autoPassword = true;
1157            updatePassword = false;
1158        }
1159
1160        long creatorUserId = 0;
1161        boolean passwordReset = false;
1162        boolean autoScreenName = false;
1163        String screenName = LDAPUtil.getAttributeValue(
1164            attributes, userMappings.getProperty("screenName")).toLowerCase();
1165        String emailAddress = LDAPUtil.getAttributeValue(
1166            attributes, userMappings.getProperty("emailAddress"));
1167        String openId = StringPool.BLANK;
1168        Locale locale = defaultUser.getLocale();
1169        String firstName = LDAPUtil.getAttributeValue(
1170            attributes, userMappings.getProperty("firstName"));
1171        String middleName = LDAPUtil.getAttributeValue(
1172            attributes, userMappings.getProperty("middleName"));
1173        String lastName = LDAPUtil.getAttributeValue(
1174            attributes, userMappings.getProperty("lastName"));
1175
1176        if (Validator.isNull(firstName) || Validator.isNull(lastName)) {
1177            String fullName = LDAPUtil.getAttributeValue(
1178                attributes, userMappings.getProperty("fullName"));
1179
1180            String[] names = LDAPUtil.splitFullName(fullName);
1181
1182            firstName = names[0];
1183            middleName = names[1];
1184            lastName = names[2];
1185        }
1186
1187        int prefixId = 0;
1188        int suffixId = 0;
1189        boolean male = true;
1190        int birthdayMonth = Calendar.JANUARY;
1191        int birthdayDay = 1;
1192        int birthdayYear = 1970;
1193        String jobTitle = LDAPUtil.getAttributeValue(
1194            attributes, userMappings.getProperty("jobTitle"));
1195        long[] groupIds = null;
1196        long[] organizationIds = null;
1197        long[] roleIds = null;
1198        List<UserGroupRole> userGroupRoles = null;
1199        long[] userGroupIds = null;
1200        boolean sendEmail = false;
1201        ServiceContext serviceContext = new ServiceContext();
1202
1203        if (_log.isDebugEnabled()) {
1204            _log.debug(
1205                "Screen name " + screenName + " and email address " +
1206                    emailAddress);
1207        }
1208
1209        if (Validator.isNull(screenName) || Validator.isNull(emailAddress)) {
1210            if (_log.isWarnEnabled()) {
1211                _log.warn(
1212                    "Cannot add user because screen name and email address " +
1213                        "are required");
1214            }
1215
1216            return null;
1217        }
1218
1219        User user = null;
1220
1221        try {
1222
1223            // Find corresponding portal user
1224
1225            String authType = PrefsPropsUtil.getString(
1226                companyId, PropsKeys.COMPANY_SECURITY_AUTH_TYPE,
1227                PropsValues.COMPANY_SECURITY_AUTH_TYPE);
1228
1229            if (authType.equals(CompanyConstants.AUTH_TYPE_SN)) {
1230                user = UserLocalServiceUtil.getUserByScreenName(
1231                    companyId, screenName);
1232            }
1233            else {
1234                user = UserLocalServiceUtil.getUserByEmailAddress(
1235                    companyId, emailAddress);
1236            }
1237
1238            // Skip if is default user
1239
1240            if (user.isDefaultUser()) {
1241                return user;
1242            }
1243
1244            // User already exists in the Liferay database. Skip import if user
1245            // fields have been already synced, if import is part of a scheduled
1246            // import, or if the LDAP entry has never been modified.
1247
1248            Date ldapUserModifiedDate = null;
1249
1250            String modifiedDate = LDAPUtil.getAttributeValue(
1251                attributes, "modifyTimestamp");
1252
1253            try {
1254                if (Validator.isNull(modifiedDate)) {
1255                    if (_log.isInfoEnabled()) {
1256                        _log.info(
1257                            "LDAP entry never modified, skipping user " +
1258                                user.getEmailAddress());
1259                    }
1260
1261                    return user;
1262                }
1263                else {
1264                    DateFormat dateFormat =
1265                        DateFormatFactoryUtil.getSimpleDateFormat(
1266                            "yyyyMMddHHmmss");
1267
1268                    ldapUserModifiedDate = dateFormat.parse(modifiedDate);
1269                }
1270
1271                if (ldapUserModifiedDate.equals(user.getModifiedDate()) &&
1272                    autoPassword) {
1273
1274                    if (_log.isDebugEnabled()) {
1275                        _log.debug(
1276                            "User is already syncronized, skipping user " +
1277                                user.getEmailAddress());
1278                    }
1279
1280                    return user;
1281                }
1282            }
1283            catch (ParseException pe) {
1284                if (_log.isDebugEnabled()) {
1285                    _log.debug(
1286                        "Unable to parse LDAP modify timestamp " +
1287                            modifiedDate);
1288                }
1289
1290                _log.debug(pe, pe);
1291            }
1292
1293            // LPS-443
1294
1295            if (Validator.isNull(screenName)) {
1296                autoScreenName = true;
1297            }
1298
1299            if (autoScreenName) {
1300                ScreenNameGenerator screenNameGenerator =
1301                    (ScreenNameGenerator)InstancePool.get(
1302                        PropsValues.USERS_SCREEN_NAME_GENERATOR);
1303
1304                screenName = screenNameGenerator.generate(
1305                    companyId, user.getUserId(), emailAddress);
1306            }
1307
1308            Contact contact = user.getContact();
1309
1310            Calendar birthdayCal = CalendarFactoryUtil.getCalendar();
1311
1312            birthdayCal.setTime(contact.getBirthday());
1313
1314            birthdayMonth = birthdayCal.get(Calendar.MONTH);
1315            birthdayDay = birthdayCal.get(Calendar.DATE);
1316            birthdayYear = birthdayCal.get(Calendar.YEAR);
1317
1318            // User exists so update user information
1319
1320            if (updatePassword) {
1321                user = UserLocalServiceUtil.updatePassword(
1322                    user.getUserId(), password, password, passwordReset, true);
1323            }
1324
1325            user = UserLocalServiceUtil.updateUser(
1326                user.getUserId(), password, StringPool.BLANK, StringPool.BLANK,
1327                user.isPasswordReset(), user.getReminderQueryQuestion(),
1328                user.getReminderQueryAnswer(), screenName, emailAddress, openId,
1329                user.getLanguageId(), user.getTimeZoneId(), user.getGreeting(),
1330                user.getComments(), firstName, middleName, lastName,
1331                contact.getPrefixId(), contact.getSuffixId(), contact.getMale(),
1332                birthdayMonth, birthdayDay, birthdayYear, contact.getSmsSn(),
1333                contact.getAimSn(), contact.getFacebookSn(), contact.getIcqSn(),
1334                contact.getJabberSn(), contact.getMsnSn(),
1335                contact.getMySpaceSn(), contact.getSkypeSn(),
1336                contact.getTwitterSn(), contact.getYmSn(), jobTitle, groupIds,
1337                organizationIds, roleIds, userGroupRoles, userGroupIds,
1338                serviceContext);
1339
1340            if (ldapUserModifiedDate != null) {
1341                UserLocalServiceUtil.updateModifiedDate(
1342                    user.getUserId(), ldapUserModifiedDate);
1343            }
1344        }
1345        catch (NoSuchUserException nsue) {
1346
1347            // User does not exist so create
1348
1349        }
1350        catch (Exception e) {
1351            _log.error(
1352                "Error updating user with screen name " + screenName +
1353                    " and email address " + emailAddress,
1354                e);
1355
1356            return null;
1357        }
1358
1359        if (user == null) {
1360            try {
1361                if (_log.isDebugEnabled()) {
1362                    _log.debug("Adding user to portal " + emailAddress);
1363                }
1364
1365                user = UserLocalServiceUtil.addUser(
1366                    creatorUserId, companyId, autoPassword, password, password,
1367                    autoScreenName, screenName, emailAddress, openId, locale,
1368                    firstName, middleName, lastName, prefixId, suffixId, male,
1369                    birthdayMonth, birthdayDay, birthdayYear, jobTitle,
1370                    groupIds, organizationIds, roleIds, userGroupIds, sendEmail,
1371                    serviceContext);
1372            }
1373            catch (Exception e) {
1374                _log.error(
1375                    "Problem adding user with screen name " + screenName +
1376                        " and email address " + emailAddress,
1377                    e);
1378            }
1379        }
1380
1381        // Import user groups and membership
1382
1383        if (importGroupMembership && (user != null)) {
1384            String userMappingsGroup = userMappings.getProperty("group");
1385
1386            if (userMappingsGroup != null) {
1387                Attribute attribute = attributes.get(userMappingsGroup);
1388
1389                if (attribute != null) {
1390                    _importGroupsAndMembershipFromLDAPUser(
1391                        ldapServerId, companyId, ctx, user.getUserId(),
1392                        attribute);
1393                }
1394            }
1395        }
1396
1397        return user;
1398    }
1399
1400    private static void _importUsersAndMembershipFromLDAPGroup(
1401            long ldapServerId, long companyId, LdapContext ctx,
1402            long userGroupId, Attribute attr)
1403        throws Exception {
1404
1405        List<Long> newUserIds = new ArrayList<Long>(attr.size());
1406
1407        for (int i = 0; i < attr.size(); i++) {
1408
1409            // Find user in LDAP
1410
1411            String fullUserDN = (String)attr.get(i);
1412
1413            Attributes userAttributes = null;
1414
1415            try {
1416                userAttributes = getUserAttributes(ldapServerId, companyId, ctx,
1417                    fullUserDN);
1418            }
1419            catch (NameNotFoundException nnfe) {
1420                _log.error("LDAP user not found with fullUserDN " + fullUserDN);
1421
1422                _log.error(nnfe, nnfe);
1423
1424                continue;
1425            }
1426
1427            User user = importLDAPUser(
1428                ldapServerId, companyId, ctx, userAttributes, StringPool.BLANK,
1429                false);
1430
1431            // Add user to user group
1432
1433            if (user != null) {
1434                if (_log.isDebugEnabled()) {
1435                    _log.debug(
1436                        "Adding " + user.getUserId() + " to group " +
1437                            userGroupId);
1438                }
1439
1440                newUserIds.add(user.getUserId());
1441            }
1442        }
1443
1444        UserLocalServiceUtil.setUserGroupUsers(
1445            userGroupId,
1446            ArrayUtil.toArray(newUserIds.toArray(new Long[newUserIds.size()])));
1447    }
1448
1449    private static List<SearchResult> _searchLDAP(
1450            long companyId, LdapContext ctx, int maxResults, String baseDN,
1451            String filter, String[] attributeIds)
1452        throws Exception {
1453
1454        List<SearchResult> results = new ArrayList<SearchResult>();
1455
1456        SearchControls cons = new SearchControls(
1457            SearchControls.SUBTREE_SCOPE, maxResults, 0, attributeIds, false,
1458            false);
1459
1460        try {
1461            byte[] cookie = new byte[0];
1462
1463            while (cookie != null) {
1464                if (cookie.length == 0) {
1465                    ctx.setRequestControls(
1466                        new Control[] {
1467                            new PagedResultsControl(
1468                                PropsValues.LDAP_PAGE_SIZE, Control.CRITICAL)
1469                        });
1470                }
1471                else {
1472                    ctx.setRequestControls(
1473                        new Control[] {
1474                            new PagedResultsControl(
1475                                PropsValues.LDAP_PAGE_SIZE, cookie,
1476                                Control.CRITICAL)
1477                        });
1478                }
1479
1480                NamingEnumeration<SearchResult> enu = ctx.search(
1481                    baseDN, filter, cons);
1482
1483                while (enu.hasMoreElements()) {
1484                    results.add(enu.nextElement());
1485                }
1486
1487                enu.close();
1488
1489                cookie = _getCookie(ctx.getResponseControls());
1490            }
1491        }
1492        catch (OperationNotSupportedException onse) {
1493            ctx.setRequestControls(null);
1494
1495            NamingEnumeration<SearchResult> enu = ctx.search(
1496                baseDN, filter, cons);
1497
1498            while (enu.hasMoreElements()) {
1499                results.add(enu.nextElement());
1500            }
1501
1502            enu.close();
1503        }
1504        finally {
1505            ctx.setRequestControls(null);
1506        }
1507
1508        return results;
1509    }
1510
1511    private static Log _log = LogFactoryUtil.getLog(PortalLDAPUtil.class);
1512
1513}