KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > com > ca > commons > jndi > TestJNDIOps


1 package com.ca.commons.jndi;
2
3 import com.ca.commons.naming.*;
4
5 import javax.naming.*;
6 import javax.naming.directory.*;
7 import java.io.*;
8
9
10 /**
11  * This is a general test bed interface to test the various jndi calls
12  * used by JXplorer. It used ldif change file format requests, and can
13  * be run directly from the command line, or given an ldif change file
14  * to process.<p>
15  * <p/>
16  * Requests are either standard ldif change file requests, such as:
17  * <pre>
18  * <p/>
19  * version: 1
20  * # Add a new entry
21  * dn: cn=Fiona Jensen, ou=Marketing, dc=airius, dc=com
22  * changetype: add
23  * objectclass: top
24  * objectclass: person
25  * objectclass: organizationalPerson
26  * cn: Fiona Jensen
27  * sn: Jensen
28  * uid: fiona
29  * telephonenumber: +1 408 555 1212
30  * jpegphoto:< file:///usr/local/directory/photos/fiona.jpg
31  * <pre>
32  * where valid 'changetype' values are [add|delete|modrdn|modify]
33  * (see draft-good-ldap-ldif-04.html) <p>
34  * This is extended by jnditest to include extra (non-standard) changetype
35  * commands, such as [connect|disconnect|list|search|searchOneLevel|read], to allow
36  * general directory access and testing.
37  * The 'connect' command takes the attributes; 'url', and the optional \n"+
38  * attributes: 'user','pwd','tracing','ldapVersion','referral' and 'useSSL' \n" +
39  * <p/>
40  * XXX coming soon - [copyTree|deleteTree|moveTree]
41  *
42  * @author Chris, Trudi
43  */

