Working with Hibernate


Hibernate has recently gained a lot of momentum in the world of Java database application development. Although products such as Toplink and others have been around for many years, Hibernate is open source (hence, free), stable, mature, well documented, and relatively easy to learn; these are probably just a few reasons why it is as popular as it is. Hibernate has been around for several years but was recently acquired by the JBoss group. (However, it continues to operate autonomously as an open source project.)

The Hibernate persistence framework can make working with relational databases using Java a pleasant experience. This is especially true if you have been developing using JDBC or using heavy-handed type entity beans. Defining the mappings can seem like a slight pain initially, but as you will see in later in this book, there are tools to generate these mapping files.

No Need for DAOs or DTOs

The extra work of defining mappings is well worth it because our persistence code will be cleaner and we will have automatically eliminated the need for Data Access Objects (DAOs), which typically are objects that know how to persist themselves. We also won't need Data Transfer Objects (DTOs), which are objects used to encapsulate business data and get transferred between layers of an application.

Supported Databases

As of the writing of this book, Hibernate supported the following databases (other databases are supported via community efforts):

  • DB2

  • HSQLDB

  • Microsoft SQL Server

  • MySQL

  • Oracle

  • PostgreSQL

  • SAP DB

  • Sybase

  • TimesTen

Note

The databases are supported via Hibernate's SQL Dialect classes such as org.hibernate.dialect. HSQLDialect, org.hibernate.dialect. OracleDialect, org.hibernate.dialect. MySQLDialect, and so on.


Hibernate and EJB 3.x

One thing worth mentioning here is that members of the Hibernate/JBoss team are part of the EJB 3.0 expert group, a group that helped simplify the EJB specifications. It should come as no surprise, then, that the latest version of Hibernate supports the EJB 3.0 specification. However, we will not cover the EJB 3.0 here because it is outside the scope of this book. The focus of this book is on lighter-weight (and open source) frameworks, not heavy-handed specifications that require commercial application servers to use these features.

Simple Test for Hibernate Setup

Before diving into Hibernate concepts and terminology, let's look at a simple hibernate program and the setup involved. The following sections outline the steps required to get our first test program, SimpleTest, working. But first, let's take another look at the development directory structure we established in Chapter 3.

Figure 5.4 shows the development directory structure for Time Expression. It is important to review this again because we will create several files in this chapter and refer to them using their relative path namesfor example, model/Department.java means file Department.java in the timex/src/java/com/visualpatterns/timex/model/ directory.

Figure 5.4. The Time Expression development directory structure.


Hibernate XML Files and Related Java Files

We will place the three types of Hibernate files (discussed next), a Hibernate configuration file, related Java classes, and table mapping files, in the same directory. This is the practice recommended in Hibernate documentation and examples.

The naming convention for the Hibernate mapping files is typically the name of the Java class name with a suffix of .hbm.xmlfor example, Timesheet.hbm.xml.

Hibernate Configuration File (hibernate.cfg.xml)

First we will create a file named hibernate.cfg.xml in the timex/src/java/com/visualpatterns/timex/model/ directory. This file will contain a SessionFactory definition (discussed later in this chapter) and reference to our first mapping file, Department.hbm.xml. Let's review some of the interesting lines from this file:

The following lines show the HSQLDB-related configuration (as we saw in timexhsqldb.xml, previously):

<property name="connection.driver_class">     org.hsqldb.jdbcDriver </property> <property name="connection.url">     jdbc:hsqldb:hsql://localhost:9005/timex </property> <property name="connection.username">sa</property>


The following lines from our hibernate.cfg.xml show the reference to the mapping files we will create in this chapter:

<mapping resource="Department.hbm.xml"/> <mapping resource="Employee.hbm.xml"/> <mapping resource="Timesheet.hbm.xml"/>


Using the complete hibernate.cfg.xml file, we will be able to create a Hibernate SessionFactory (discussed later in this chapter).

Mapping File (Department.hbm.xml)

We will create our first mapping file, Department.hbm.xml, in the timex/src/java/com/visualpatterns/timex/model/ directory.

To keep things simple, I chose to start with the Department table because it is one of the simpler tables, and we will also use it in our slightly more complex example later in this chapter. Let's review the Department.hbm.xml file a bit closer.

The following line maps our Java class to the database table:

