ProblemYou notice that you're writing the same code to perform common operations in multiple programs. SolutionPut routines to perform those operations in a library file, and have your programs access the library. Then write the code only once. You might need to set an environment variable so that your scripts can find the library. DiscussionThis section describes how to put code for common operations in library files. Encapsulation (or modularization) isn't really a "recipe" so much as a programming technique. Its principal benefit is that you don't have to repeat code in each program you write. Instead, you just call a routine that's in the library. For example, by putting the code for connecting to the cookbook database into a library routine, you need not write out all the parameters associated with making that connection. Simply invoke the routine from your program, and you're connected. Connection establishment isn't the only operation you can encapsulate, of course. Later sections in this book develop other utility functions to be placed in library files. All such files, including those shown in this section, can be found under the lib directory of the recipes distribution. As you write your own programs, you'll probably identify several operations that you perform often and that are good candidates for inclusion in a library. The techniques demonstrated in this section will help you write your own library files. Library files have other benefits besides making it easier to write programs. They can help portability. For example, if you write connection parameters directly into each program that connects to the MySQL server, you have to change all those programs if you move them to another machine that uses different parameters. If instead you write your programs to connect to the database by calling a library routine, you localize the changes that need to be made: it's necessary to modify only the affected library routine, not all the programs that use it. Code encapsulation also can improve security in some ways. If you make a private library file readable only to yourself, only scripts run by you can execute routines in the file. Or suppose that you have some scripts located in your web server's document tree. A properly configured server will execute the scripts and send their output to remote clients. But if the server becomes misconfigured somehow, the result can be that your scripts are sent to clients as plain text, thus displaying your MySQL username and password. (And you'll probably realize it too late. Oops.) If the code for establishing a connection to the MySQL server is placed in a library file that's located outside the document tree, those parameters won't be exposed to clients.
The examples of programs that follow demonstrate how to write, for each API, a library file that contains a routine for connecting to the cookbook database on the MySQL server. The calling program can use the error-checking techniques discussed in Section 2.2 to determine whether a connection attempt fails. The connection routine for each language except PHP returns a database handle or connection object when it succeeds or raises an exception if the connection cannot be established. The PHP routine returns an object that represents a connection or an error, because that is what the PEAR DB connection method does (it does not raise an exception). Libraries are of no utility in themselves, so each one's use is illustrated by a short "test harness" program. You can use any of these harness programs as the basis for creating new programs of your own: make a copy of the file and add your own code between the connect and disconnect calls. Library file writing involves not only the question of what to put in the file but also subsidiary issues such as where to install the file so it can be accessed by your programs, and (on multiuser systems such as Unix) how to set its access privileges so its contents aren't exposed to people who shouldn't see it. Choosing a library file installation locationIf you install a library file in a directory that a language processor searches by default, programs written in that language need do nothing special to access the library. However, if you install a library file in a directory that the language processor does not search by default, you'll have to tell your scripts how to find the library. There are two common ways to do this:
We'll use the second approach. For our API languages, the following table shows the relevant variables. In each case, the variable value is a directory or list of directories.
For general information on setting environment variables, see Appendix B. You can use those instructions to set environment variables to the values in the following discussion. Suppose that you want to install library files in a directory that language processors do not search by default. For purposes of illustration, let's use /usr/local/lib/mcb on Unix or C:\lib\mcb on Windows. (To put the files somewhere else, adjust the pathnames in the variable settings accordingly. For example, you might want to use a different directory, or you might want to put libraries for each language in separate directories.) Under Unix, if you put Perl library files in the /usr/local/lib/mcb directory, you can set the PERL5LIB environment variable. For a shell in the Bourne shell family (sh, bash, ksh), set the variable like this in the appropriate startup file: export PERL5LIB=/usr/local/lib/mcb NOTE
If you are using the original Bourne shell, sh, you may need to split this into two commands: PERL5LIB=/usr/local/lib/mcb export PERL5LIB For a shell in the C shell family (csh, tcsh), set PERL5LIB like this in your .login file: setenv PERL5LIB /usr/local/lib/mcb Under Windows, if you put Perl library files in C:\lib\mcb, you can set PERL5LIB as follows: PERL5LIB=C:\lib\mcb In each case, the variable setting tells Perl to look in the specified directory for library files, in addition to whatever other directories it would search by default. If you set PERL5LIB to name multiple directories, the separator character between directory pathnames is colon (:) on Unix or semicolon (;) on Windows. The other environment variables (RUBYLIB, PYTHONPATH, and CLASSPATH) are specified using the same syntax. NOTE
Setting these environment variables as just discussed should suffice for scripts that you run from the command line. But for scripts that are intended to be executed by a web server, you'll likely have to configure the server as well so that it can find the library files. See Section 17.2 for details on how to do this. For PHP, the search path is defined by the value of the include_path variable in the php.ini PHP initialization file. On Unix, the file's pathname is likely to be /usr/lib/php.ini or /usr/local/lib/php.ini. Under Windows, the file is likely to be found in the Windows directory or under the main PHP installation directory. The value of include_path is defined with a line like this: include_path = "value" value is specified using the same syntax as for environment variables that name directories. That is, it's a list of directory names, with the names separated by colons on Unix or semicolons on Windows. For example, on Unix, if you want PHP to look for include files in the current directory and in /usr/local/lib/mcb, set include_path like this: include_path = ".:/usr/local/lib/mcb" On Windows, to search the current directory and C:\lib\mcb, set include_path like this: include_path = ".;C:\lib\mcb" If you modify the php.ini file, and PHP is running as an Apache module, you'll need to restart Apache to make your changes take effect. Setting library file access privilegesQuestions about file ownership and access mode are issues about which you'll need to make decisions if you're using a multiple-user system such as Unix:
Now let's construct a library for each API. Each section here demonstrates how to write the library file itself and discusses how to use the library from within programs. PerlIn Perl, library files are called modules, and typically have an extension of .pm ("Perl module"). Here's a sample module file, Cookbook.pm, that implements a module named Cookbook. (It's conventional for the basename of a Perl module file to be the same as the identifier on the package line in the file.) package Cookbook; # Cookbook.pm - library file with utility method for connecting to MySQL # via Perl DBI module use strict; use warnings; use DBI; my $db_name = "cookbook"; my $host_name = "localhost"; my $user_name = "cbuser"; my $password = "cbpass"; my $port_num = undef; my $socket_file = undef; # Establish a connection to the cookbook database, returning a database # handle. Raise an exception if the connection cannot be established. sub connect { my $dsn = "DBI:mysql:host=$host_name"; my %conn_attrs = (PrintError => 0, RaiseError => 1, AutoCommit => 1); $dsn .= ";database=$db_name" if defined $db_name; $dsn .= ";mysql_socket=$socket_file" if defined $socket_file; $dsn .= ";port=$port_num" if defined $port_num; return (DBI->connect ($dsn, $user_name, $password, \%conn_attrs)); } 1; # return true The module encapsulates the code for establishing a connection to the MySQL server into a connect( ) method, and the package identifier establishes a Cookbook namespace for the module, so you invoke the connect( ) method using the module name: $dbh = Cookbook::connect (); The final line of the module file is a statement that trivially evaluates to true. This is needed because Perl assumes that something is wrong with a module and exits after reading it if the module doesn't return a true value. Perl locates library files by searching through the directories named in its @INC array. This array contains a default list of directories. To check the value of this variable on your system, invoke Perl as follows at the command line: % perl -V The last part of the output from the command shows the directories listed in the @INC array. If you install a library file in one of those directories, your scripts will find it automatically. If you install the module somewhere else, you need to tell your scripts where to find it by setting the PERL5LIB environment variable, as discussed earlier in the introduction to this recipe. After installing the Cookbook.pm module, try it from a test harness script harness.pl written as follows: #!/usr/bin/perl # harness.pl - test harness for Cookbook.pm library use strict; use warnings; use Cookbook; my $dbh; eval { $dbh = Cookbook::connect (); print "Connected\n"; }; die "$@" if $@; $dbh->disconnect (); print "Disconnected\n"; harness.pl has no use DBI statement. It's not necessary because the Cookbook.php library file itself imports the DBI module, so any script that uses Cookbook also gains access to DBI. If you don't want to bother catching connection errors explicitly, you can write the body of the script more simply. In this case, Perl will catch any connection exception and terminate the script after printing the error message generated by the connect( ) method: my $dbh = Cookbook::connect (); print "Connected\n"; $dbh->disconnect (); print "Disconnected\n"; RubyThe following Ruby library file, Cookbook.rb, defines a Cookbook class that implements a connect method: # Cookbook.rb - library file with utility method for connecting to MySQL # via Ruby DBI module require "dbi" # Establish a connection to the cookbook database, returning a database # handle. Raise an exception if the connection cannot be established. class Cookbook @@host = "localhost" @@db_name = "cookbook" @@user_name = "cbuser" @@password = "cbpass" # class method for connecting to server to access # cookbook database; returns database handle object. def Cookbook.connect return DBI.connect("DBI:Mysql:host=#{@@host};database=#{@@db_name}", @@user_name, @@password) end end The connect method is defined in the library as Cookbook.connect because Ruby class methods are defined as class_name.method_name. Ruby locates library files by searching through the directories named in its $LOAD_PATH variable (also known as $:), which is an array that contains a default list of directories. To check the value of this variable on your system, use Ruby to execute this statement: puts $LOAD_PATH If you install a library file in one of those directories, your scripts will find it automatically. If you install the file somewhere else, you need to tell your scripts where to find it by setting the RUBYLIB environment variable, as discussed earlier in the introduction to this recipe. After installing the Cookbook.rb library file, try it from a test harness script harness.rb written as follows: #!/usr/bin/ruby -w # harness.rb - test harness for Cookbook.rb library require "Cookbook" begin dbh = Cookbook.connect print "Connected\n" rescue DBI::DatabaseError => e puts "Cannot connect to server" puts "Error code: #{e.err}" puts "Error message: #{e.errstr}" exit(1) end dbh.disconnect print "Disconnected\n" harness.rb has no require statement for the DBI module. It's not necessary, because the Cookbook module itself imports DBI, so any script that imports Cookbook also gains access to DBI. If you just want a script to die if an error occurs without checking for an exception yourself, write the body of the script like this: dbh = Cookbook.connect print "Connected\n" dbh.disconnect print "Disconnected\n" PHPThe contents of PHP library files are written like regular PHP scripts. You can write such a file, Cookbook.php, that implements a Cookbook class with a connect( ) method as follows: <?php # Cookbook.php - library file with utility method for connecting to MySQL # via PEAR DB module require_once "DB.php"; class Cookbook { # Establish a connection to the cookbook database, returning a # connection object, or an error object if an error occurs. function connect () { $dsn = array ( "phptype" => "mysqli", "username" => "cbuser", "password" => "cbpass", "hostspec" => "localhost", "database" => "cookbook" ); return (DB::connect ($dsn)); } } # end Cookbook ?> Although most PHP examples throughout this book don't show the <?php and ?> tags, I've shown them as part of Cookbook.php here to emphasize that library files must enclose all PHP code within those tags. The PHP interpreter doesn't make any assumptions about the contents of a library file when it begins parsing it because you might include a file that contains nothing but HTML. Therefore, you must use <?php and ?> to specify explicitly which parts of the library file should be considered as PHP code rather than as HTML, just as you do in the main script. PHP looks for libraries by searching the directories named in the value of the include_path variable in the PHP initialization file, as described earlier in the introduction to this recipe. Assuming that Cookbook.php is installed in one of those directories, you can access it from a test harness script harness.php written as follows: <?php # harness.php - test harness for Cookbook.php library require_once "Cookbook.php"; $conn =& Cookbook::connect (); if (PEAR::isError ($conn)) die ("Cannot connect to server: " . $conn->getMessage () . "\n"); print ("Connected\n"); $conn->disconnect (); print ("Disconnected\n"); ?> harness.php has no statement to include DB.php. It's not necessary because the Cookbook module itself includes DB.php, which gives any script that includes Cookbook.php access to DB.php.
PythonPython libraries are written as modules and referenced from scripts using import or from statements. To create a method for connecting to MySQL, we can write a module file Cookbook.py: # Cookbook.py - library file with utility method for connecting to MySQL # via MySQLdb module import MySQLdb host_name = "localhost" db_name = "cookbook" user_name = "cbuser" password = "cbpass" # Establish a connection to the cookbook database, returning a connection # object. Raise an exception if the connection cannot be established. def connect (): return MySQLdb.connect (db = db_name, host = host_name, user = user_name, passwd = password) The filename basename determines the module name, so the module is called Cookbook. Module methods are accessed through the module name; thus you would invoke the connect( ) method of the Cookbook module like this: conn = Cookbook.connect (); The Python interpreter searches for modules in directories named in the sys.path variable. You can find out what the default value of sys.path is on your system by running Python interactively and entering a couple of commands: % python >>> import sys >>> sys.path If you install Cookbook.py in one of the directories named by sys.path, your scripts will find it with no special handling. If you install Cookbook.py somewhere else, you'll need to set the PYTHONPATH environment variable, as discussed earlier in the introduction to this recipe. After installing the Cookbook.py library file, try it from a test harness script harness.py written as follows: #!/usr/bin/python # harness.py - test harness for Cookbook.py library import sys import MySQLdb import Cookbook try: conn = Cookbook.connect () print "Connected" except MySQLdb.Error, e: print "Cannot connect to server" print "Error code:", e.args[0] print "Error message:", e.args[1] sys.exit (1) conn.close () print "Disconnected" NOTE
The Cookbook.py file imports the MySQLdb module, but a script that imports Cookbook does not thereby gain access to MySQLdb. If the script needs MySQLdb-specific information (such as MySQLdb.Error), the script must also import MySQLdb. If you just want a script to die if an error occurs without checking for an exception yourself, write the body of the script like this: conn = Cookbook.connect () print "Connected" conn.close () print "Disconnected" JavaJava library files are similar to Java programs in most ways:
Java library files also differ from Java programs in some ways:
A common convention for Java package identifiers is to begin them with the reverse domain of the code author; this helps make identifiers unique and avoids conflict with classes written by other authors. Domain names proceed right to left from more general to more specific within the domain namespace, whereas the Java class namespace proceeds left to right from general to specific. Thus, to use a domain as the prefix for a package name within the Java class namespace, it's necessary to reverse it. In my case, the domain is kitebird.com, so if I write a library file and place it under mcb within my domain's namespace, the library begins with a package statement like this: package com.kitebird.mcb; Java packages developed for this book are placed within the com.kitebird.mcb namespace to ensure their uniqueness in the package namespace. The following library file, Cookbook.java, defines a Cookbook class that implements a connect( ) method for connecting to the cookbook database. connect( ) returns a Connection object if it succeeds, and throws an exception otherwise. To help the caller deal with failures, the Cookbook class also defines getErrorMessage( ) and printErrorMessage( ) utility methods that return the error message as a string or print it to System.err. // Cookbook.java - library file with utility method for connecting to MySQL // via MySQL Connector/J package com.kitebird.mcb; import java.sql.*; public class Cookbook { // Establish a connection to the cookbook database, returning // a connection object. Throw an exception if the connection // cannot be established. public static Connection connect () throws Exception { String url = "jdbc:mysql://localhost/cookbook"; String user = "cbuser"; String password = "cbpass"; Class.forName ("com.mysql.jdbc.Driver").newInstance (); return (DriverManager.getConnection (url, user, password)); } // Return an error message as a string public static String getErrorMessage (Exception e) { StringBuffer s = new StringBuffer (); if (e instanceof SQLException) // JDBC-specific exception? { // print general message, plus any database-specific message s.append ("Error message: " + e.getMessage () + "\n"); s.append ("Error code: " + ((SQLException) e).getErrorCode () + "\n"); } else { s.append (e + "\n"); } return (s.toString ()); } // Get the error message and print it to System.err public static void printErrorMessage (Exception e) { System.err.println (Cookbook.getErrorMessage (e)); } } The routines within the class are declared using the static keyword, which makes them class methods rather than instance methods. That is done here because the class is used directly rather than creating an object from it and invoking the methods through the object. To use the Cookbook.java file, compile it to produce Cookbook.class, and then install the class file in a directory that corresponds to the package identifier. This means that Cookbook.class should be installed in a directory named com/kitebird/mcb (or com\kitebird\mcb under Windows) that is located under some directory named in your CLASSPATH setting. For example, if CLASSPATH includes /usr/local/lib/mcb under Unix, you can install Cookbook.class in the /usr/local/lib/mcb/com/kitebird/mcb directory. (See the Java discussion in Section 2.1 for more information about the CLASSPATH variable.) To use the Cookbook class from within a Java program, import it, and then invoke the Cookbook.connect( ) method. The following test harness program, Harness.java, shows how to do this: // Harness.java - test harness for Cookbook library class import java.sql.*; import com.kitebird.mcb.Cookbook; public class Harness { public static void main (String[] args) { Connection conn = null; try { conn = Cookbook.connect (); System.out.println ("Connected"); } catch (Exception e) { Cookbook.printErrorMessage (e); System.exit (1); } finally { if (conn != null) { try { conn.close (); System.out.println ("Disconnected"); } catch (Exception e) { String err = Cookbook.getErrorMessage (e); System.out.println (err); } } } } } Harness.java also shows how to use the error message utility methods from the Cookbook class when a MySQL-related exception occurs:
|