44
45 public class TestJNDIOps
46 {
47     /**
48      * Open the appropriate input stream, and create a jdbctest object.
49      */

50     // static String version = "1.0(build"+BuildNumber.value +")";
51

52     BufferedReader in;
53
54     PrintStream out;
55
56     DXOps myOps = null;
57     //AdvancedOps myAdvOps = null;
58

59     LdifUtility ldifutil = new LdifUtility(); // ldif utility used to read / write ldif files
60

61     boolean debug = true;
62     boolean terminating = false; // whether the program should fail, returning -1, on error
63
boolean printstack = false; //TE: whether to print the stack trace.
64

65     public static int OK = 0;
66     public static int ERROR = -1;
67     public static int FINISHED = 1;
68
69     public static void main(String JavaDoc args[])
70     {
71
72         String JavaDoc fileName = null;
73         String JavaDoc url = null;
74         String JavaDoc user = null;
75         String JavaDoc pwd = null;
76         String JavaDoc version = "3";
77         String JavaDoc referral = "follow";
78         boolean useSSL = false;
79         boolean tracing = false;
80         boolean debugFlag = true;
81         boolean terminateFlag = false;
82         boolean printstackFlag = false;
83
84         int i = 0;
85
86         try
87         {
88             while (i < args.length)
89             {
90                 String JavaDoc arg = (args[i].charAt(0) != '-') ? args[i] : args[i].substring(1);
91                 switch (arg.charAt(0))
92                 {
93                     case '?':
94                     case 'H':
95                     case 'h':
96                         if (args.length > i + 1)
97                             printHelp(args[i + 1]);
98                         else
99                             printHelp(null);
100                         return; // print and exit program
101

102                     case 'C':
103                     case 'c':
104                         url = args[++i];
105                         break;
106
107                     case 'D':
108                     case 'd':
109                         debugFlag = true;
110                         break;
111
112                     case 'E':
113                     case 'e':
114                         terminateFlag = true;
115                         break;
116
117                     case 'F':
118                     case 'f':
119                         fileName = args[++i];
120                         break;
121
122                     case 'P':
123                     case 'p':
124                         pwd = args[++i];
125                         break;
126
127                     case 'R':
128                     case 'r':
129                         referral = args[++i];
130                         break;
131
132                     case 'S':
133                     case 's':
134                         useSSL = true;
135                         break;
136
137                     case 'T':
138                     case 't':
139                         tracing = true;
140                         break;
141
142                     case 'U':
143                     case 'u':
144                         user = args[++i];
145                         break;
146
147                     case 'V':
148                     case 'v':
149                         version = args[++i];
150                         break;
151
152                     case 'X':
153                     case 'x':
154                         printstackFlag = true;
155                         break;
156
157                     default :
158                         System.out.println("\n\nInvalid command line argument: -" + arg);
159                         printHelp(null);
160                         return; // print and exit program
161
}
162                 i++;
163             }
164         }
165         catch (Exception JavaDoc e)
166         {
167             System.out.println("Error reading command line arguments.");
168             printHelp("");
169             System.exit(-1);
170         }
171         
172         // create jnditest object
173
TestJNDIOps tester = new TestJNDIOps(fileName, url, user, pwd, tracing, version, debugFlag, terminateFlag, referral, useSSL, printstackFlag);
174         
175         // use jnditest object to process data until input finished.
176
tester.processInput();
177
178         tester.out.println("\nnormal finish\n");
179     }
180
181
182     public static void printHelp(String JavaDoc subject)
183     {
184         if ("full".equalsIgnoreCase(subject))
185         {
186             printFullHelp();
187             return;
188         }
189         if ("changetype".equalsIgnoreCase(subject))
190         {
191             printChangeTypeHelp();
192             return;
193         }
194
195         System.out.println("" +
196                 "\n\njndiTest is a small utility/test program designed to check the ldap/jndi\n" +
197                 "functionality of JXplorer. It reads input or files in ldif change file format\n" +
198                 "with some extra commands added to test searching and attribute reading.\n\n" +
199                 " usage: java jnditest [<options>]\n\n" +
200                 " options:\n" +
201                 " -c an optional connection url of the form ldap:\\host:port\n" +
202                 " -d debug (verbose) mode\n" +
203                 " -e exit on error, returning -1\n" +
204                 " -f filename name of an ldif changes input file\n" +
205                 " -h [subject] this help message [full|changetype]\n" +
206                 " -p password an option password\n" +
207                 " -r referral the jndi referral type [follow|ignore|throw]\n" +
208                 " -t set BER tracing on\n" +
209                 " -u userdn an optional user dn\n" +
210                 " -v set ldap version (default 3)\n\n" +
211                 " -x print stack trace\n\n" +
212                 "in addition to normal ldif changes commands (cf draft-good-ldap-ldif-04.html) a:\n" +
213                 "new keyword 'actiontype' with values [list|search|read|connect|disconnect]\n" +
214                 "is defined, with a usage similar to 'changetype'");
215     }
216
217     protected static void printFullHelp()
218     {
219         printChangeTypeHelp();
220     }
221
222     protected static void printChangeTypeHelp()
223     {
224         System.out.println("" +
225                 " * This is a general test bed interface to test the various jndi calls \n" +
226                 " * used by JXplorer. It used ldif change file format requests, and can \n" +
227                 " * be run directly from the command line, or given an ldif change file\n" +
228                 " * to process.<p>\n" +
229                 " *\n" +
230                 " * Requests are either standard ldif change file requests, such as:\n" +
231                 " *\n" +
232                 " * version: 1\n" +
233                 " * # Add a new entry\n" +
234                 " * changetype: add\n" +
235                 " * objectclass: top\n" +
236                 " * objectclass: person\n" +
237                 " * objectclass: organizationalPerson\n" +
238                 " * cn: Fiona Jensen\n" +
239                 " * sn: Jensen\n" +
240                 " * uid: fiona\n" +
241                 " * telephonenumber: +1 408 555 1212\n" +
242                 " * jpegphoto:< file:///usr/local/directory/photos/fiona.jpg\n" +
243                 " * <pre>\n" +
244                 " * where valid 'changetype' values are [add|delete|modrdn|modify] \n" +
245                 " * This is extended by jnditest to include extra (non-standard) changetype\n" +
246                 " * commands, such as [connect|disconnect|list|search|searchOneLevel|read], to allow\n" +
247                 " * general directory access and testing.\n" +
248                 " * The 'connect' command takes the attributes; 'url', and the optional \n" +
249                 " * attributes: 'user','pwd','tracing','ldapVersion','referral' and 'useSSL' \n" +
250                 " *\n" +
251                 " * XXX coming soon - [copyTree|deleteTree|moveTree]\n" +
252                 " *");
253     }
254
255     /**
256      * Constructs a jnditest object, opening a connection (if non-null input is
257      * passed for connection opening) and opening an input file (if a non-null
258      * file name is passed).<p>
259      * <p/>
260      * All the following parameters can be null.
261      *
262      * @param fileName the name of an LDIF changes input file.
263      * @param url a url of the form ldap://hostname:portnumber.
264      * @param user a user to bind to the directory as.
265      * @param pwd the user's password.
266      * @param tracing whether to set BER tracing on or not.
267      * @param version the LDAP Version (2 or 3) being used.
268      * @param debugFlag echo all system statement.
269      * @param terminateFlag exit on error, returning -1.
270      * @param referral the jndi referral type [follow|ignore|throw].
271      * @param useSSL to use SSL.
272      * @param printstackFlag whether to print a stack trace.
273      */

274
275     public TestJNDIOps(String JavaDoc fileName, String JavaDoc url, String JavaDoc user, String JavaDoc pwd, boolean tracing, String JavaDoc version, boolean debugFlag, boolean terminateFlag, String JavaDoc referral, boolean useSSL, boolean printstackFlag)
276     {
277         out = System.out; // may want to make this configurable in future :-)
278

279         debug = debugFlag;
280         terminating = terminateFlag;
281         printstack = printstackFlag;
282
283         // open a connection (if we've been given one)
284
if (url != null)
285             openConnection(url, user, pwd, tracing, version, referral, useSSL);
286
287         // open an input stream - file if we have one, console if not...
288
if (fileName != null)
289             in = openFile(fileName);
290         else
291             in = new BufferedReader(new InputStreamReader(System.in));
292     }
293
294     /**
295      * Open an ldap connection using jndi, via the standard JXplorer classes
296      * (DXOps.java, which wraps BasicOps.java).
297      *
298      * @param url a url of the form ldap://hostname:portnumber
299      * @param user a user to bind to the directory as.
300      * @param pwd the user's password.
301      * @param tracing whether to set BER tracing on or not
302      * @param version the LDAP Version (2 or 3) being used.
303      * @param referralType the jndi referral type [follow|ignore|throw].
304      * @param useSSL to use SSL.
305      */

306
307     public void openConnection(String JavaDoc url, String JavaDoc user, String JavaDoc pwd, boolean tracing, String JavaDoc version, String JavaDoc referralType, boolean useSSL)
308     {
309         if (referralType == null) referralType = "follow";
310         if ("ignorefollowthrow".indexOf(referralType) == -1)
311         {
312             error("unknown referraltype " + referralType, null);
313             referralType = "follow";
314         }
315
316         if (url.toLowerCase().startsWith("ldap://") == false)
317             url = "ldap://" + url;
318
319         try
320         {
321             if (debug) out.println("opening connection with :\n url: " + url + "\n user: " + user + "\n pwd: " + pwd + "\n tracing: " + tracing + "\n version: " + version + "\n referral: " + referralType);
322
323             if ((url == null) || (url.length() == 0))
324             {
325                 error("unable to open connection - no url supplied", null);
326                 return;
327             }
328
329             if ((version == null) || (version.length() == 0)) version = "3"; // default to ldap version 3
330

331             char[] password = (pwd == null || pwd.length() == 0) ? null : pwd.toCharArray();
332             int versn = Integer.parseInt(version);
333
334             ConnectionData cData = new ConnectionData(versn, url, user, password, tracing, null, null);
335             DirContext ctx = BasicOps.openContext(cData);
336             //DirContext ctx = BasicOps.openContext(versn, url, user, password, tracing, null, null);
337
if (ctx != null)
338                 myOps = new DXOps(ctx);
339
340             if (myOps == null)
341                 error("unable to open connection " + url, null);
342             else if (debug) out.println("connection " + url + " open. ");
343         }
344         catch (Exception JavaDoc e)
345         {
346             error("error opening connection " + url + "\n ", e);
347
348         }
349     }
350
351     /**
352      * Opens a text file as a Buffered Reader.
353      *
354      * @param fileName the name of the file to open.
355      * @return the Input Stream.
356      */

357
358     public BufferedReader openFile(String JavaDoc fileName)
359     {
360         try
361         {
362             File myFile = new File(fileName);
363             ldifutil.setFileDir(myFile.getParent());
364             return new BufferedReader(new InputStreamReader(new FileInputStream(myFile)));
365         }
366         catch (Exception JavaDoc e)
367         {
368             error("unable to open file : " + fileName + "\n ", e);
369             System.exit(-1);
370         }
371         return null;
372     }
373
374     /**
375      * Reads input as an ldif entry at a time
376      * (i.e. as a series of colon seperated att:value pairs, until
377      * a blank line or eof is read).
378      */

379
380     public void processInput()
381     {
382         // start the infinite loop...
383
try
384         {
385             DXEntry commandSet;
386             while ((commandSet = ldifutil.readLdifEntry(in)) != null)
387             {
388                 if (commandSet == null)
389                     error("internal error in jnditest - parsed ldif command set null", null);
390
391                 if (processCommand(commandSet) == FINISHED)
392                     return;
393             }
394         }
395         catch (Exception JavaDoc e)
396         {
397             error("error reading input - program stopped." + "\n ", e);
398             System.exit(-1);
399         }
400     }
401
402     /**
403      * This checks that an entry is valid, and
404      * then passes it to the appropriate command handler.
405      *
406      * @param entry the DXEntry object containing a dn attribute,
407      * a changetype attribute, and any data attributes required
408      * by the ldif command.
409      * @return code - 0 = normal, 1 = error, -1 = finished.
410      */

411
412     public int processCommand(DXEntry entry)
413     {
414         // do some sanity checking
415
if (entry.size() == 0)
416         {
417             if (debug) out.println("\n\nEnding on double blank line");
418             return FINISHED;
419         }
420
421         if (entry.get("version") != null)
422             entry.remove("version"); // strip out version number.
423

424         if (entry.getDN().size() == 0)
425         {
426             DXAttribute temp = (DXAttribute) entry.get("changetype");
427             String JavaDoc test = "";
428             try
429             {
430                 test = temp.get().toString();
431             }
432             catch (Exception JavaDoc e)
433             {
434             } // never happen
435
// two commands do not require a dn...
436
if ((test.equalsIgnoreCase("connect") == false) &&
437                     (test.equalsIgnoreCase("disconnect") == false))
438             {
439                 error("error reading input - no dn attribute in entry!\n*******\n" + entry + "******\n", null);
440                 return ERROR;
441             }
442         }
443
444         DXAttribute command = (DXAttribute) entry.get("changetype");
445         if (command == null || command.size() == 0)
446         {
447             error("error reading input - no 'changetype' attribute in entry.\n******\n" + entry + "******\n", null);
448             return ERROR;
449         }
450
451         String JavaDoc commandString = "";
452
453         try
454         {
455             commandString = command.get().toString();
456         }
457         catch (NamingException e)
458         {
459             error("internal error in processCommand()\n ", e);
460             return ERROR;
461         } // never happen :-)
462

463         if (debug) System.out.println("\n\nCOMMAND= " + commandString);
464
465         entry.remove("changetype");
466
467         if ((myOps == null) && !(commandString.equalsIgnoreCase("connect") || commandString.equalsIgnoreCase("disconnect")))
468             error("Attempting operation " + commandString + " without an open connection!", null);
469
470         // branch to appropriate handler
471
try
472         {
473             if (commandString.equalsIgnoreCase("add"))
474                 addEntry(entry, returnType(entry));
475             else if (commandString.equalsIgnoreCase("delete"))
476                 deleteEntry(entry, returnType(entry));
477             else if (commandString.equalsIgnoreCase("modrdn"))
478                 modrdnEntry(entry, modRDN(entry));
479             else if (commandString.equalsIgnoreCase("modify"))
480                 modifyEntry(entry, returnType(entry));
481             else if (commandString.equalsIgnoreCase("list"))
482                 listEntry(entry, list(entry));
483             else if (commandString.equalsIgnoreCase("search"))
484                 searchEntry(entry, search(entry));
485             else if (commandString.equalsIgnoreCase("searchOneLevel"))
486                 searchOneLevelEntry(entry, search(entry));
487             else if (commandString.equalsIgnoreCase("read"))
488                 readEntry(entry, read(entry));
489             else if (commandString.equalsIgnoreCase("connect"))
490                 connect(entry);
491             else if (commandString.equalsIgnoreCase("disconnect"))
492                 disconnect(entry);
493             else if (commandString.equalsIgnoreCase("copy"))
494                 copy(entry, copy(entry, command));
495             else if (commandString.equalsIgnoreCase("cut"))
496                 cut(entry, cut(entry, command));
497         }
498         catch (NamingException e)
499         {
500             error("Naming Exception in " + commandString + "\n ", e);
501             return ERROR;
502         }
503
504         return OK;
505     }
506
507
508     /**
509      * Gets the 'returntype' value from the test ldif file and parses it into a string.
510      * 'returntype' is a boolean value pertaining to the add, delete or modify 'changetype'
511      * functions in the ldif file. i.e. it signifies if an entry should actually be
512      * added or deleted. Changes the value to boolean. Removes the 'returntype' attribute.
513      *
514      * @param entry the DXEntry object containing a dn attribute, a changetype attribute, and any data attributes required.
515      * @return expectedVal a boolean value that represents the success or failure of the modification.
516      */

517
518     public boolean returnType(DXEntry entry)
519     {
520
521         DXAttribute returnType = (DXAttribute) entry.get("returntype");
522
523         String JavaDoc myType = "";
524         try
525         {
526             myType = returnType.get().toString();
527         }
528         catch (Exception JavaDoc e)
529         {
530         }
531
532         boolean expectedVal = "true".equalsIgnoreCase(myType);
533
534         if (returnType != null)
535         {
536             if (debug) System.out.println("\n\nparsed returntype: " + myType + "\n as: " + expectedVal + "\n");
537             entry.remove("returntype");
538         }
539         return expectedVal;
540     }
541
542
543     /**
544      * Add an entry to a directory. Compares the 'returntype' value in the ldif file (if
545      * present) to the result of the add operation. If the two don't match the program exits.
546      *
547      * @param entry contains the att/val pairs to be added, and the dn to add them to.
548      * @param expectedValue a flag that indicates the modification success.
549      */

550
551     public void addEntry(DXEntry entry, boolean expectedValue)
552     {
553         if (debug) System.out.println("\nADD: " + entry);
554
555         boolean ret = false;
556
557         try
558         {
559             myOps.addEntry(entry.getDN(), entry);
560             ret = true;
561         }
562         catch (NamingException e)
563         {
564         }
565
566         //TE: flag to test if the modification succeeded or not.
567

568         if (ret != expectedValue) //TE: tests if the modification status is what we expected.
569
{
570             if (debug) System.out.println("\nret equals expectedValue: " + (ret == expectedValue) + "\n");
571             if (debug) System.out.println("\n&&&& RET: " + ret + " &&&& EXPECTEDVALUE: " + expectedValue);
572             error("\nadd operation failed for: " + entry + "\n", null);
573         }
574     }
575
576
577     /**
578      * Delete an entry from a directory. Compares the 'returntype' value in the ldif file (if
579      * present) to the result of the delete operation. If the two don't match the program exits.
580      *
581      * @param entry contains dn to delete.
582      * @param expectedValue exptectedValue a flag that indicates the modification success.
583      */

584
585     public void deleteEntry(DXEntry entry, boolean expectedValue)
586     {
587         if (debug) System.out.println("\nDELETE: " + entry);
588
589         boolean ret = false;
590
591         try
592         {
593             myOps.deleteEntry(entry.getDN());
594             ret = true;
595         }
596         catch (NamingException e)
597         {
598         }
599
600         //TE: flag to test if the modification succeeded or not.
601

602         if (ret != expectedValue) //TE: tests if the modification status is what we expected.
603
{
604             if (debug) System.out.println("\n\nRET " + ret + " EXPECTEDVALUE " + expectedValue);
605             if (debug) System.out.println("\nret equals expectedValue: " + (ret == expectedValue) + "\n");
606             error("\ndelete operation failed for: " + entry + "\n", null);
607         }
608     }
609
610
611     /**
612      * Gets the 'modrdnresult' value from the test ldif file and parses into a string.
613      * 'modrdnresult' is the 'cn' attribute of the entry after the rename.
614      * Removes the 'modrdnresult' attribute. Note: 'modrdnresult' can be configured to
615      * represent any string attribute.
616      *
617      * @param entry contains the dn to start searching from, and a 'filter' attribute with a search filter.
618      * @return myModRdnCn
619      */

620
621     public String JavaDoc modRDN(DXEntry entry)
622     {
623
624         DXAttribute modRdnResult = (DXAttribute) entry.get("modrdnresult");
625
626         String JavaDoc myModRdnCn = "";
627         try
628         {
629             myModRdnCn = modRdnResult.get().toString();
630         }
631         catch (Exception JavaDoc e)
632         {
633         }
634
635         if (modRdnResult != null)
636         {
637             if (debug) System.out.println("\n\nparsed modrdnresult (DXAttribute): " + modRdnResult + "\n to mymodrdnCn(String): " + myModRdnCn);
638             entry.remove("modrdnresult");
639         }
640         return myModRdnCn;
641     }
642
643
644     /**
645      * Modifies the rdn of a directory. Compares the 'modrdnresult' value in the ldif file (if
646      * present) to the rdn of the entry. If the two don't match the program exits.
647      *
648      * @param entry contains dn to be modified.
649      * @param expectedModrdn a flag that indicates the value that the modify is expected to return.
650      */

651
652     public void modrdnEntry(DXEntry entry, String JavaDoc expectedModrdn)
653     {
654         String JavaDoc newDNString = entry.getString("newrdn");
655         DN newDN = new DN(newDNString);
656         if (debug) System.out.println("modrdn: " + entry.getDN() + "\n to: " + newDN);
657
658         boolean ret = false;
659
660         try
661         {
662             myOps.renameEntry(entry.getDN(), newDN);
663             ret = true;
664         }
665         catch (NamingException e)
666         {
667         }
668
669         if (ret == false)
670             error("modrdn operation failed for: " + entry, null);
671
672         int compare = -2; //TE: the flag to compare the expected DN with the actual DN.
673

674         compare = expectedModrdn.compareTo(newDNString); //TE: the compare of the expected DN with the actual DN.
675

676         if (compare != 0) //TE: if 0, the two results are the same, therefore the modify performed as expected.
677
{
678             if (debug) System.out.println("\n\nnewDN CN String: " + newDNString);
679             if (debug) System.out.println("EXPECTEDVALUE : " + expectedModrdn);
680             error("\nmodrdn operation failed for: " + entry + "\nExpected read result for cn: " + expectedModrdn + "\nActual result for cn: " + newDNString, null);
681         }
682     }
683
684
685     /**
686      * Modifies the attributes of an entry in a directory. Compares the 'returntype' value in the ldif file (if
687      * present) to the result of the modify operation. If the two don't match the program exits.
688      *
689      * @param entry contains the entry to be modified, and the list of att/val modification pairs (see ldif spec.).
690      * @param expectedValue a flag that indicates the modification success.
691      */

692
693     public void modifyEntry(DXEntry entry, boolean expectedValue)
694     {
695         if (debug) System.out.println("modify: " + entry);
696
697         Name myDN = entry.getDN();
698
699
700         /*TE: The three operations to modify an entry are:
701                                 add
702                                 delete
703                                 replace
704                 A test is done to see which operation is to be carried out.
705
706                 Note: within one ldif entry at this stage we can't do multiple operations
707                 on the same attribute or the same operations on multiple attributes
708                 because of the way the parser works,
709                 for example:
710                                 dn: cn=T,ou=Applications,ou=Customer,o=Democorp,c=AU
711                                 changetype: modify
712                                 add: drink
713                                 drink: red wine
714                                 drink: white wine
715                                 -
716                                 delete: drink
717                                 drink: red wine
718
719                 would result in both the 'drink' attributes being deleted. To delete just the
720                 'red wine' attribute, write another ldif entry,
721                 for example:
722                                 dn: cn=T,ou=Applications,ou=Customer,o=Democorp,c=AU
723                                 changetype: modify
724                                 delete: drink
725                                 drink: red wine
726
727                 however, deleting another attribute not used by the add operation should work fine
728                 for example:
729                                 dn: cn=T,ou=Applications,ou=Customer,o=Democorp,c=AU
730                                 changetype: modify
731                                 add: drink
732                                 drink: red wine
733                                 drink: white wine
734                                 -
735                                 delete: cn
736                                 cn: TEST
737         */

738
739         DXAttribute add = null;
740         DXAttribute delete = null;
741         DXAttribute replace = null;
742
743         add = (DXAttribute) entry.get("add");
744         delete = (DXAttribute) entry.get("delete");
745         replace = (DXAttribute) entry.get("replace");
746
747
748         //TE: Adds new attribute values to the specified attribute in the directory,
749
// for example:
750
// dn: cn=T,ou=Applications,ou=Customer,o=Democorp,c=AU
751
// changetype: modify
752
// add: drink
753
// drink: red wine
754
// drink: white wine
755
// drink: water
756
//
757
// will add three new 'drink' attributes to the entry.
758

759         if (add != null) //TE: tests if the modify operation is 'add'.
760
{
761             String JavaDoc attrString = "";
762             try
763             {
764                 attrString = add.get().toString();
765             }
766             catch (Exception JavaDoc e)
767             {
768             } // never happen
769

770             DXAttribute attr = (DXAttribute) entry.get(attrString);
771
772             boolean ret = false;
773
774             try
775             {
776                 myOps.addAttribute(myDN, attr);
777                 ret = true;
778             }
779             catch (NamingException e1)
780             {
781             }
782
783             //TE: flag to test if the modification succeeded or not.
784

785             if (ret != expectedValue) //TE: tests if the modification status is what we expected.
786
{
787                 if (debug) System.out.println("\n\nRET " + ret + " EXPECTEDVALUE " + expectedValue);
788                 if (debug) System.out.println("\nret equals expectedValue: " + (ret == expectedValue) + "\n");
789                 error("\nmodify-add operation failed for: " + entry + "\n", null);
790             }
791         }
792
793
794         //TE: Deletes attribute values of a specified attribute within a specified entry,
795
// for example:
796
// dn: cn=T,ou=Applications,ou=Customer,o=Democorp,c=AU
797
// changetype: modify
798
// delete: drink
799
// drink: red wine
800
// drink: white wine
801
// drink: water
802
//
803
// will delete these three 'drink' attributes from the entry.
804

805         if (delete != null) //TE: tests if the modify operation is 'delete'.
806
{
807             String JavaDoc attrString = "";
808             try
809             {
810                 attrString = delete.get().toString();
811             }
812             catch (Exception JavaDoc e)
813             {
814             } // never happen
815

816             DXAttribute attr = (DXAttribute) entry.get(attrString);
817
818             boolean ret = false;
819
820             try
821             {
822                 myOps.deleteAttribute(myDN, attr);
823                 ret = true;
824             }
825             catch (NamingException e1)
826             {
827             }
828
829             //TE: flag to test if the modification succeeded or not.
830

831             if (ret != expectedValue) //TE: tests if the modification status is what we expected.
832
{
833                 if (debug) System.out.println("\n\nRET " + ret + " EXPECTEDVALUE " + expectedValue);
834                 if (debug) System.out.println("\nret equals expectedValue: " + (ret == expectedValue) + "\n");
835                 error("\nmodify-delete operation failed for: " + entry + "\n", null);
836             }
837         }
838
839
840         //TE: Replaces all of the values of a specified attribute in the specified entry,
841
// for example:
842
// dn: cn=T,ou=Applications,ou=Customer,o=Democorp,c=AU
843
// changetype: modify
844
// replace: drink
845
// drink: beer
846
//
847
// will replace all of the current values of attribute 'drink' with value 'beer'.
848
// In other words, if there are currently multiple 'drink' attributes they all
849
// will be removed and replaced with this one new value.
850

851         if (replace != null) //TE: tests if the modify operation is 'replace'.
852
{
853             String JavaDoc attrString = "";
854             try
855             {
856                 attrString = replace.get().toString();
857             }
858             catch (Exception JavaDoc e)
859             {
860             } // never happen
861

862             DXAttribute attr = (DXAttribute) entry.get(attrString);
863
864             boolean ret = false;
865
866             try
867             {
868                 myOps.updateAttribute(myDN, attr);
869                 ret = true;
870             }
871             catch (NamingException e1)
872             {
873             }
874
875             //TE: flag to test if the modification succeeded or not.
876

877             if (ret != expectedValue) //TE: tests if the modification status is what we expected.
878
{
879                 if (debug) System.out.println("\n\nRET " + ret + " EXPECTEDVALUE " + expectedValue);
880                 if (debug) System.out.println("\nret equals expectedValue: " + (ret == expectedValue) + "\n");
881                 error("\nmodify-replace operation failed for: " + entry + "\n", null);
882             }
883         }
884     }
885
886
887     /**
888      * Gets the 'listresult' from the test ldif file and parses it into a string.
889      * 'searchresult' is an integer value pertaining to the amount of list results
890      * expected to be returned from a search. Converts to int. Removes the 'listresult' attribute.
891      *
892      * @param entry the DXEntry object containing a dn attribute, a changetype attribute, and any data attributes required.
893      * @return list an integer value representing the amount of entries that are expected to be returned by the list.
894      */

895
896     public int list(DXEntry entry)
897     {
898
899         DXAttribute listResult = (DXAttribute) entry.get("listresult");
900
901         String JavaDoc myListResult = "";
902         try
903         {
904             myListResult = listResult.get().toString();
905         }
906         catch (Exception JavaDoc e)
907         {
908         }
909
910         int list = -1;
911         try
912         {
913             list = Integer.parseInt(myListResult);
914         }
915         catch (Exception JavaDoc e)
916         {
917         }
918
919         if (listResult != null)
920         {
921             if (debug) System.out.println("\n\nparsed listresult(DXAttribute): " + listResult + "\n to myListResult(String): " + myListResult + ", to list(int): " + list);
922             entry.remove("listresult");
923         }
924         return list;
925     }
926
927
928     /**
929      * Lists an entry in a directory. Compares the 'listresult' value in the ldif file to the
930      * number of entries returned by the list. If the two are not equal the program exists.
931      *
932      * @param entry contains the dn to start searching from, and a 'filter' attribute with a search filter.
933      * @param expectedList a flag that indicates the number of entries that are expected to be listed.
934      */

935
936     public void listEntry(DXEntry entry, int expectedList)
937             throws NamingException
938     {
939         if (debug) System.out.println("\nlist: " + entry);
940         NamingEnumeration names = myOps.list(entry.getDN());
941
942         if (debug) out.println("\nlist of children:");
943
944         int i = 0; //TE: the flag to compare the expected number of list results with the actual number of list results.
945

946         while (names.hasMore()) //TE: Counts & lists entries.
947
{
948             i++;
949             if (debug) out.println(((NameClassPair) names.next()).getName());
950         }
951
952         if (i != expectedList)
953             error("\nList operation failed for: " + entry + "\nExpected list results: " + expectedList + "\nActual list results: " + i, null);
954     }
955
956
957     /**
958      * Gets the 'searchresult' value from the test ldif file and parses it into a string.
959      * 'searchresult' is an integer value pertaining to the amount of search results
960      * expected to be returned from a search. Converts to int. Removes the 'searchresult' attribute.
961      *
962      * @param entry the DXEntry object containing a dn attribute, a changetype attribute, and any data attributes required.
963      * @return search an integer value representing the amount of entries expected to be returned by the search.
964      */

965
966     public int search(DXEntry entry)
967     {
968
969         DXAttribute searchResult = (DXAttribute) entry.get("searchresult");
970
971         String JavaDoc mySearch = "";
972         try
973         {
974             mySearch = searchResult.get().toString();
975         }
976         catch (Exception JavaDoc e)
977         {
978         }
979
980         int search = -1;
981         try
982         {
983             search = Integer.parseInt(mySearch);
984         }
985         catch (Exception JavaDoc e)
986         {
987         }
988
989         if (searchResult != null)
990         {
991             if (debug) System.out.println("\n\nparsed searchresult(DXAttribute): " + searchResult + "\n to mySearch(String): " + mySearch + ", to myResult(int): " + search);
992             entry.remove("searchresult");
993         }
994         return search;
995     }
996
997
998     /**
999      * Searches a directory. Compares the 'searchresult' value in the ldif file to the
1000     * number of entries returned by the search. If the two are not equal the program exists.
1001     *
1002     * @param entry contains.
1003     * @param expectedSearch a flag that indicates the number of results returned by the search.
1004     * <ul>
1005     * <li> dn to start searching from
1006     * <li> a 'filter' attribute containing an ldap search filter
1007     * <li> an (optional) 'limit' attribute containing a number of returned entries limit
1008     * <li> an (optional) 'timeout' attribute containing a time limit (milliseconds).
1009     * </ul>
1010     */

1011
1012    public void searchEntry(DXEntry entry, int expectedSearch)
1013            throws NamingException
1014    {
1015        String JavaDoc filter = entry.getString("filter");
1016        String JavaDoc limit = entry.getString("limit");
1017        String JavaDoc timeout = entry.getString("timeout");
1018        if (limit == null) limit = "0";
1019        if (timeout == null) timeout = "0";
1020
1021        int lim = Integer.parseInt(limit);
1022        int time = Integer.parseInt(timeout);
1023
1024        if (debug) System.out.println("\nSEARCH " + entry + "\n filter: " + filter + "\n limit: " + limit + "\n timeout: " + timeout);
1025
1026
1027        NamingEnumeration names = myOps.searchSubTree(entry.getDN(), filter, lim, time);
1028        if (debug) out.println("\nNAMES: " + names + "\n\nDN: " + entry);
1029
1030        if (debug) out.println("\nsubtree search results:");
1031
1032        int i = 0; //TE: the flag to compare the expected number of search results with the actual number of search results.
1033

1034        while (names.hasMore()) //TE: Counts & lists search results.
1035
{
1036            i++;
1037            if (debug) out.println(((SearchResult) names.next()).getName());
1038        }
1039
1040        if (i != expectedSearch)
1041            error("\nSearch operation failed for: " + entry + "\nExpected search results: " + expectedSearch + "\nActual search results: " + i, null);
1042    }
1043
1044
1045    /**
1046     * Searches one level of a directory. Compares the 'searchresult' value in the ldif file to the
1047     * number of entries returned by the search one level. If the two are not equal the program exists.
1048     *
1049     * @param entry contains.
1050     * @param expectedSearch a flag that indicates the number of results returned by the search.
1051     * <ul>
1052     * <li> dn to start searching from.
1053     * <li> a 'filter' attribute containing an ldap search filter.
1054     * <li> an (optional) 'limit' attribute containing a number of returned entries limit.
1055     * <li> an (optional) 'timeout' attribute containing a time limit (milliseconds).
1056     * </ul>
1057     */

1058
1059    public void searchOneLevelEntry(DXEntry entry, int expectedSearch)
1060            throws NamingException
1061    {
1062        String JavaDoc filter = entry.getString("filter");
1063        String JavaDoc limit = entry.getString("limit");
1064        String JavaDoc timeout = entry.getString("timeout");
1065        if (limit == null) limit = "0";
1066        if (timeout == null) timeout = "0";
1067
1068        if (debug) System.out.println("\n\nSEARCHONELEVEL: " + entry + "\n filter: " + filter + "\n limit: " + limit + "\n timeout: " + timeout);
1069
1070        int lim = Integer.parseInt(limit);
1071        int time = Integer.parseInt(timeout);
1072
1073        NamingEnumeration names = myOps.searchOneLevel(entry.getDN(), filter, lim, time);
1074        if (debug) out.println("\n\none level search results:");
1075
1076        int i = 0; //TE: the flag to compare the expected number of search results with the actual number of search results.
1077

1078        while (names.hasMore()) //TE: Counts & lists search results.
1079
{
1080            i++;
1081            if (debug) out.println(((SearchResult) names.next()).getName());
1082        }
1083
1084        if (i != expectedSearch)
1085            error("\n\nSearchOneLevel operation failed for: " + entry + "\n\nExpected search results: " + expectedSearch + "\nActual search results: " + i, null);
1086    }
1087
1088
1089    /**
1090     * Gets the 'readresultcn' value from the test ldif file and parses into a string.
1091     * 'readresultcn' is the 'cn' attribute of the entry, for example cn: Cora BALDWIN.
1092     * Removes the 'readresultcn' attribute. Note: 'readresultcn' can be configured to
1093     * represent any string attribute.
1094     *
1095     * @param entry the DXEntry object containing a dn attribute, a changetype attribute, and any data attributes required.
1096     * @return myReadCn a string 'cn' that is expected to be returned by the read.
1097     */

1098
1099    public String JavaDoc read(DXEntry entry)
1100    {
1101        DXAttribute readResultCn = (DXAttribute) entry.get("readresultcn");
1102
1103        String JavaDoc myReadCn = "";
1104        try
1105        {
1106            myReadCn = readResultCn.get().toString();
1107        }
1108        catch (Exception JavaDoc e)
1109        {
1110        }
1111
1112        if (readResultCn != null)
1113        {
1114            if (debug) System.out.println("\n\nparsed readresultcn (DXAttribute): " + readResultCn + "\n to myReadCn(String): " + myReadCn);
1115            entry.remove("readresultcn");
1116        }
1117        return myReadCn;
1118    }
1119
1120
1121    /**
1122     * Reads an entry in a directory. Currently compares the 'readresultcn' value in the ldif file
1123     * (can be configured to any value), to the 'cn' value of the result of the read. If the two
1124     * values are not equal the program exits.
1125     *
1126     * @param entry contains the dn to list the attributes of.
1127     * @param expectedRead a flag that indicates the value that the read is expected to return.
1128     */

1129
1130    public void readEntry(DXEntry entry, String JavaDoc expectedRead)
1131            throws NamingException
1132    {
1133        if (debug) System.out.println("\nread: " + entry);
1134
1135        Attributes atts = myOps.read(entry.getDN());
1136        if (atts == null)
1137        {
1138            throw new NamingException("\nUnable to read entry " + entry.getDN());
1139        }
1140
1141        Attribute readCn = (Attribute) atts.get("cn");
1142        if (debug) out.println("\nREAD: " + readCn);
1143
1144        String JavaDoc myReadCn = "";
1145        try
1146        {
1147            myReadCn = readCn.get().toString();
1148        }
1149        catch (Exception JavaDoc e)
1150        {
1151        }
1152
1153        int compare = -2; //TE: the flag to compare the expected read with the actual read.
1154

1155        compare = expectedRead.compareTo(myReadCn); //TE: the compare of the expected read with the actual read.
1156

1157        if (compare != 0) //TE: if 0, the two results are the same, therefore the read performed as expected.
1158
{
1159            if (debug) out.println("\n\nREAD CN String: " + myReadCn);
1160            if (debug) out.println("EXPECTEDVALUE : " + expectedRead);
1161            error("\nRead operation failed for: " + entry + "\nExpected read result for cn: " + expectedRead + "\nActual read result for cn: " + myReadCn, null);
1162        }
1163
1164        DXEntry val = new DXEntry(atts);
1165        if (debug) out.println(val);
1166    }
1167
1168
1169    /**
1170     * Gets the 'copyTo' value from the test ldif file and parses into a string.
1171     * 'copyTo' is the 'DN' of the new entry to where the DN of the old entry gets copied to.
1172     * Removes the 'copyTo' attribute.
1173     *
1174     * @param entry the DXEntry object containing a dn attribute, a changetype attribute, and any data attributes required.
1175     * @param command the type of test to be performed, i.e copy.
1176     * @return myCopyTo the DN of the where the old entry is to be copy (or moved) to.
1177     */

1178
1179    public String JavaDoc copy(DXEntry entry, DXAttribute command)
1180    {
1181        DXAttribute copyTo = (DXAttribute) entry.get("copyTo");
1182
1183        String JavaDoc myCopyTo = "";
1184        try
1185        {
1186            myCopyTo = copyTo.get().toString();
1187        }
1188        catch (Exception JavaDoc e)
1189        {
1190        }
1191
1192        if (copyTo != null)
1193        {
1194            if (debug) System.out.println("copyTo: " + copyTo + "\ncommand: " + command + "\nmyCopyTo: " + myCopyTo);
1195            if (debug) System.out.println("\n\nparsed copyTo (DXAttribute): " + copyTo + "\n to myCopyTo(String): " + myCopyTo);
1196            entry.remove("copyTo");
1197        }
1198        return myCopyTo;
1199    }
1200
1201
1202    /**
1203     * Copies an entry to a new DN.
1204     *
1205     * @param oldEntry is what is being copied.
1206     * @param newEntry is the new DN of where the entry is copied to.
1207     */

1208
1209    public void copy(DXEntry oldEntry, String JavaDoc newEntry)
1210    {
1211
1212
1213        Name newDN = myOps.postParse(newEntry); //TE: Converts the new DN string (copyTo) from the LDIF file into Name.
1214
Name oldDN = oldEntry.getDN();
1215
1216        if (debug) System.out.println("old DN: " + oldDN);
1217        if (debug) System.out.println("new DN: " + newDN);
1218
1219        NamingEnumeration namesOld;
1220
1221        try
1222        {
1223            namesOld = myOps.list(oldDN);
1224        }
1225        catch (NamingException e1)
1226        {
1227            System.err.println("failed getting old names");
1228            e1.printStackTrace();
1229            return;
1230        }
1231
1232        if (debug) out.println("\nCopy of OLD children:");
1233
1234        int n = 0; //TE: Counter for OLD entries.
1235

1236        try
1237        {
1238            while (namesOld.hasMore()) //TE: Counts & lists OLD entries.
1239
{
1240                n++;
1241                if (debug) out.println("Old Entries: " + ((NameClassPair) namesOld.next()).getName());
1242            }
1243        }
1244        catch (Exception JavaDoc e)
1245        {
1246            if (debug) System.out.println("List for OLD entries failed during copy process");
1247        }
1248
1249        try
1250        {
1251            myOps.copyTree(oldDN, newDN);
1252        }
1253        catch (NamingException e2)
1254        {
1255            System.err.println("error in copyTree()");
1256            e2.printStackTrace();
1257        }
1258
1259        //TE: copies old entry to new entry.
1260

1261        if (debug) System.out.println("Copy: " + oldEntry);
1262        NamingEnumeration namesNew;
1263
1264        try
1265        {
1266            namesNew = myOps.list(newDN);
1267        }
1268        catch (NamingException e3)
1269        {
1270            System.err.println("error in getting new list");
1271            e3.printStackTrace();
1272            return;
1273        }
1274
1275
1276        if (debug) out.println("\nCopy of NEW children:");
1277
1278        int i = 0; //TE: Counter for NEW entries.
1279

1280        try
1281        {
1282            while (namesNew.hasMore()) //TE: Counts & lists NEW entries.
1283
{
1284                i++;
1285                if (debug) out.println("New Entries: " + ((NameClassPair) namesNew.next()).getName());
1286            }
1287        }
1288        catch (Exception JavaDoc e)
1289        {
1290            if (debug) System.out.println("List for NEW entries failed during copy process");
1291        }
1292
1293        if (i != n) //TE: checks that the list contains the same number of entries.
1294
error("\nCopy operation failed for: " + oldEntry + "\nExpected number of copied entries: " + n + "\nActual number of copied entries: " + i, null);
1295    }
1296
1297
1298    /**
1299     * Gets the 'cutTo' value from the test ldif file and parses into a string.
1300     * 'cutTo' is the 'DN' of the new entry to where the DN of the old entry gets cut (or moved) to.
1301     * Removes the 'cutTo' attribute.
1302     *
1303     * @param entry the DXEntry object containing a dn attribute, a changetype attribute, and any data attributes required.
1304     * @param command the type of test to be performed, i.e cut.
1305     * @return myCutTo the DN of the where the old entry is to be cut (or moved) to.
1306     */

1307
1308    public String JavaDoc cut(DXEntry entry, DXAttribute command)
1309    {
1310        DXAttribute cutTo = (DXAttribute) entry.get("cutTo");
1311
1312        String JavaDoc myCutTo = "";
1313        try
1314        {
1315            myCutTo = cutTo.get().toString();
1316        }
1317        catch (Exception JavaDoc e)
1318        {
1319        }
1320
1321        if (cutTo != null)
1322        {
1323            if (debug) System.out.println("cutTo: " + cutTo + "\ncommand: " + command + "\nmyCutTo: " + myCutTo);
1324            if (debug) System.out.println("\n\nparsed cutTo (DXAttribute): " + cutTo + "\n to myCutTo(String): " + myCutTo);
1325            entry.remove("cutTo");
1326        }
1327        return myCutTo;
1328    }
1329
1330
1331    /**
1332     * Cuts an entry to a new DN.
1333     *
1334     * @param oldEntry is what is being cut.
1335     * @param newEntry is the new DN of where the entry is cut to.
1336     */

1337
1338    public void cut(DXEntry oldEntry, String JavaDoc newEntry)
1339    {
1340
1341
1342        Name newDN = myOps.postParse(newEntry); //TE: Converts the new DN string (copyTo) from the LDIF file into Name.
1343
Name oldDN = oldEntry.getDN();
1344
1345        if (debug) System.out.println("old: " + oldDN);
1346        if (debug) System.out.println("new: " + newDN);
1347
1348        NamingEnumeration namesOld;
1349
1350        try
1351        {
1352            namesOld = myOps.list(oldDN);
1353        }
1354        catch (NamingException e1)
1355        {
1356            System.err.println("error getting namesOld");
1357            e1.printStackTrace();
1358            return;
1359        }
1360
1361        if (debug) out.println("\nCut of OLD children:");
1362
1363        int n = 0; //TE: Counter for OLD entries.
1364

1365        try
1366        {
1367            while (namesOld.hasMore()) //TE: Counts & lists OLD entries.
1368
{
1369                n++;
1370                if (debug) out.println("Old Entries: " + ((NameClassPair) namesOld.next()).getName());
1371            }
1372        }
1373        catch (Exception JavaDoc e)
1374        {
1375            if (debug) System.out.println("List for OLD entries failed during cut process");
1376        }
1377
1378        try
1379        {
1380            myOps.moveTree(oldDN, newDN);
1381        }
1382        catch (NamingException e2)
1383        {
1384            System.err.println("error in moveTree()");
1385            e2.printStackTrace();
1386        } //TE: cuts (or moves) old entry to new entry.
1387

1388        if (debug) System.out.println("Copy: " + oldEntry);
1389        NamingEnumeration namesNew;
1390
1391        try
1392        {
1393            namesNew = myOps.list(newDN);
1394        }
1395        catch (NamingException e3)
1396        {
1397            System.err.println("error getting namesNew");
1398            e3.printStackTrace();
1399            return;
1400        }
1401
1402
1403        if (debug) out.println("\nCut of NEW children:");
1404
1405        int i = 0; //TE: Counter for NEW entries.
1406

1407        try
1408        {
1409            while (namesNew.hasMore()) //TE: Counts & lists NEW entries.
1410
{
1411                i++;
1412                if (debug) out.println("New Entries: " + ((NameClassPair) namesNew.next()).getName());
1413            }
1414        }
1415        catch (Exception JavaDoc e)
1416        {
1417            if (debug) System.out.println("List for NEW entries failed during cut process");
1418        }
1419
1420        if (i != n) //TE: checks that the list contains the same number of entries.
1421
error("\nCut operation failed for: " + oldEntry + "\nExpected number of cut entries: " + n + "\nActual number of cut entries: " + i, null);
1422    }
1423
1424
1425    /**
1426     * Opens a connection.
1427     *
1428     * @param entry a 'fake' entry with no dn, but a bunch of attributes.
1429     */

1430
1431    public void connect(DXEntry entry)
1432    {
1433        if (debug) System.out.println("connect: " + entry);
1434        if (myOps != null)
1435            try
1436            {
1437                myOps.close();
1438            }
1439            catch (NamingException e)
1440            {
1441                System.err.println("error in myOps.close()");
1442                e.printStackTrace();
1443            }
1444
1445        String JavaDoc url = entry.getString("url");
1446        String JavaDoc user = entry.getString("user");
1447        String JavaDoc pwd = entry.getString("pwd");
1448        String JavaDoc tracing = entry.getString("tracing");
1449        String JavaDoc version = entry.getString("ldapVersion");
1450        String JavaDoc referral = entry.getString("referral");
1451        String JavaDoc useSSL = entry.getString("useSSL");
1452
1453        boolean trace = ((tracing != null) && (tracing.equalsIgnoreCase("true")));
1454        boolean ssl = ((useSSL != null) && (useSSL.equalsIgnoreCase("true")));
1455        openConnection(url, user, pwd, trace, version, referral, ssl);
1456    }
1457
1458
1459    /**
1460     * Disconnected from the directory.
1461     */

1462
1463    public void disconnect(DXEntry entry)
1464    {
1465        if (debug) System.out.println("disconnected. ");
1466        try
1467        {
1468            myOps.close();
1469        }
1470        catch (NamingException e)
1471        {
1472            System.err.println("error in myOps.close()");
1473            e.printStackTrace();
1474        }
1475    }
1476
1477
1478    /**
1479     * Prints error message then terminates.
1480     */

1481
1482    public void error(String JavaDoc msg, Exception JavaDoc e)
1483    {
1484        out.println(msg + "\n");
1485
1486        if (e != null && printstack)
1487            e.printStackTrace();
1488
1489        if (terminating)
1490            System.exit(-1);
1491    }
1492}
Popular Tags