[ Team LiB ] Previous Section Next Section

25.5 Incorporating Searches into ASP

ADO searches can be easily incorporated into ASPs using the information in this chapter and Chapter 20. In this first example, we will navigate through a resultset using server-side scripts in order to populate a table that gets created dynamically. To make it easier to understand, Figure 25-1 is what the final result should look like for a new server with very few users.

Figure 25-1. A navigable table on a web page populated by ADO
figs/ads2.2501.gif

This ASP includes all its code in the body of the web page. To begin with, we must retrieve the resultset:

<%
  Set objConn = CreateObject("ADODB.Connection")
  objConn.Provider = "ADSDSOObject"
  objConn.Open "", _
    "CN=Administrator,CN=Users,dc=mycorp,dc=com", ""
   
  Set objRS = objConn.Execute _
    ("<LDAP://dc=mycorp,dc=com>;" _
      & "(objectClass=User);Name,ADsPath;SubTree")
%>

Having done this, we can now begin to create the table. The table definition must include the number of columns. Even though we know that we are retrieving two columns, we will include the value returned from the query rather than hardcoding a value of 2 into the table so that we can extend the page later. The table definition then looks like this:

<TABLE BORDER=1 COLS=<% = objRS.Fields.Count%>>

Now we need to include column headings. Again, if we take these directly from the query, then we can expand the query much more easily later:

  <TR>
    <% For Each adoField In objRS.Fields %>
         <TH> <% = adoField.Name %> </TH>
    <% Next %>
  </TR>

Now we can navigate through the actual resultset and populate the table. Each row is created via the <TR>...</TR> pair of tags by navigating through the resultset using a Do While...Loop construct. As soon as we go past the end of the loop, the table closing tag is sent. Each individual row is populated using a For...Each loop:

  <% Do While Not objRS.EOF %>
    <TR>
      <% For Each adoField In objRS.Fields %>
         ' Populate the cells here
      <% Next 
      objRS.MoveNext %>
    </TR>
  <% Loop %>
</TABLE>

Each cell within each column of that row is created using the <TD> tag within that For loop, like so:

<TD ALIGN=LEFT>
<% If IsNull(adoField) Then 
     Response.Write ""
   Else
     Response.Write adoField.Value
   End If %>
</TD>

The whole section of code comes together in Example 25-4. Ignore the first line for now; we'll come back to it in a minute.

Example 25-4. Incorporating searches into ASP
<!--#include file="adovbs.inc" -->
<html>
<HEAD>
<TITLE>Navigating a simple resultset using ADSI, ADO and ASP</TITLE>
</HEAD>
   
<BODY>
<%
  On Error Resume Next
   
  Set objConn = CreateObject("ADODB.Connection")
  objConn.Provider = "ADSDSOObject"
  objConn.Open "", _
    "CN=Administrator,CN=Users,dc=mycorp,dc=com", ""
   
  Set objRS = objConn.Execute _
    ("<LDAP://dc=mycorp,dc=com>;" _
      & "(objectClass=User);Name,ADsPath;SubTree")
%>
   
<TABLE BORDER=1 COLS=<% = objRS.Fields.Count%>> 
  <TR>
    <% For Each oField In objRS.Fields %>
         <TH> <% = oField.Name %> </TH>
    <% Next %>
  </TR>
  <% Do While Not objRS.EOF %>
    <TR>
      <% For Each oField In objRS.Fields %>
           <TD ALIGN=LEFT>
           <% If IsNull(oField) Then 
                Response.Write "&nbsp;"
              Else
                Response.Write oField.Value
              End If %>
           </TD>  

      <% Next 
      objRS.MoveNext %>
    </TR>
  <% Loop %>
</TABLE>
   
<%
  objConn.Close
  Set objRS = Nothing
%>
   
</BODY>
</HTML>

25.5.1 ASP Searches Allowing User Navigation of a Resultset

We'll now go through a rather more complex example so that you can see how to allow users to navigate through a resultset. This example came from a need to be able to display the name, description, and ADsPath of every object in the tree in a simple fashion on a web page. The most obvious solution was to use an ADO resultset with Move First, Move Last, Previous, and Next buttons to step through it. Once the simple example is assembled, we will expand it to include a demonstration of filters.

The ASP is split up as usual between the server-side script and the HTML of the web page itself. The resultset is retrieved as part of the server-side script and looks identical to those we considered earlier:

<%
  Set objConn = CreateObject("ADODB.Connection")
  objConn.Provider = "ADSDSOObject"
  objConn.Open "", _
    "CN=Administrator,CN=Users,dc=mycorp,dc=com", ""
   
  Set objRS = objConn.Execute _
    ("<LDAP://dc=mycorp,dc=com>;(objectClass=User);ADsPath;SubTree")
%>

You may need to add either Microsoft Data Access Components (MDAC) or ADO components to your installation of IIS before IIS will accept ADO on web pages. If you find that you are getting continual errors with simple ADO queries, you may have forgotten to install the relevant components so that IIS can interpret ADO code.

We'll leave the server-side script for now and concentrate on the HTML elements. The web page needs to display the name, description, and ADsPath of the user. To do that, we need to bind to the user via the ADsPath of the current record of the resultset. We will use IADsOpenDSObject::OpenDSObject here, although GetObject would do just as well:

<% 
  strUsername = "cn=Administrator,cn=Users,dc=mycorp,dc=com"
  strPassword = ""
   
  Set objNamespace = GetObject("LDAP:")
  Set objUser = objNamespace.OpenDSObject(objRS("ADsPath"), _
    strUsername,strPassword,0)
   
  Response.Write "Name: <B>" & objUser.Name & "</B><P>"
  Response.Write "ADsPath: <B>" & objUser.ADsPath & "</B><P>"
  Response.Write "Description: <B>" & objUser.Description & "</B><P>"
%>

For the form itself, we've made sure that the Previous button is not displayed at the first resultset record and that the Next button is not displayed at the final resultset record. This prevents the resultset from going out of range, and is simple to do using server-side scripting within the HTML code by checking the Recordset::AbsolutePosition and Recordset::RecordCount properties:

<FORM METHOD="POST" ACTION="rs_demo.asp">
  <% If objRS.AbsolutePosition = objRS.RecordCount Then %>
    <INPUT TYPE="SUBMIT" NAME="Previous" VALUE="Previous">
  <% ElseIf objRS.AbsolutePosition = 1 Then %>
    <INPUT TYPE="SUBMIT" NAME="Next" VALUE="Next">
  <% Else %>
    <INPUT TYPE="SUBMIT" NAME="Previous" VALUE="Previous">
    <INPUT TYPE="SUBMIT" NAME="Next" VALUE="Next">
  <% End If %>
   
  <INPUT TYPE="SUBMIT" NAME="First" VALUE="Move First">
  <INPUT TYPE="SUBMIT" NAME="Last" VALUE="Move Last">
</FORM>

This is essentially it for the client HTML code. Before looking at the server-side code, there is one HTML line we have to add for ADO prior to anything else on the page:

<!--#include file="adovbs.inc" -->

This line is known as a Server-side Include (SSI) and is used to include all the ADO constants you may wish to use in your ASP without having to redeclare them yourself. This file is installed with the ADO component of IIS in the \ProgramFiles\Common Files\System\ado directory as a text file so you can easily open it and look through the constants that are available to you. If you are using JScript, you need to use adojavas.inc instead.

After including the code to retrieve the resultset, we now need to include the code to navigate that resultset according to which buttons are clicked on the form. However, at this point we have a problem. Because the page reloads the resultset every time, the current record will always be the first no matter what button is selected. For this web page to properly navigate a resultset, we will have to maintain some sort of indicator to the current record between refreshes of each page. This is very easy to do using the HIDDEN attribute of fields on the existing form. All we need to do is set up an extra entry anywhere on the form that includes a reference to the current record. We can do this using the Recordset::AbsolutePosition of the resultset:

<INPUT TYPE="HIDDEN" NAME="AbsPosition" 
  VALUE="<% = objRS.AbsolutePosition %>">

If we do this, whenever the form is submitted, the current record's position is transmitted with the form.

There is one problem with using Recordset::AbsolutePosition in this example: the resultset may not be static throughout every query. If users are being created and deleted while the page is being accessed, there is a chance, however small, that the current record may disappear between page refreshes or that a navigation moves to a new record that did not previously exist. Solutions to this problem are discussed later in the chapter.

Assuming that we do this, we can navigate through the new resultset using the following code:

<%
  If Request.Form("Next") <> "" Then
    objRS.AbsolutePosition = Request.Form("AbsPosition") + 1
  ElseIf Request.Form("Previous") <> "" Then
    objRS.AbsolutePosition = Request.Form("AbsPosition") - 1
  ElseIf Request.Form("First") <> "" Then
    objRS.MoveFirst
  ElseIf Request.Form("Last") <> "" Then
    objRS.MoveLast
  End If
%>

At this point, the code is essentially complete. Example 25-5 shows it in its entirety.

Example 25-5. Navigating through a Resultset
<!--#include file="adovbs.inc" -->
<%
  Set objConn = CreateObject("ADODB.Connection")
  objConn.Provider = "ADSDSOObject"
  objConn.Open "", _
    "CN=Administrator,CN=Users,dc=mycorp,dc=com", ""
   
  Set objRS = objConn.Execute _
    ("<LDAP://dc=mycorp,dc=com>;(objectClass=User);ADsPath;SubTree")
   
  If Request.Form("Next") <> "" Then
    objRS.AbsolutePosition = Request.Form("AbsPosition") + 1
  ElseIf Request.Form("Previous") <> "" Then
    objRS.AbsolutePosition = Request.Form("AbsPosition") - 1
  ElseIf Request.Form("First") <> "" Then
    objRS.MoveFirst
  ElseIf Request.Form("Last") <> "" Then
    objRS.MoveLast
  End If
%>
   
<! Start the main page>
<html>
<HEAD>
<TITLE>Recordset Navigation using ADSI, ADO and ASP</TITLE>
</HEAD>
   
<BODY>
<% 
  strUsername = "cn=Administrator,cn=Users,dc=mycorp,dc=com"
  strPassword = ""
   
  Set objNamespace = GetObject("LDAP:")
  Set objUser = objNamespace.OpenDSObject(objRS("ADsPath"), _
    strUsername,strPassword,0)
   
  Response.Write "Name: <B>" & objUser.Name & "</B><P>"
  Response.Write "ADsPath: <B>" & objUser.ADsPath & "</B><P>"
  Response.Write "Description: <B>" & objUser.Description & "</B><P>"
%>
<FORM METHOD="POST" ACTION="rs_demo.asp">
  <INPUT TYPE="HIDDEN" NAME="AbsPosition" 
    VALUE="<% = objRS.AbsolutePosition %>">
  <% If objRS.AbsolutePosition = objRS.RecordCount Then %>
    <INPUT TYPE="SUBMIT" NAME="Previous" VALUE="Previous">
  <% ElseIf objRS.AbsolutePosition = 1 Then %>
    <INPUT TYPE="SUBMIT" NAME="Next" VALUE="Next">
  <% Else %>
    <INPUT TYPE="SUBMIT" NAME="Previous" VALUE="Previous">
    <INPUT TYPE="SUBMIT" NAME="Next" VALUE="Next">
  <% End If %>
   
  <INPUT TYPE="SUBMIT" NAME="First" VALUE="Move First">
  <INPUT TYPE="SUBMIT" NAME="Last" VALUE="Move Last">
</FORM>
   
</BODY>
</HTML>

25.5.2 Enhancing the User Navigation ASP

There are a number of enhancements that you can make to the code, not only to make it more user friendly but also to demonstrate the use of filtering an existing resultset. We'll deal with these enhancements individually and then combine them all at the end into an expanded ASP incorporating all of the enhancements.

25.5.2.1 Empty resultsets

Occasionally, you will write an ASP that generates an empty resultset. If that is the case, we should make sure that the page handles this properly. We could print a simple message and stop processing the page any further. In addition, we could provide a Restart button that could reload the page from scratch if desired. Here is the section of code to accomplish this:

<%
  If objRS.EOF Then
    Response.Write("No users found!")
%>
    <FORM METHOD="POST" ACTION="rs_demo.asp">
      <INPUT TYPE="SUBMIT" NAME="Restart" VALUE="Restart">
    </FORM>
<%
    objConn.Close
    Set objRS = Nothing
    Set objConn = Nothing
    Response.End
  End If
%>
25.5.2.2 Starting from scratch

Occasionally, it would be nice to wipe the resultset clean and start again from scratch at the first item. This is easy to achieve using another button on the form:

<INPUT TYPE="SUBMIT" NAME="Restart" VALUE="Restart">

