1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package net.sf.ldaptemplate;
17
18 import java.util.List;
19
20 import javax.naming.Binding;
21 import javax.naming.Name;
22 import javax.naming.NameNotFoundException;
23 import javax.naming.NamingEnumeration;
24 import javax.naming.NamingException;
25 import javax.naming.PartialResultException;
26 import javax.naming.directory.Attributes;
27 import javax.naming.directory.DirContext;
28 import javax.naming.directory.ModificationItem;
29 import javax.naming.directory.SearchControls;
30 import javax.naming.directory.SearchResult;
31
32 import net.sf.ldaptemplate.support.DistinguishedName;
33
34 import org.apache.commons.lang.Validate;
35 import org.apache.commons.logging.Log;
36 import org.apache.commons.logging.LogFactory;
37 import org.springframework.beans.factory.InitializingBean;
38 import org.springframework.dao.DataAccessException;
39
40 /***
41 * Executes core LDAP functionality and helps to avoid common errors, relieving
42 * the user of the burden of looking up contexts, looping through
43 * NamingEnumerations and closing contexts.
44 * <p>
45 * <b>Note for Active Directory (AD) users:</b> AD servers are apparently
46 * unable to handle referrals automatically, which causes a
47 * <code>PartialResultException</code> to be thrown whenever a referral is
48 * encountered in a search. To avoid this, set the
49 * <code>ignorePartialResultException</code> property to <code>true</code>.
50 * There is currently no way of manually handling these referrals in the form of
51 * <code>ReferralException</code>, i.e. either you get the exception (and
52 * your results are lost) or all referrals are ignored (if the server is unable
53 * to handle them properly. Neither is there any simple way to get notified that
54 * a <code>PartialResultException</code> has been ignored (other than in the
55 * log).
56 *
57 * @see net.sf.ldaptemplate.ContextSource
58 *
59 * @author Mattias Arthursson
60 * @author Ulrik Sandberg
61 */
62 public class LdapTemplate implements LdapOperations, InitializingBean {
63
64 private static final Log log = LogFactory.getLog(LdapTemplate.class);
65
66 private static final int DEFAULT_SEARCH_SCOPE = SearchControls.SUBTREE_SCOPE;
67
68 private static final boolean DONT_RETURN_OBJ_FLAG = false;
69
70 private static final boolean RETURN_OBJ_FLAG = true;
71
72 private ContextSource contextSource;
73
74 private NamingExceptionTranslator exceptionTranslator = new DefaultNamingExceptionTranslator();
75
76 private boolean ignorePartialResultException = false;
77
78 /***
79 * Constructor for bean usage.
80 */
81 public LdapTemplate() {
82 }
83
84 /***
85 * Constructor to setup instance directly.
86 *
87 * @param contextSource
88 * the ContextSource to use.
89 */
90 public LdapTemplate(ContextSource contextSource) {
91 this.contextSource = contextSource;
92 }
93
94 /***
95 * Set the ContextSource. Call this method when the default constructor has
96 * been used.
97 *
98 * @param contextSource
99 * the ContextSource.
100 */
101 public void setContextSource(ContextSource contextSource) {
102 this.contextSource = contextSource;
103 }
104
105 /***
106 * Specify whether <code>PartialResultException</code> should be ignored
107 * in searches. AD servers typically have a problem with referrals. Normally
108 * a referral should be followed automatically, but this does not seem to
109 * work with AD servers. The problem manifests itself with a a
110 * <code>PartialResultException</code> being thrown when a referral is
111 * encountered by the server. Setting this property to <code>true</code>
112 * presents a workaround to this problem by causing
113 * <code>PartialResultException</code> to be ignored, so that the search
114 * method returns normally. Default value of this parameter is
115 * <code>false</code>.
116 *
117 * @param ignore
118 * <code>true</code> if <code>PartialResultException</code>
119 * should be ignored in searches, <code>false</code> otherwise.
120 * Default is <code>false</code>.
121 */
122 public void setIgnorePartialResultException(boolean ignore) {
123 this.ignorePartialResultException = ignore;
124 }
125
126
127
128
129
130
131 public void search(Name base, String filter, int searchScope,
132 boolean returningObjFlag, SearchResultCallbackHandler handler) {
133
134 search(base, filter, getDefaultSearchControls(searchScope,
135 returningObjFlag), handler);
136 }
137
138
139
140
141
142
143 public void search(String base, String filter, int searchScope,
144 boolean returningObjFlag, SearchResultCallbackHandler handler)
145 throws DataAccessException {
146
147 search(base, filter, getDefaultSearchControls(searchScope,
148 returningObjFlag), handler);
149 }
150
151
152
153
154
155
156 public void search(final Name base, final String filter,
157 final SearchControls controls, SearchResultCallbackHandler handler) {
158
159
160 SearchExecutor se = new SearchExecutor() {
161 public NamingEnumeration executeSearch(DirContext ctx)
162 throws NamingException {
163 return ctx.search(base, filter, controls);
164 }
165 };
166
167 search(se, handler);
168 }
169
170
171
172
173
174
175 public void search(final String base, final String filter,
176 final SearchControls controls, SearchResultCallbackHandler handler) {
177
178
179 SearchExecutor se = new SearchExecutor() {
180 public NamingEnumeration executeSearch(DirContext ctx)
181 throws NamingException {
182 return ctx.search(base, filter, controls);
183 }
184 };
185
186 search(se, handler);
187 }
188
189
190
191
192
193 public void search(SearchExecutor se, SearchResultCallbackHandler handler) {
194 DirContext ctx = contextSource.getReadOnlyContext();
195
196 NamingEnumeration results = null;
197 try {
198 results = se.executeSearch(ctx);
199
200 while (results.hasMore()) {
201 SearchResult searchResult = (SearchResult) results.next();
202 handler.handleSearchResult(searchResult);
203 }
204 } catch (NameNotFoundException e) {
205
206
207
208 } catch (PartialResultException e) {
209
210 if (ignorePartialResultException) {
211 log.debug("PartialResultException encountered and ignored", e);
212 } else {
213 throw getExceptionTranslator().translate(e);
214 }
215 } catch (NamingException e) {
216 throw getExceptionTranslator().translate(e);
217 } finally {
218 closeContextAndNamingEnumeration(ctx, results);
219 }
220 }
221
222
223
224
225
226 public void search(Name base, String filter,
227 SearchResultCallbackHandler handler) throws DataAccessException {
228
229 search(base, filter, getDefaultSearchControls(DEFAULT_SEARCH_SCOPE,
230 DONT_RETURN_OBJ_FLAG), handler);
231 }
232
233
234
235
236
237 public void search(String base, String filter,
238 SearchResultCallbackHandler handler) throws DataAccessException {
239
240 search(base, filter, getDefaultSearchControls(DEFAULT_SEARCH_SCOPE,
241 DONT_RETURN_OBJ_FLAG), handler);
242 }
243
244
245
246
247
248 public List search(Name base, String filter, int searchScope,
249 AttributesMapper mapper) {
250
251 return search(base, filter, getDefaultSearchControls(searchScope,
252 DONT_RETURN_OBJ_FLAG), mapper);
253 }
254
255
256
257
258
259 public List search(String base, String filter, int searchScope,
260 AttributesMapper mapper) throws DataAccessException {
261
262 return search(base, filter, getDefaultSearchControls(searchScope,
263 DONT_RETURN_OBJ_FLAG), mapper);
264 }
265
266
267
268
269
270 public List search(Name base, String filter, AttributesMapper mapper)
271 throws DataAccessException {
272
273 return search(base, filter, DEFAULT_SEARCH_SCOPE, mapper);
274 }
275
276
277
278
279
280 public List search(String base, String filter, AttributesMapper mapper)
281 throws DataAccessException {
282
283 return search(base, filter, DEFAULT_SEARCH_SCOPE, mapper);
284 }
285
286
287
288
289
290 public List search(Name base, String filter, int searchScope,
291 ContextMapper mapper) {
292
293 return search(base, filter, getDefaultSearchControls(searchScope,
294 RETURN_OBJ_FLAG), mapper);
295 }
296
297
298
299
300
301 public List search(String base, String filter, int searchScope,
302 ContextMapper mapper) throws DataAccessException {
303
304 return search(base, filter, getDefaultSearchControls(searchScope,
305 RETURN_OBJ_FLAG), mapper);
306 }
307
308
309
310
311
312 public List search(Name base, String filter, ContextMapper mapper)
313 throws DataAccessException {
314
315 return search(base, filter, DEFAULT_SEARCH_SCOPE, mapper);
316 }
317
318
319
320
321
322 public List search(String base, String filter, ContextMapper mapper)
323 throws DataAccessException {
324
325 return search(base, filter, DEFAULT_SEARCH_SCOPE, mapper);
326 }
327
328
329
330
331
332
333 public List search(String base, String filter, SearchControls controls,
334 ContextMapper mapper) {
335
336 assureReturnObjFlagSet(controls);
337 ContextMapperCallbackHandler handler = new ContextMapperCallbackHandler(
338 mapper);
339 search(base, filter, controls, handler);
340
341 return handler.getList();
342 }
343
344
345
346
347
348
349 public List search(Name base, String filter, SearchControls controls,
350 ContextMapper mapper) {
351
352 assureReturnObjFlagSet(controls);
353 ContextMapperCallbackHandler handler = new ContextMapperCallbackHandler(
354 mapper);
355 search(base, filter, controls, handler);
356
357 return handler.getList();
358 }
359
360
361
362
363
364
365 public List search(Name base, String filter, SearchControls controls,
366 AttributesMapper mapper) {
367
368 AttributesMapperCallbackHandler handler = new AttributesMapperCallbackHandler(
369 mapper);
370 search(base, filter, controls, handler);
371
372 return handler.getList();
373 }
374
375
376
377
378
379
380 public List search(String base, String filter, SearchControls controls,
381 AttributesMapper mapper) {
382
383 AttributesMapperCallbackHandler handler = new AttributesMapperCallbackHandler(
384 mapper);
385 search(base, filter, controls, handler);
386
387 return handler.getList();
388 }
389
390
391
392
393 public Object executeReadOnly(ContextExecutor ce) {
394 DirContext ctx = contextSource.getReadOnlyContext();
395 return executeWithContext(ce, ctx);
396 }
397
398
399
400
401 public Object executeReadWrite(ContextExecutor ce) {
402 DirContext ctx = contextSource.getReadWriteContext();
403 return executeWithContext(ce, ctx);
404 }
405
406 private Object executeWithContext(ContextExecutor ce, DirContext ctx) {
407 try {
408 return ce.executeWithContext(ctx);
409 } catch (NamingException e) {
410 throw getExceptionTranslator().translate(e);
411 } finally {
412 closeContext(ctx);
413 }
414 }
415
416
417
418
419 public Object lookup(final Name dn) {
420 return executeReadOnly(new ContextExecutor() {
421 public Object executeWithContext(DirContext ctx)
422 throws NamingException {
423 return ctx.lookup(dn);
424 }
425 });
426 }
427
428
429
430
431 public Object lookup(final String dn) throws DataAccessException {
432 return executeReadOnly(new ContextExecutor() {
433 public Object executeWithContext(DirContext ctx)
434 throws NamingException {
435 return ctx.lookup(dn);
436 }
437 });
438 }
439
440
441
442
443
444 public Object lookup(final Name dn, final AttributesMapper mapper) {
445 return executeReadOnly(new ContextExecutor() {
446 public Object executeWithContext(DirContext ctx)
447 throws NamingException {
448 Attributes attributes = ctx.getAttributes(dn);
449 return mapper.mapFromAttributes(attributes);
450 }
451 });
452 }
453
454
455
456
457
458 public Object lookup(final String dn, final AttributesMapper mapper)
459 throws DataAccessException {
460
461 return executeReadOnly(new ContextExecutor() {
462 public Object executeWithContext(DirContext ctx)
463 throws NamingException {
464 Attributes attributes = ctx.getAttributes(dn);
465 return mapper.mapFromAttributes(attributes);
466 }
467 });
468 }
469
470
471
472
473
474 public Object lookup(final Name dn, final ContextMapper mapper) {
475 return executeReadOnly(new ContextExecutor() {
476 public Object executeWithContext(DirContext ctx)
477 throws NamingException {
478 Object object = ctx.lookup(dn);
479 return mapper.mapFromContext(object);
480 }
481 });
482 }
483
484
485
486
487
488 public Object lookup(final String dn, final ContextMapper mapper)
489 throws DataAccessException {
490
491 return executeReadOnly(new ContextExecutor() {
492 public Object executeWithContext(DirContext ctx)
493 throws NamingException {
494 Object object = ctx.lookup(dn);
495 return mapper.mapFromContext(object);
496 }
497 });
498 }
499
500
501
502
503
504 public void modifyAttributes(final Name dn, final ModificationItem[] mods) {
505 executeReadWrite(new ContextExecutor() {
506 public Object executeWithContext(DirContext ctx)
507 throws NamingException {
508 ctx.modifyAttributes(dn, mods);
509 return null;
510 }
511 });
512 }
513
514
515
516
517
518 public void modifyAttributes(final String dn, final ModificationItem[] mods)
519 throws DataAccessException {
520
521 executeReadWrite(new ContextExecutor() {
522 public Object executeWithContext(DirContext ctx)
523 throws NamingException {
524 ctx.modifyAttributes(dn, mods);
525 return null;
526 }
527 });
528 }
529
530
531
532
533
534 public void bind(final Name dn, final Object obj,
535 final Attributes attributes) {
536
537 executeReadWrite(new ContextExecutor() {
538 public Object executeWithContext(DirContext ctx)
539 throws NamingException {
540 ctx.bind(dn, obj, attributes);
541 return null;
542 }
543 });
544 }
545
546
547
548
549
550 public void bind(final String dn, final Object obj,
551 final Attributes attributes) throws DataAccessException {
552
553 executeReadWrite(new ContextExecutor() {
554 public Object executeWithContext(DirContext ctx)
555 throws NamingException {
556 ctx.bind(dn, obj, attributes);
557 return null;
558 }
559 });
560 }
561
562
563
564
565 public void unbind(final Name dn) {
566 doUnbind(dn);
567 }
568
569
570
571
572 public void unbind(final String dn) throws DataAccessException {
573 doUnbind(dn);
574 }
575
576
577
578
579
580 public void unbind(final Name dn, boolean recursive)
581 throws DataAccessException {
582 if (!recursive) {
583 doUnbind(dn);
584 } else {
585 doUnbindRecursively(dn);
586 }
587 }
588
589
590
591
592 public void unbind(final String dn, boolean recursive)
593 throws DataAccessException {
594 if (!recursive) {
595 doUnbind(dn);
596 } else {
597 doUnbindRecursively(dn);
598 }
599 }
600
601 private void doUnbind(final Name dn) throws DataAccessException {
602 executeReadWrite(new ContextExecutor() {
603 public Object executeWithContext(DirContext ctx)
604 throws NamingException {
605 ctx.unbind(dn);
606 return null;
607 }
608 });
609 }
610
611 private void doUnbind(final String dn) throws DataAccessException {
612 executeReadWrite(new ContextExecutor() {
613 public Object executeWithContext(DirContext ctx)
614 throws NamingException {
615 ctx.unbind(dn);
616 return null;
617 }
618 });
619 }
620
621 private void doUnbindRecursively(final Name dn) throws DataAccessException {
622 executeReadWrite(new ContextExecutor() {
623 public Object executeWithContext(DirContext ctx)
624 throws NamingException {
625 deleteRecursively(ctx, new DistinguishedName(dn));
626 return null;
627 }
628 });
629 }
630
631 private void doUnbindRecursively(final String dn)
632 throws DataAccessException {
633 executeReadWrite(new ContextExecutor() {
634 public Object executeWithContext(DirContext ctx)
635 throws NamingException {
636 deleteRecursively(ctx, new DistinguishedName(dn));
637 return null;
638 }
639 });
640 }
641
642 /***
643 * Delete all subcontexts including the current one recursively.
644 *
645 * @param ctx
646 * The context to use for deleting.
647 * @param name
648 * The starting point to delete recursively.
649 * @throws DataAccessException
650 * if any error occurs
651 */
652 protected void deleteRecursively(DirContext ctx, DistinguishedName name)
653 throws DataAccessException {
654
655 NamingEnumeration enumeration = null;
656 try {
657 enumeration = ctx.listBindings(name);
658 while (enumeration.hasMore()) {
659 Binding binding = (Binding) enumeration.next();
660 DistinguishedName childName = new DistinguishedName(binding
661 .getName());
662 childName.prepend((DistinguishedName) name);
663 deleteRecursively(ctx, childName);
664 }
665 ctx.unbind(name);
666 if (log.isDebugEnabled()) {
667 log.debug("Entry " + name + " deleted");
668 }
669 } catch (NamingException e) {
670 throw exceptionTranslator.translate(e);
671 } finally {
672 try {
673 enumeration.close();
674 } catch (Exception e) {
675
676 }
677 }
678 }
679
680
681
682
683
684 public void rebind(final Name dn, final Object obj,
685 final Attributes attributes) throws DataAccessException {
686
687 executeReadWrite(new ContextExecutor() {
688 public Object executeWithContext(DirContext ctx)
689 throws NamingException {
690 ctx.rebind(dn, obj, attributes);
691 return null;
692 }
693 });
694 }
695
696
697
698
699
700 public void rebind(final String dn, final Object obj,
701 final Attributes attributes) throws DataAccessException {
702
703 executeReadWrite(new ContextExecutor() {
704 public Object executeWithContext(DirContext ctx)
705 throws NamingException {
706 ctx.rebind(dn, obj, attributes);
707 return null;
708 }
709 });
710 }
711
712
713
714
715
716 public void rename(final Name oldDn, final Name newDn)
717 throws DataAccessException {
718
719 executeReadWrite(new ContextExecutor() {
720 public Object executeWithContext(DirContext ctx)
721 throws NamingException {
722 ctx.rename(oldDn, newDn);
723 return null;
724 }
725 });
726 }
727
728
729
730
731
732 public void rename(final String oldDn, final String newDn)
733 throws DataAccessException {
734
735 executeReadWrite(new ContextExecutor() {
736 public Object executeWithContext(DirContext ctx)
737 throws NamingException {
738 ctx.rename(oldDn, newDn);
739 return null;
740 }
741 });
742 }
743
744
745
746
747 public void afterPropertiesSet() throws Exception {
748 if (contextSource == null) {
749 throw new IllegalArgumentException(
750 "Property 'contextSource' must be set.");
751 }
752 }
753
754 private void closeContextAndNamingEnumeration(DirContext ctx,
755 NamingEnumeration results) {
756
757 closeNamingEnumeration(results);
758 closeContext(ctx);
759 }
760
761 /***
762 * Close the supplied DirContext if it is not null. Swallow any exceptions,
763 * as this is only for cleanup.
764 *
765 * @param ctx
766 * the context to close.
767 */
768 private void closeContext(DirContext ctx) {
769 if (ctx != null) {
770 try {
771 ctx.close();
772 } catch (Exception e) {
773
774 }
775 }
776 }
777
778 /***
779 * Close the supplied NamingEnumeration if it is not null. Swallow any
780 * exceptions, as this is only for cleanup.
781 *
782 * @param results
783 * the NamingEnumeration to close.
784 */
785 private void closeNamingEnumeration(NamingEnumeration results) {
786 if (results != null) {
787 try {
788 results.close();
789 } catch (Exception e) {
790
791 }
792 }
793 }
794
795 /***
796 * Get the NamingExceptionTranslator that will be used by this instance. If
797 * no exceptionTranslator has been set, a default instance will be created.
798 *
799 * @return the NamingExceptionTranslator to be used by this instance.
800 */
801 public NamingExceptionTranslator getExceptionTranslator() {
802 return exceptionTranslator;
803 }
804
805 /***
806 * Set the NamingExceptionTranslator to be used by this instance.
807 *
808 * @param exceptionTranslator
809 * the NamingExceptionTranslator to use.
810 */
811 public void setExceptionTranslator(
812 NamingExceptionTranslator exceptionTranslator) {
813
814 this.exceptionTranslator = exceptionTranslator;
815 }
816
817 private SearchControls getDefaultSearchControls(int searchScope,
818 boolean returningObjFlag) {
819
820 SearchControls controls = new SearchControls();
821 controls.setSearchScope(searchScope);
822 controls.setReturningObjFlag(returningObjFlag);
823 return controls;
824 }
825
826 /***
827 * Make sure the returnObjFlag is set in the supplied SearchControls. Set it
828 * and log if it's not set.
829 *
830 * @param controls
831 * the SearchControls to check.
832 */
833 private void assureReturnObjFlagSet(SearchControls controls) {
834 Validate.notNull(controls);
835 if (!controls.getReturningObjFlag()) {
836 log.info("The returnObjFlag of supplied SearchControls is not set"
837 + " but a ContextMapper is used - setting flag to true");
838 controls.setReturningObjFlag(true);
839 }
840 }
841
842 /***
843 * A CollectingSearchResultCallbackHandler to wrap an AttributesMapper. That
844 * is, the found objects are extracted from all SearchResults, and then
845 * passed to the specified AttributesMapper for translation. This class
846 * needs to be nested, since we want to be able to get hold of the exception
847 * translator of this instance.
848 *
849 * @author Mattias Arthursson
850 * @author Ulrik Sandberg
851 */
852 public class AttributesMapperCallbackHandler extends
853 CollectingSearchResultCallbackHandler {
854 private AttributesMapper mapper;
855
856 public AttributesMapperCallbackHandler(AttributesMapper mapper) {
857 this.mapper = mapper;
858 }
859
860 protected Object getObjectFromResult(SearchResult searchResult) {
861 Attributes attributes = searchResult.getAttributes();
862 try {
863 return mapper.mapFromAttributes(attributes);
864 } catch (NamingException e) {
865 throw getExceptionTranslator().translate(e);
866 }
867 }
868 }
869
870 /***
871 * A CollectingSearchResultCallbackHandler to wrap a ContextMapper. That is,
872 * the found objects are extracted from all SearchResults, and then passed
873 * to the specified ContextMapper for translation.
874 *
875 * @author Mattias Arthursson
876 */
877 public class ContextMapperCallbackHandler extends
878 CollectingSearchResultCallbackHandler {
879 private ContextMapper mapper;
880
881 public ContextMapperCallbackHandler(ContextMapper mapper) {
882 this.mapper = mapper;
883 }
884
885 protected Object getObjectFromResult(SearchResult searchResult) {
886 Object object = searchResult.getObject();
887 if (object == null) {
888 throw new EntryNotFoundException(
889 "SearchResult did not contain any object.");
890 }
891 return mapper.mapFromContext(object);
892 }
893 }
894 }