<class name="com.visualpatterns.timex.model. Department" table="Department">


The following line establishes departmentCode as the object id (as we discussed earlier) and the database primary key, and also maps the two:

<id name="departmentCode" column="departmentCode">


The generator value shown next tells Hibernate that we will be responsible for setting the value of this object id in our Java class, and Hibernate does not need to do anything special, such as get the next sequence from an auto-increment type column (for example, HSQLDB's identity data type):

<generator />


This line maps the remainder of the Department tablethat is, the name column to a name property in the Department.java class file (discussed next):

<property name="name" column="name"/>


Java Code

We will write two Java classes, one called com.visualpatterns.timex.model. Department and another called com.visualpatterns.timex.test. HibernateTest.

Department.java

The Department.java (under src/java/com/visualpatterns/timex/model) contains a simple JavaBean class, which provides accessors (get methods or getters) and mutators (set methods or setters) for these two variables:

String departmentCode; String name;


HibernateTest.java

Now we will write some simple code to accomplish two things: test the Hibernate setup and also look at a basic example of how to use Hibernate. Let's review our HibernateTest.java file (under src/java/com/visualpatterns/timex/test) step-by-step.

The first few lines show how we obtain a Hibernate SessionFactory class and get a single Department record back for the departmentCode "IT":

SessionFactory sessionFactory = new Configuration().configure()         .buildSessionFactory(); Session session = sessionFactory.getCurrentSession(); Transaction tx = session.beginTransaction(); Department department; department = (Department) session.get(Department.class, "IT"); System.out.println("Name for IT = " + department.getName());


The following lines shows how to get and process a java.util. List of Department objects.

List departmentList = session.createQuery("from Department").list(); for (int i = 0; i < departmentList.size(); i++) {     department = (Department) departmentList.get(i);     System.out.println("Row " + (i + 1) + "> " + department.getName()             + " (" + department.getDepartmentCode() + ")"); }


The remaining notable line closes out theSessionFactory.

sessionFactory.close();


Note the HibernateTest.java file provides a simple example of using Hibernate by bunching all the code in a single (main) method. Later in this chapter, we will look at a better way of building a SessionFactory and subsequently obtaining Session objects from it.

Now we are going to try running our test using our Ant build.xml file, introduced in Chapter 4, "Environment Setup: JDK, Ant, and JUnit." Our Ant target, hibernatetest, is as follows:

<target name="hibernatetest" depends="build">   <java fork="true" classpathref="master-classpath"         classname="com.visualpatterns.timex.test. HibernateTest"/> </target>


To run our test, we need to:

  • Change (cd) to the timex/ (top-level) directory.

  • Type the ant hibernatetest command, as shown in Figure 5.5.

    Figure 5.5. Ant errors due to missing lib/hibernate3.jar file in classpath.

Notice that there are errors on the screen, such as package org.hibernate does not exist. This means it is time to download and set up Hibernate in our environment!

Installing Hibernate

Hibernate can be found at http://hibernate.org. At this point, we will follow the setup instructions provided on this site to download and install it to the recommended (or default) directory.

After we have the Hibernate installed, we will copy all the recommended libraries (for example, hibernate3.jar and antlr.jar) in the Hibernate documentation to the rapidjava/lib directory.

Note that I also needed to copy ehcache-1.1.jar and antlr-2.7.6rc1.jar (which was not mentioned in the Hibernate reference documentation at the time of this writing). Here is what I ended up with, in my timex/lib/ directory:

  • antlr-2.7.6rc1.jar

  • asm-attrs.jar

  • asm.jar

  • cglib-2.1.3.jar

  • commons-collections-2.1.1.jar

  • commons-logging-1.0.4.jar

  • dom4j-1.6.1.jar

  • ehcache-1.1.jar

  • hibernate3.jar

  • jta.jar

  • log4j-1.2.11.jar

Before rerunning our test, we need to temporarily alter the hibernate.cfg.xml file. Because we have only Department.hbm.xml implemented, we need to temporarily remove the following lines (to conduct this test) from our hibernate.cfg.xml file:

<mapping resource="Timesheet.hbm.xml"/> <mapping resource="Employee.hbm.xml"/>


Finally, we can rerun the ant hibernatetest command. If we run the ant as shown earlier in Figure 5.5, this time our command is successful, as shown in Figure 5.6!

Figure 5.6. Output of the HibernateTest class.


At this point, we can reinsert the following two lines into the hibernate.cfg.xml file:

<mapping resource="Timesheet.hbm.xml"/> <mapping resource="Employee.hbm.xml"/>


Notice the log4j warning messages in Figure 5.6. We could ignore these because they are harmless. However, we'll go ahead and create a minimal log4j.properties file (available in this book's code zip file) in our timex/src/conf directory. Logging will be discussed in more detail in Chapter 9, "Logging, Debugging, Monitoring, and Profiling."