If the page is opened using the Restart button, we reload the page from scratch by using the trick of redirecting the browser to the page. This triggers the browser to get a new copy of the page, clearing all the values set by the form on previous pages. The server-side code looks like this:

<%
  If Request.Form("Restart") = "Restart" Then
    Response.Redirect("rs_demo.asp")
    Response.End
  End If
%>
25.5.2.3 Filters

To include filters on an existing resultset, we need to monitor two extra values when the form is submitted. First, we need to know that a filter is being applied to the resultset, so that we can reapply it when the page is refreshed after the submission of a form. Second, we need to keep a copy of the actual filter itself. The first is easily taken care of by another hidden field in the form:

<INPUT TYPE="HIDDEN" NAME="IsFilterOn" VALUE="<% = bolFilter %>">

To set this value, we need to include some server-side code to cope with the fact that the value will not be set for the first-ever access to the page. We can do it like this:

<%
  If Request.Form("IsFilterOn") = "FALSE" Then
    bolFilter = "FALSE"
  ElseIf Request.Form("IsFilterOn") = "TRUE" Then
    bolFilter = "TRUE"
  Else
    bolFilter = "FALSE"
  End If
%>

We could just as easily use an INPUT field of type CHECKBOX here if desired. This is a Boolean input and would work just as well.

The second requirement can be taken care of by an INPUT field on the form:

<INPUT TYPE="TEXT" NAME="FilterText" VALUE= "<% = strFilter %>">

We would also like to include a default value for this filter, so strFilter needs to be set somewhere prior to the form itself. After the page has been accessed once, the value of this field entry will have been set. From then on, we should be able to use the existing value of this field as the base for the input field. Here is the code:

<% 
  If CStr(Request.Form("FilterText")) = "" Then 
    strFilter = "Name LIKE 'a*'"
  Else 
    strFilter = CStr(Request.Form("FilterText"))
  End If
%>

We also need some way of being able to set and remove the filter on the resultset. As there will always be a value in the filter field, we cannot use this to trigger the addition of a filter to the resultset. Once again, the simplest solution is to use two more buttons on the form:

<INPUT TYPE="SUBMIT" NAME="SetFilter" VALUE="Set Filter!">
<INPUT TYPE="SUBMIT" NAME="EraseFilter" VALUE="Erase Filter!">

We now can write the code that actually applies and removes the filter using these two buttons:

ElseIf Request.Form("SetFilter") <> "" Then
  objRS.Filter = CStr(Request.Form("FilterText"))
  bolFilter = "TRUE"
ElseIf Request.Form("EraseFilter") <> "" Then
  objRS.Filter = adFilterNone
  bolFilter = "FALSE"

There is still one small piece of code missing. While we can set a filter using the appropriate button, as soon as we begin to navigate the filtered resultset, we will be clicking other buttons. We need to make sure that the filter is applied while the IsFilterOn field is true. To do this, we add an extra line to the code that sets bolFilter, which we laid out earlier. The code should now look like this:

If Request.Form("IsFilterOn") = "FALSE" Then
  bolFilter = "FALSE"
ElseIf Request.Form("IsFilterOn") = "TRUE" Then
  objRS.Filter = CStr(Request.Form("FilterText"))
  bolFilter = "TRUE"
Else
  bolFilter = "FALSE"
End If

This makes sure that the filter is always applied after it has been initially set.

25.5.2.4 Displaying the location of individual records

We will add two other lines of code to the ASP. While they are not strictly necessary, these two lines serve to demonstrate how resultsets work:

This is user number <% = objRS.AbsolutePosition %>.
There are <% = objRS.RecordCount %> users in the recordset.<P>

The last line will always tell you how many records you can currently navigate through in the resultset. If there are 12 records and you have no filter, the result will be 12. If you have 12 records but have a filter that limits it to 4, the result will be 4. However, the first line always refers to the record number out of the entire recordset total, regardless of whether a filter has been set.

This means that including the following code can lead to undesired results:

<P>This is user <% = objRS.AbsolutePosition %> out of a total of 
<% = objRS.RecordCount %> users in the recordset.</P>

If a filter were applied to a resultset that limited the resultset to the entries 1, 3, 8, and 9, when you navigated between the four results, you would receive the following responses:

This is user 1 out of a total of four users in the recordset.
This is user 3 out of a total of four users in the recordset.
This is user 8 out of a total of four users in the recordset.
This is user 9 out of a total of four users in the recordset.

It is important to understand this distinction.

25.5.2.5 The enhanced ASP search

Example 25-6 lists the code for the enhanced version of the ASP ADO search.

Example 25-6. The enhanced version of the ASP ADO search
<!--#include file="adovbs.inc" -->
<%
  ' If the page is opened using the Restart button then reload the page 
  ' from scratch by redirecting to the page itself
  If Request.Form("Restart") = "Restart" Then
    Response.Redirect("rs_demo.asp")
    Response.End
  End If
   
  ' Retrieve the Resultset
  Set objConn = CreateObject("ADODB.Connection")
  objConn.Provider = "ADSDSOObject"
  objConn.Open "", _
    "CN=Administrator,CN=Users,dc=mycorp,dc=com", ""
   
  Set objRS = objConn.Execute _
    ("<LDAP://dc=mycorp,dc=com>;(objectClass=User);ADsPath;SubTree")
   
  If Request.Form("IsFilterOn") = "FALSE" Then
    bolFilter = "FALSE"
  ElseIf Request.Form("IsFilterOn") = "TRUE" Then
    objRS.Filter = CStr(Request.Form("FilterText"))
    bolFilter = "TRUE"
  Else
    bolFilter = "FALSE"
  End If
   
  If Request.Form("Next") <> "" Then
    objRS.AbsolutePosition = Request.Form("AbsPosition") + 1
  ElseIf Request.Form("Previous") <> "" Then
    objRS.AbsolutePosition = Request.Form("AbsPosition") - 1
  ElseIf Request.Form("First") <> "" Then
    objRS.MoveFirst
  ElseIf Request.Form("Last") <> "" Then
    objRS.MoveLast
  ElseIf Request.Form("SetFilter") <> "" Then
    objRS.Filter = CStr(Request.Form("FilterText"))
    bolFilter = "TRUE"
  ElseIf Request.Form("EraseFilter") <> "" Then
    objRS.Filter = adFilterNone
    bolFilter = "FALSE"
  End If
   
  ' If no results were returned, then end the session
  ' but provide a restart button
  If objRS.EOF Then
    Response.Write("No users found!")
%>
    <FORM METHOD="POST" ACTION="rs_demo.asp">
      <INPUT TYPE="SUBMIT" NAME="Restart" VALUE="Restart">
    </FORM>
<%
    objConn.Close
    Set objRS = Nothing
    Set objConn = Nothing
    Response.End
  End If
%>
   
<! Start the main page>
<html>
<HEAD>
<TITLE>Recordset Navigation using ADSI, ADO and ASP</TITLE>
</HEAD>
   
<BODY>
This is user number <% = objRS.AbsolutePosition %>.
There are <% = objRS.RecordCount %> users in the recordset.<P>
   
<% 
  If CStr(Request.Form("FilterText")) = "" Then 
    strFilter = "Name LIKE 'a*'"
  Else 
    strFilter = CStr(Request.Form("FilterText"))
  End If
  strUsername = "cn=Administrator,cn=Users,dc=mycorp,dc=com"
  strPassword = ""
   
  Set objNamespace = GetObject("LDAP:")
  Set objUser = objNamespace.OpenDSObject(objRS("ADsPath"), _
    strUsername,strPassword,0)
   
  Response.Write "Name: <B>" & objUser.Name & "</B><P>"
  Response.Write "ADsPath: <B>" & objUser.ADsPath & "</B><P>"
  Response.Write "Description: <B>" & objUser.Description & "</B><P>"
  Response.Write "Class: <B>" & objUser.Schema & "</B><P>"
%>
   