Hibernate Basics

Now that we have looked at a small preview of Hibernate-related Java code and XML files, let's get a high-level understanding of some basic Hibernate concepts before we look at slightly more complex Hibernate code for the Time Expression application.

Dialect

Hibernate provides dialect classes for the various supported databases mentioned earlier. This is essentially to ensure that the correct and most optimized SQL is used for the database product being used. For example, we are using the org.hibernate.dialect. HSQLDialect class for HSQLDB.

SessionFactory, Session, and Transaction

SessionFactory, as you might guess, manages a collection of Session objects. Each SessionFactory is mapped to a single database. The Session object essentially is a wrapper for a JDBC connection and is also a factory for Transaction objects. A Transaction is a wrapper for the underlying transaction, typically a JDBC transaction.

Built-In Connection Pooling

A side but important benefit of using Hibernate is that it provides built-in database connection poolinghence, one less thing for us to worry about. Connection pooling, as you might be aware, is used to create a specified pool of open database connections (see connection.pool_size property in our hibernate.cfg.xml). By using a pool of connections, we can achieve more efficiency in our use of the database because existing open connections are reused. Furthermore, we get performance gains because we reuse open connections, thereby avoiding any delays in opening and closing database connections.

Working with Database Records (as Java Objects)

Several methods available in Hibernate's org.hibernate. Session interface enable us to work with database records as objects. The most notable methods are save, load, get, update, merge, saveOrUpdate, delete, and createQuery (several of these are demonstrated later in this chapter).

Another noteworthy interface to mention is org.hibernate. Query, which is returned by calling the Session.createQuery Hibernate method in our HibernateTest.java file. The Query class can be used to obtain a group of records in the form of a java.util. Collection object (for example, Hibernate provides mapping elements such as an array, set, bag, and others).

One last interface worth mentioning here is org.hibernate. Criteria, which can be used for database queries in an OO fashion, as an alternative to the Query class (which is HQL based).

We will look at examples of most of these interfaces and methods in this chapter.

Object States

Hibernate defines three states for object instances: persistent, detached, and transient. Persistent objects are ones that are currently associated with a Hibernate session; as soon as the session is closed (or the object is evicted), the objects become detached. Hibernate ensures that Java objects in a persistent state for an active session match the corresponding record(s) in the database. Transient objects are ones that are not (and most likely, never were) associated with Hibernate session and also do not have an object identity.

Data Types

Hibernate supports a large number of Java, SQL, and Hibernate typesmore than you will probably need for a typical application. Also, you can have Hibernate automatically convert from one type to another by using a different type for a given property in a entity/class mapping file.

The following is a partial list of types supported: integer, long, short, float, double, character, byte, boolean, yes_no, true_false, string, date, time, timestamp, calendar, calendar_date, big_decimal, big_integer, locale, timezone, currency, class, binary, text, serializable, clob, and blob.

Hibernate Query Language (HQL)

HQL is Hibernate's robust SQL-like query language, which is not case sensitive. HQL has many of the features defined in ANSI SQL and beyond, because it is fully object-oriented and supports OO concepts such as inheritance, polymorphism, and more. The following are some basic clauses and features supported in HQL. You will see some examples of these later in the chapter:

  • SELECT, UPDATE, DELETE, INSERT, FROM, WHERE, GROUP BY, ORDER BY

  • Joins (inner, outer)

  • Subqueries

  • Aggregate functions (for example, sum and count)

  • Expressions and functions (mathematical, string, date, internal functions, and more)

Furthermore, Hibernate provides methods that enable you to use native SQL (discussed in Chapter 10, "Beyond the Basics") for the somewhat rare occasions when HQL is insufficient.

You will see basic examples of HQL throughout this chapter.