<FORM METHOD="POST" ACTION="rs_demo.asp">
  <INPUT TYPE="SUBMIT" NAME="Restart" VALUE="Restart">
  <INPUT TYPE="HIDDEN" NAME="AbsPosition" 
    VALUE="<% = objRS.AbsolutePosition %>">
  <INPUT TYPE="HIDDEN" NAME="IsFilterOn" VALUE="<% = bolFilter %>">
   
  <% If objRS.AbsolutePosition = objRS.RecordCount Then %>
    <INPUT TYPE="SUBMIT" NAME="Previous" VALUE="Previous">
  <% ElseIf objRS.AbsolutePosition = 1 Then %>
    <INPUT TYPE="SUBMIT" NAME="Next" VALUE="Next">
  <% Else %>
    <INPUT TYPE="SUBMIT" NAME="Previous" VALUE="Previous">
    <INPUT TYPE="SUBMIT" NAME="Next" VALUE="Next">
  <% End If %>
   
  <INPUT TYPE="SUBMIT" NAME="First" VALUE="Move First">
  <INPUT TYPE="SUBMIT" NAME="Last" VALUE="Move Last">
  <P>
  <INPUT TYPE="TEXT" NAME="FilterText" VALUE= "<% = strFilter %>">
  <INPUT TYPE="SUBMIT" NAME="SetFilter" VALUE="Set Filter!">
  <INPUT TYPE="SUBMIT" NAME="EraseFilter" VALUE="Erase Filter!">
</FORM>
   
</BODY>
</HTML>
25.5.2.6 Problems with this example

As mentioned earlier, there is the problem that the resultset may not be static throughout every query. One way around this is to pass the ADsPath of the user back in a hidden field, and once the query is executed, confirm that the current record of the new query is the same current record of the old query prior to performing any actions from the buttons. If there were a problem, you could pop up a MsgBox or write some text to the screen to that effect.

We have not integrated the use of bookmarks into this code, since Microsoft specifically warns against moving to a record in a resultset using a bookmark from another query. Because the query is executed again each time the page is loaded, the use of bookmarks is not appropriate. While the bookmarks for the ADSI OLE DB provider are currently only a copy of the Recordset::AbsolutePosition field, it would be wise to follow Microsoft's advice in case they change the format in the future.

25.5.3 Other Ideas for Expansion

There are many other ways that you could extend the look and functionality of the existing code. For example, you could place Previous and Next buttons on each page even if you were at the first or last record. If you did this, you could use the following section of code to cycle around the resultset. If you click Next from the last record, you will go to the first; if you click Previous from the first you will get to the last:

If Request.Form("Next") <> "" Then
  objRS.AbsolutePosition = Request.Form("AbsPosition") + 1
  If objRS.EOF Then
    objRS.MoveFirst
  End If
ElseIf Request.Form("Previous") <> "" Then
  If Request.Form("AbsPosition") = 1 Then
    objRS.MoveLast

  Else
    objRS.AbsolutePosition = Request.Form("AbsPosition") - 1  End If  
End If

You can actually modify the descriptions on the buttons themselves using a script if you wish. For example, while you were on Record 3, you could replace Next and Previous with Move to Record 2 and Move to Record 4.

While we bound to Active Directory using the administrator username and password, you could easily adapt the examples so that the web page had fields for both of these. That removes the authentication details from the ASP. In addition, as we have authenticated to Active Directory, you could use this fact to extend this page to manipulate the existing Active Directory information. For example, instead of displaying the description for a user as text, you could make the current description the default value for a text INPUT field in the existing form. Then you could modify this description and click another button that you included, which would write that new description back to Active Directory.

If you wanted to use the ASP to display every attribute for every mandatory and optional property that user objects have, you could walk the property list based on the schema class definition and write the results to a web page rather than a file (see Chapter 19). This is easily achieved using the IADs::Schema property (i.e., objUser.Schema).

You could modify what happens when you get an empty resultset due to an incorrect filter. Instead of just providing a Restart button and ending the session, you could put up the three filter fields and allow people to see and correct their mistakes.

Obviously, you also could expand and extend the search so that it could search for any classes of objects, possibly via a list box within a form.

Even though the HTML code on the ASPs is dynamically generated and sent to the client by server-side scripts, the HTML is static once it has been generated. This means that for a new set of data to be sent to the client, a new page has to be opened. The data on the page returned to the client has to change each time a button is pressed, so the web page is therefore reloaded with every button click. This means that the query is executed again and the resultset is retrieved afresh with every click of the button. The only way to alter the HTML code that exists on the client after it has been generated is to use Dynamic HTML or DHTML. This update to HTML does exactly what it says: it allows HTML to be updated dynamically on the client. While you could use DHTML here, it lies outside the scope of this book. The point is that there are quite a few things that you can do with ADO searches of Active Directory within Active Server Pages.

    [ Team LiB ] Previous Section Next Section