Unique Object Identifier (<id>)

Hibernate requires mapped classes to identify a table's primary key via the <id> element. For example, the following code excerpt from our Department.hbm.xml file shows departmentCode defined as the primary key (for the Department table mapping):

<id name="departmentCode" column="departmentCode">     <generator /> </id>


Notice the generator class of type "assigned" in this code excerpt; this means the application will provide a value for this id property prior to any database operations on this object.

Hibernate provides several ways to generate unique ids for inserted records, including increment, identity, sequence, hilo, seqhilo, uuid, guid, native, assigned, select, and foreign. The hibernate reference documentation provides ample explanation of each of these. We will use the assigned and identity generators for our examples.

Mandatory Hibernate Transactions

According to the Hibernate documentation related to working with this API, "transactions are never optional, all communication with a database has to occur inside a transaction, no matter if you read or write data."

Hence, you will find the following types of calls in all our Hibernate-related code:

  • session.beginTransaction()

  • session.getTransaction().commit()

  • session.getTransaction().rollback()

HibernateUtil.java

The Hibernate reference documentation recommends the use of a helper class (named HibernateUtil, for example) for setting up a SessionFactory and providing access to it (via a getter method).

A sample HibernateUtil.java class file can be found under the timex\src\java\com\visualpatterns\timex\test directory. This helper class contains only a few lines of actual code. The first notable lines are the following, which build a SessionFactory object:

sessionFactory = new Configuration().configure()                                     .buildSessionFactory();


The only other interesting code in this class is a convenient getter method to return the SessionFactory, as shown here:

public static SessionFactory getSessionFactory() {    return sessionFactory; }


Then we can obtain a Session object, as demonstrated in this code snippet, which fetches a list of Department database objects:

List departmentList=null; Session session = HibernateUtil.getSessionFactory().getCurrentSession(); session.beginTransaction(); departmentList = session.createQuery("from Department ORDER BY name").list(); session.getTransaction().commit();


Further Reading

Again, we have looked only at high-level explanations of the various Hibernate concepts. I will refer you to the Hibernate documentation (found on their website) for detailed information on the concepts discussed here; however, we will be using many of these concepts in our examples, so I will provide additional explanations and code examples along the way.

Now that we have the basics covered, let's begin doing some real work by implementing a full-fledged example to exercise some of Hibernate's many features.

Developing TimesheetManager.java Using Hibernate

If you think about the business requirements of Time Expression, combined with the prototyped screens we looked at in Chapter 2, "The Sample Application: An Online Timesheet System," and the database model shown in Figure 5.2, you can easily see that the Timesheet table is at the heart of our sample application (Time Expression). Hence, I've chosen to use this table as an example in this chapter because our requirements will exercise both read and write type database operations on this table. For example, most screens in Time Expression will either modify data in this table or fetch data from it.

Based on the class/package design we defined in Chapter 3 and the Hibernate configuration files needed, here are the files we will end up with for the example to follow:

  • test/TimesheetManagerTest.java

  • model/TimesheetManager.java

  • model/Timesheet.java

  • model/Timesheet.hbm.xml

The following sections review each of these files in more detail.

TimesheetManagerTest.java

Let's begin by writing a little JUnit test class. Remember, write tests first whenever possible. We already looked at the reasons and benefits of writing tests first in Chapter 4, so I won't repeat the same information here.

The only class we need to write a unit test for is TimesheetManager because Timesheet.java is a JavaBean and hence has no real logic in its methods (just setters and getters).

If we analyze the following two screens from Chapter 2, we can come up with the type of functionality we need our TimesheetManager class to provide:

  • Timesheet List A list of Timesheet records for a given employeeId.

  • Enter Hours The capability to insert and update a single Timesheet record.

Let's look at an example of the functionality we need and how we might implement it. We know we need a list of Timesheet records for the Timesheet List screen (shown in Figure 5.7); this list will be fetched from the database using an employeeId (that is, the employee who is logged in) and also for the current pay period. Hence, I can already picture a method in the TimesheetManager class with a signature that looks something like this: getTimesheet(int employeeId, Date periodEndingDate). So, we can easily write a test case (method) such as testGetByEmployeeIdAndPeriod().

Figure 5.7. Timesheet list prototype screen (from Chapter 2).


This book's code zip file contains TimesheetManagerTest.java, a complete JUnit test suite class to exercise all the methods in our TimesheetManager class.

Note

Note that I didn't write the entire test class in one shot. As I mentioned earlier, unit testing and coding happen in the same sitting. So you would write a little test code (perhaps a few lines in a single method), write a little implementation code, compile, try, and repeat the steps until the method has been fully implemented. The idea is to write small methods, which can be tested relatively easily. This technique also enables us to write the minimal code required to satisfy our user requirements (nothing more, nothing less).


Let's review some of the test code behind this class next; we won't walk through the entire file because we only require fetching (get) of Timesheet objects and saving of individual ones, so let's review methods related to these operations next.

testGetByEmployeeId()


Let's start with the testGetByEmployeeId() method. The first few lines of this code ensure that we get a java.util. List of Timesheet objects back before proceeding:

List timesheetList = timesheetManager.getTimesheets(); assertNotNull(timesheetList); assertTrue(timesheetList.size() > 0);


After we know we have at least one Timesheet object, we can fetch Timesheet records using the employeeId found in the first Timesheet object, as shown here:

int employeeId=((Timesheet)timesheetList.get(0)).getEmployeeId(); timesheetList = timesheetManager.getTimesheets(employeeId); assertNotNull(timesheetList);


Now we can simply test each Timesheet object in the list to ensure that these records belong to the employeeId we requested, as demonstrated next:

Timesheet timesheet; for (int i=0; i < timesheetList.size(); i++) {     timesheet = (Timesheet)timesheetList.get(i);     assertEquals(employeeId, timesheet.getEmployeeId());     System.out.println(">>>> Department name = "                       + timesheet.getDepartment().getName()); } testSaveSingle()


Let's review one more test method from our TimesheetManagerTest.java file, testSaveSingle. The first half of this method sets up a Timesheet object to save; however, the following lines are worth exploring:

timesheetManager.saveTimesheet(timesheet); Timesheet timesheet2 = timesheetManager.getTimesheet(EMPLOYEE_ID,         periodEndingDate); assertEquals(timesheet2.getEmployeeId(), timesheet.getEmployeeId()); assertEquals(timesheet2.getStatusCode(), "P");


We essentially save a Timesheet object, and then fetch it back from the database and compare the two objects' attributes using the assertEquals method.

TimesheetManager.java

Next we will look at our bread-and-butter class (so to speak). We will use this class extensively in Chapter 7 when we implement our user interfaces (for example, Timesheet List and Enter Hours).

The key methods we will review here are getTimesheets, getTimesheet, and saveTimesheet.

Let's start with the TimesheetManager. getTimesheets(int employeeId) method. The key lines code essentially get a java.util. List of Timesheet objects from the database using the Hibernate Session.createQuery method, as shown here:

timesheetList = session.createQuery(         "from Timesheet" + " where employeeId = ?").setInteger(0,         employeeId).list();


The next method, TimesheetManager.getTimesheet(int employeeId, Date periodEndingDate), is slightly different from the getTimesheets(int employeeId) method we just looked at; the key difference is the use of Hibernate's uniqueResult method, which is a convenient method to get only one object back from a query. The following code shows the notable lines from our getTimesheet method:

timesheet = (Timesheet) session.createQuery(         "from Timesheet" + " where employeeId = ?"                 + " and periodEndingDate = ?").setInteger(0,         employeeId).setDate(1, periodEndingDate).uniqueResult();


The last method in TimesheetManager that we will review here is saveTimesheet(Timesheet timesheet). This is a very straightforward method, and the only code worth showing here is the Hibernate's session.saveOrUpdate method, which either does an INSERT or UPDATE underneath the covers, depending on whether the record exists in the database:

session.saveOrUpdate(timesheet)


Timesheet.java (and Timesheet.hbm.xml)

Before we can successfully compile and use TimesheetManager.java, we need to quickly write files it relies on, namely Timesheet.java and its mapping file, Timesheet.hbm.xml (both available in this book's code file). There is not much to these files; the Java code is a simple JavaBean and the XML file simply maps the bean's properties to the appropriate database columns.

Employee.* and DepartmentManager.java

The other files provided in this book's code zip file but not explicitly discussed here include the following as we will need these to implement our first five user stories (page 36).

At this point, we will create these files in our src/java/com/visualpatterns/timex/model directory:

  • DepartmentManager.java

  • Employee.hbm.xml

  • Employee.java

  • EmployeeManager.java

Files Required in Classpath

The various Hibernate files, such as the hibernate.cfg.xml and mapping files (for example, Department.hbm.xml) need to be in the CLASSPATH; accordingly our Ant script, build.xml, automatically copies these files to the timex/build/timex/WEB-INF/classes directory during a build process.

Running the Test Suite Using Ant

Now we can run our test suite (TimesheetManagerTest) discussed previously. However, before we can run the test suite, we need to run HSQLDB in server mode. We can either do it manually as shown here:

java -cp /hsqldb/lib/hsqldb.jar org.hsqldb. Server     -database.0 data\time xdb -dbname.0 timex -port 9005


Note

I've assumed the HSQLDB directory is installed under the root directory (that is, /hsqldb); alter the java command shown here according to your environment.


Or we can run it using our handy Ant script, as follows:

ant -f timexhsqldb.xml starthsql


After we start up the HSQLDB server successfully and have all our files created in the correct directories, we can test our new classes by typing ant rebuild test on the command line (from the timex/ top-level directory). The output of this command is shown Figure 5.8.

Figure 5.8. Running JUnit test suites via Ant.


Deleting Records

We covered database reads and writes. However, we have not covered deleting records, a basic need in any CRUD application. Deleting records is not part of our Time Expression application's requirement. Nevertheless, I've added one for demonstration purposes. The only thing different from what we have already seen is the Session.delete, which deletes a database record (object) and Session.load, which fetches a record from the database, as demonstrated here:

session.delete(session.load(Timesheet.class, new Integer(timesheetId)));


Alternatively, the delete code can be written using the Query.executeUpdate() method, useful for bulk processing, as shown here:

int updated = session.createQuery("DELETE from Timesheet"                                  + " where timesheetId = ?")                      .setInteger(0, timesheetId)                      .executeUpdate();


Criteria Queries

Until now, we have utilized Hibernate's Query interface (via the Session.createQuery method) to fetch records from the database. However, there is a slightly more dynamic, and arguably cleaner, way of fetching records using Hibernate's Criteria interface. This provides a more object-oriented approach, which can result in fewer bugs because it can be type checked and can avoid potential HQL-related syntax errors/exceptions. This method is cleaner because developer does use a more object-oriented approachmore objects rather than simple text queries, hence, more type checking, hence fewer bugs, especially, QueryExceptions. It could be a problem though if query syntax is too complex.

The Criteria interface can be obtained using the Session.createCriteria method as shown in the following code excerpt:

timesheetList = session.createCriteria(Timesheet.class)                        .add(Restrictions.eq("employeeId", employeeId))                        .list();


In addition, Hibernate provides several classes in the org.hibernate.criterion package, which work with the Criteria interface to provide robust querying functionality using objects. Some examples of these classes are Restrictions, Order, Junction, Distinct, and several others.

Exception Handling

Most of the database-related exceptions thrown while using the Hibernate API are wrapped inside org.hibernate.HibernateException; more details on this can be found in Hibernate's reference manual. Meanwhile, the following strategy is recommended in Hibernate's reference manual for handling database exceptions:

"If the Session throws an exception (including any SQLException), you should immediately rollback the database transaction, call Session.close() and discard the Session instance. Certain methods of Session will not leave the session in a consistent state. No exception thrown by Hibernate can be treated as recoverable. Ensure that the Session will be closed by calling close() in a finally block."

We are following these guidelines, of course. However, you might also have noticed in our model code that we rethrow any caught exceptions. It is generally a good idea to pass exceptions up the call stack, so the top-level methods can determine how to process the exception. We will discuss exception handling in more detail in Chapter 10.

I think the problem with this piece of code may be that the developer will never know what exception has actually occurred, because all we do in the catch block is roll back the transaction. Some kind of logging mechanism or exception propagation mechanism to outer callers is necessary to make sure the exception is noticed and handled properly (otherwise, we'll never know about the failure details, except by knowing that timesheet did not get saved).



Agile Java Development with Spring, Hibernate and Eclipse
Agile Java Development with Spring, Hibernate and Eclipse
ISBN: 0672328968
EAN: 2147483647
Year: 2006
Pages: 219

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net