Hacking Java Professional Resource Kit
Hacking Java Professional Resource Kit
C O N T E N T S
Introduction Chapter 1
G G G G
What Is Java?
Java as a Web Programming Language Java as an Applications Programming Language New Features on the Horizon Java as an Embedded Systems Language
Chapter 2
G
G G
No Java? No Problem H Displaying an Image in Place of an Applet Passing Parameters to Applets Improving Applet Startup Time
Chapter 3
G
Applet Security
CONTENTS
G G G G
File Access Restrictions Network Restrictions Other Security Restrictions Getting Around Security Restrictions H Using Digital Signatures for Increased Access H Creating a Customized Security Manager
Chapter 4
G G
Displaying Images
G G
Images in Java Displaying Simple Images H Shrinking and Stretching Images Creating Your Own Images Displaying Other Image Formats H The Microsoft Windows Bitmap (BMP) File Format Manipulating Images H Performing Image-Processing Algorithms Filtering Image Colors H Filtering Based on Pixel Position Downloading Images
Chapter 5
G G G G G G G
Animating Images
Animation An Animation Driver Animating Image Sequences Animating Portions of an Image Animating with a Filter Cycling the Color Palette Animating Graphics H Redrawing the Entire Screen H Doing Animation with XOR Eliminating Flicker H Double-Buffering
Chapter 6
CONTENTS
G
G G G G G G
lass
Getting Files Using Sockets Performing a Query with GET Posting Data with the URL Class Posting Data Using Sockets Supporting the Cookie Protocol
Chapter 7
G G G G G G G
Smarter Forms Creating Forms with the AWT Checking for Errors on the Client Side Adding Context-Sensitive Help Creating Dynamic Forms Loading Another URL from an Applet Creating Image Maps with Hot Spots
Chapter 8 Applet
G G
G G
Applets and Files Using the JFS Filesystem for Applets H Printing Files Using JFS H Accessing Other Web Servers from JFS Saving Files Using HTTP Post Storing and Retrieving Files with FTP H Sending FTP Commands H Establishing an FTP Session H Sending Simple FTP Commands H Establishing a Data Connection
CONTENTS
G G
Reusable Components The Command Pattern H Invoking Commands from a Menu Creating a Reusable Image Button H Setting the Size of a Canvas H Handling Input Events H Painting the Canvas H Watching for Image Updates Creating a CommandImageButton Using the Observer Interface H The Model-View-Controller Paradigm H Observables and the Model-View-Controller Paradigm Using Observables for Other Classes
H
Chapter 10
G G G G
Inter-Applet Communication
Locating Other Applets Exchanging Data Using Piped Streams Creating Multi-Client Pipes Sharing Information with Singleton Objects
Chapter 11
G G G
Sending E-Mail Sending E-Mail Using the SMTP Protocol Accessing Your Mailbox with the POP3 Protocol
Chapter 12
G G G G G
Protecting Your Code from Unauthorized Use Embedding Copyrights in Your Code Verifying the Origin of the Applet Hiding Information in Your Applet Obfuscating a Working Program H Make All Your Function and Variable Names Meaningless H Perform Occasional Useless Computations or Loops
CONTENTS
H H H H
Hide Small Numbers in Strings Create Large Methods Spread Methods Out Among Subclasses Using a Commercial Obfuscator
Chapter 13
G G G G
Differences Between Applets and Applications Allowing an Applet to Run as an Application The Applet's Runtime Environment Creating an Applet Context
Chapter 14 Files
G G G G G G
Class Archive Files Creating Your Own Archive File with Info-ZIP Viewing the Contents of a Zip Archive Adding Classes Directly to the Browser's Library Creating Class Archives with Other Zip Archivers Creating Cabinet Files for Internet Explorer
Chapter 15
G
Organizing Your Data for a Relational Database H Using SQL H Combining Data from Multiple Tables Using Joins Designing Client/Server Database Applications H Client/Server System Tiers H Handling Transactions H Dealing with Cursors H Replication H How Does JDBC Work? H JDBC Security Model H Accessing ODBC Databases with the JDBC-ODBC Bridge H JDBC Classes-Overview
CONTENTS
G G
G G G G
Anatomy of a JDBC Application H JDBC API Examples The Connection Class Handling SQL Statements H Creating and Using Direct SQL Statements H Creating and Using Compiles SQL Statements (PreparedStatement) H Calling Stored Procedures (CallableStatement) Retrieving Results in JDBC Handling Exceptions in JDBC-SQLException Class Handling Exceptions in JDBC-SQLWarnings Class Handling Date and Time H java.sql.Date H java.sql.Time H java.sql.Timestamp Handling SQL Types H java.sql.Types JDBC in Perspective
H
G G G
Creating 3-Tier Applications RMI Features Creating an RMI Server H Defining a Remote Interface H Creating the Server Implementation H Creating the Stub Class Creating an RMI Client Creating Peer-to-Peer RMI Applications Garbage Collection, Remote Objects, and Peer-to-Peer
Chapter 17
G G G
Defining IDL Interfaces Compiling IDL Interfaces for Java Clients Writing a Client Applet
CONTENTS
G G G G G
Handling Exceptions CGI Programs, Java.net.*, and Java.io.* May Not Be the Best Choices Using the Dynamic Invocation Interface and the Interface Repository Using Filters Some Points About Distributed System Architecture
Chapter 18
G G
G G
What Is CORBA? Sun's IDL to Java Mapping H IDL Modules H IDL Constants H IDL Data Types H Enumerated Types Structures H Unions H Sequences and Arrays H Exceptions H Interfaces H Attributes Using CORBA in Applets H Choosing Between CORBA and RMI Creating CORBA Clients with JavaIDL Creating CORBA Clients with VisiBroker
Chapter 19
G
G G
Creating a Basic CORBA Server H Using Classes Defined by IDL Structs H VisiBroker Skeletons H Using the VisiBroker TIE Interface H JavaIDL Skeletons Creating Callbacks in CORBA Wrapping CORBA Around an Existing Object H Mapping to and from CORBA-Defined Types H Creating Remote Method Wrappers H Implementing Wrapped Callbacks
CONTENTS
Chapter 20
G
Double-Buffering to Speed Up Drawing H Detecting the Best Drawing Method at Runtime Creating an Autodetecting update Performing Selective Updates Redrawing Changed Areas
H
Method
G G
Chapter 21
G G
Download Strategies
Huffman Coding and Lempel-Ziv Compression Delayed Downloading H Delayed Instantiation H Downloading in the Background Providing Local Libraries H Installing Local Libraries for Hotjava and Appletviewer H Installing Local Libraries for Netscape H Installing Local Libraries for Internet Explorer Downloading Classes in Zipped Format H Zip Downloading in Netscape Navigator Version 3 H A Zipfile Class Loader Packaging Classes in Jars and Cabinets
Chapter 22
G G
Reducing Image Size Image Strips H Using the Graphics.clipRect Method H Creating Another Graphics Context Storing Only Parts on an Image Strip
Chapter 23
G G
CONTENTS
G G G
The Web Server as a Computing Server Adding Web Access to Your Java Applications Migrating off the Web Server in the Future
Chapter 24
G G
What Is Jeeves? The Jeeves HTTP Server H Architectural Overview H Installing and Running the Jeeves HTTP Server H Administering the Jeeves Web Server H HTTP Server Security Extending Jeeves' Functionality with Servlets H Employing the Servlet API H Using the Jeeves Development Toolkit Building a Database Servlet H Getting the Information from the Users H Connecting Your Servlet to a JDBC Database H Inserting Data in the Database H Searching the Database Building a Simple Autonomous Agent System with Jeeves H Using Object Serialization to Transport Agents Across the Internet H Building the Remote Agency H Creating a Generic Agent Interface H Implementing a Database Search Agent H Building the Home Agency H Launching the Agent H Debriefing the Agent
Chapter 25
G
Architectural Overview H Handling the HTTP Protocol with the Daemon Module H Managing the Server Information Space with the Resource Module H Maintaining Server State via Object Persistence H Pre and Post Request Processing with Resource Filters Jigsaw Interface
CONTENTS
G G G G G
The HTTPResource Class H The FilteredResource Class H The DirectoryResource Class H The FileResource Class Installation and Setup of the Jigsaw HTTP Server Adding Content to the Jigsaw Server Extending the Server with Java Writing Resource Filters in Java Handling Forms and the POST Method in Java
H
Chapter 26 Signatures
G G G G
G G
What Are Digital Signatures? Allowing More Access for Signed Applets Using a Third Party for Applet Signatures Potential Security Problems with Digital Signatures H Using Phony Signatures H Receiving Old Software H Mistaken Trust in Signed Applets H Running a Phony Web Browser Obtaining a Digital Signature Certificate Other Uses for Digital Signatures
Chapter 27
G G
Encrypting Data
Choosing the Right Kind of Encryption Guarding Against Malicious Attacks H Resisting a Playback Attack H Don't Store Keys in Your Applets H Using Public Key Encryption to Exchange Session Keys H Using Secure HTTP to Thwart Impersonations Getting Encryption Software H Getting SSLava, the Secure Sockets Library H Getting the Cryptix Library H Getting the Acme Crypto Package
CONTENTS
Chapter 28
G G G G
G G G
Getting a Secure Web Server Preventing Impersonations Accessing Remote Data Passing Keys to Clients H Don't Reuse Symmetric Keys H Using Public Key Encryption to Get a Private Key H Passing a Private Key as an Applet Parameter Implementing a Single-Client Secure Server Implementing a Multiclient Secure Server Creating Other Secure Remote Access Programs
Chapter 29
G G
Designing a Basic Shopping Cart Creating a Shopping Cart User Interface H Creating a Catalog Applet H Creating the Shopping Cart Applet
Chapter 30
G G G
G G
Letting Customers Digitally Sign Orders Using Encryption in All Network Communications Creating Java Services for Netscape Servers H Creating a Server-Side "Hello World" H Installing a New Server-Side Java Applet H Handling Forms from Server-Side Applets H Sending Files as a Response H Returning Multi-Part Responses H Maintaining Information Between Applet Invocations Making Server-Side Applets Work on Different Web Servers Performing Secure Transactions
Chapter 31
CONTENTS
Framework (JECF)
G
G G
G G
G G
The Difficulties of Electronic Commerce H Theft of Information H Fraudulent Programs H Proprietary Solutions H Static Solutions H Platform-Dependence Creating Online Services with the JECF Storing Information in the Wallet Database H Keeping Data Safe H Performing Transactions Implementing a Shopping Cart Applet with the JECF Offering Services with Cassettes H Creating Other Wallet Services H Ensuring Cassette Security H Dealing with System Failures JECF Availability Getting More Information About the JECF
Chapter 32
G G G G G
G G G G
Focusing on Function, not Form Providing Access to New Systems Using CORBA to Open Up a Closed System Encapsulating a TCP/IP System Encapsulating with Native Method Calls H Wrapping Java Around a Native Interface H Writing Native Methods in C Encapsulating by Emulating a User Getting Assistance from the Legacy System Presenting a Different Interface Combining Multiple Systems H Handling Deletions Originating in the Legacy System H Using a Two-Phase Commit Protocol H Implementing a Two-Phase Commit Some Real-World Examples
CONTENTS
H H H H
An Example Legacy System Creating a New Application for the Existing Terminal Base Creating a New Interface for an Existing Application Clearing a Path for Migration off the Legacy System
Chapter 33
G
Using Encapsulations to Access Legacy Data H Aiming for Session-Less Transactions H Storing Session Information in the Web Page H Using HTTP Cookies to Preserve Session Information H Choosing a Good Session Identifier H Clearing Out Old Sessions Accessing Legacy Data from Servlets
Chapter 34
G G G G G G
A Thumbnail Sketch of CICS The CICS External Call Interface The Java-CICS Gateway API Creating Multiple-Call LUWs Creating Web Interfaces to CICS Providing a CORBA Interface to CICS H Creating a CORBA-CICS Gateway H Creating CORBA Interfaces to CICS Programs
Chapter 35 HotJava
G
Writing a Protocol Handler H Step One: Decide Upon a Package Name H Step Two: Create the Directories H Step Three: Set Your CLASSPATH H Step Four: Implement the Protocol H Step Five: Create the Handler Class H Step Six: Compile the Sources
CONTENTS
G
Using Protocol Handlers with HotJava H Step One: Update the properties File H Step Two: Run HotJava Using Protocol Handlers with Your Own Applications H The main() Method: Starting FetchWhois H The FetchWhois Constructor: Where the Work Gets Done H The whoisUSHFactory Class: Registering the Protocol Handler H Running FetchWhois More on URLStreamHandlerFactory
Chapter 36
G
Writing Content Handlers H Step One: Decide upon a Package Name H Step Two: Create the Directories H Step Three: Set Your CLASSPATH H Step Four: Write the Content Handler H Step Five: Compile the Source Using Content Handlers with HotJava H Step One: Disable Special MIME Handling H Step Two: Update the PROPERTIES File H Step Three: Run HotJava Using Content Handlers with Your Own Applications H Start FetchFuddify H The ContentHandlerFactory Implementation H Running the Application
Chapter 37
G G
G G
Designing Multi-User Applications Adding Socket-Based Access to Multi-User Applications H Creating a Socket-Based Server H Sending Messages over Sockets Other Issues When Dealing with Sockets Adding RMI Access to Multi-User Applications
Chapter 38
CONTENTS
Services
G G
G G
G G
Java's Suitability for On-Demand Applications Using the On-Demand Audio Applet H Logging In H Playing Audio Clips Adding Sound to Applets On-Demand Music Applet Code Review H Applet Architecture H Initialization and Registration H Song Selection H Playing the Songs Java Shortcomings New Features
G G G
G G
Java's Suitability for Multimedia Applications H Java Is Portable H Java Is Compact H Java Can Handle Streaming Data H Java Is Based on the Client/Server Model H Java Supports PDAs Easily Using the Multimedia Encyclopedia Adding Images and Sound to Applets The On-Line Multimedia Encyclopedia In-Depth H Applet Architecture H Index Window H Topic Window Shortcomings New Features
CONTENTS
G G G
Characteristics of Non-Traditional Devices The New Computing Model Designing Applications to Support Non-Traditional Devices H Separating the User Interface from the Application H Avoiding Large, Monolithic Applications H Sticking to Standard Libraries H Avoiding Long, Complex Transactions Designing User Interfaces for Small Devices H Creating Obvious, Self-Documenting Interfaces H Avoiding Extraneous Pictures or Information H Keeping Everything Readable H Supporting Multiple Sources of Input Creating Reusable Components for Small Devices H Using the CardLayout Layout Manager as a Stack H Creating a Keyboard/Keypad Input Filter H Creating a Pop-Up Keypad for Pen and Touch-Screen Users
Credits
HTML conversion by : M/s. LeafWriters (India) Pvt. Ltd. Website : http://leaf.stpn.soft.net e-mail : leafwriters@leaf.stpn.soft.net
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/index.htm (16 of 22) [8/14/02 10:52:31 PM]
CONTENTS
President Publishing Manager Editorial Services Director Director of Marketing Acquisitions Editor Production Editor
Roland Elgey Jim Minatel Elizabeth Keaffaber Lynn E. Zingraf Stephanie Gould Sean Dixon
Acquisitions Manager Cheryl D. Willoughby Product Directors Editors Mark Cierzniak, Jon Steever Kelly Brooks, Judith Goode, Sidney Jones, Kelly Oliver Christy M. Miller Jim Hoffman, Russ Jacobs, Ernie Sanders, Eugene W. Sotirescu, Steve Tallon Jane K. Brownlow Andrea Duvall Barbara Kordesh
Technical Support Specialist Software Relations Coordinator Interior Book Designer Production Team
Kevin Cliburn, Tammy Graham, Jason Hand, Heather Howell, Dan Julian, Bob LaRoche, Casey Price, Erich Richter, Laura Robbins, Marvin Van Tiem, Paul Wilson Chris Barrick
Indexer
Acknowledgments
Writing a book like this is quite an experience, and one of the most important parts of that experience has been the people I have worked with and the people who helped me get through it. I would especially like to thank my wife Ceal, who somehow managed to keep me close to my normal level of sanity (which is minimal at best). Thanks also go to Chris, Amy, Samantha, and Kaitlynn, who had to endure endless hours of clicking keys and to my Mom, who taught me, by example, how to work hard and to strive constantly to improve myself.
CONTENTS
Joe Weber, author of Special Edition Using Java, provided some excellent suggestions about the outline for this book, as well as some good advice about being an author. In addition, Cliff McCartney provided me with technical feedback on various aspects of the book, especially in the area of legacy system migration-a subject near and dear to both of our hearts. This book was not written by a single person. I am extremely grateful for the work of the other authors. The technical expertise that each of them brought to this book has truly made it a book of expert solutions. I would also like to thank the staff at Que, who have been great to work with-Stephanie Gould, Mark Cierzniak, Ben Milstead, Jon Steever, Sean Dixon, and the many people behind the scenes. Finally, I would like to thank Geddy, Alex, and Neil for EXCELLENT music to code by. You guys have gotten me through hundreds of thousands of lines of code.
CONTENTS
Stephen N. Matsuba is cofounder of Alt.Reality Technologies Corporation, a company developing virtual reality and multimedia applications, and SHOC Interactive, a company developing multimedia games and educational applications. In his other life, he is completing his Ph.D. in computational linguistics and English literature at York University, Canada. His research interests include Shakespeare, literary theory, computational linguistics, artificial intelligence and cognitive science, computer applications in humanities research and education, VR, and multimedia design. He also coauthored Special Edition Using VRML (Que, 1996) with Bernie Roehl. George Menyhert is currently the Director of the Harmony Product and a member of the technical staff at Cinebase Software where he concentrates on multimedia application engineering. He is also a freelance Java developer. George has a degree in engineering from the University of Cincinnati. He can be reached via his Web page at http://w3.one.net/~menyhert or through one of his various e-mail accounts: george.menyhert@cinesoft.com, menyhert@one.net, or menyhert@acm.org. Krishna Sankar has been a computer professional since 1980. He has worked on strategic business systems for companies like HP, AT&T, Pratt & Whitney, Testek, Ford, TRW, Caterpillar, Qantas Airlines, and Air Canada, as well as for the U.S. Air Force and U.S. Navy. He still believes in information re-engineering and development of competitive business systems and is excited about the possibilities of intranet applets and servlets in those areas. He has two master's degrees, one in production engineering and the other in computer science. He is now pursuing his MBA. He is a Microsoft Product Specialist as well as a Lotus Certified Professional. He is the founder of U.S. Systems & Services, a Silicon Valley intranet systems and Java technology company. Nowadays, you can meet him in the corridors of venture capitalists and banks promoting products "for those whose life is not Internet but want to leverage the net to enjoy it." Mark Wutka is a senior systems architect who refuses to give up his programming hat. For the past two years he has worked as the chief architect on a large, object-oriented distributed system providing automation for the flight operations division of a major airline. Over the past eight years, he has designed and implemented numerous systems in C, C++, Smalltalk, and Java for that same airline. He is currently the Vice President of Research and Development for Pioneer Technologies, a consulting firm specializing in distributed systems and legacy system migration. He can be reached via e-mail at wutka@netcom.com. He also claims responsibility for the random bits of humor found at http://www.webcom.com/wutka.
CONTENTS
Thanks in advance-your comments will help us to continue publishing the best books available on new computer technologies in today's market. Mark Cierzniak Product Development Specialist Que Corporation 201 W. 103rd Street Indianapolis, Indiana 46290 USA
Introduction
by Mark Wutka Java is one of the most significant software products to hit the scene in a long time. Unlike Netscape, whose impact was big and immediate, Java's full impact won't be realized for a long time. Java is more than just a programming language. It requires a different mindset when developing applications. Sure, you can use Java to spruce up your Web pages-it works quite well for that. This book will even give you some tips on ways to do it. But that's not the main purpose of Java. If you only use it for pretty Web pages, you are missing a lot. Hacking Java: The Java Professional's Resource Kit not only gives you lots of useful Java classes and programming tips, it relates the "vision" of Java. You get an overview from the 30,000-foot level, as well as from down in the trenches, to borrow some management clichs. Both of these views are important. When you're digging a trench, you still need to look up to see where you're headed. Java will have a significant impact on the future of software development, and even the future of technology. If you don't already understand why this is so, you need this book. One of the important things to realize about Java is that it is young and still evolving. There are many features yet to come, and many more uses of Java to be discovered. This book will help guide you in making design decisions that may be affected by some of these new applications of Java.
CONTENTS
thought of. This is especially true when it comes to the overall philosophy of Java and its multitude of uses. This book is not an introduction to programming in Java. There is no discussion of what classes and methods are. Special Edition Using Java by Que will give you a good introduction to Java. This book is meant to complement Special Edition Using Java, giving you the kind of advice that you don't get from a book on programming.
CONTENTS
Section XI discusses some of the issues involved with running Java on small devices like cellular phones and personal digital assistants (PDA). As these devices become more readily available, your systems will have to cooperate with them. This section gives you guidelines that let you start planning for these devices now. On the CD, three chapters will show you how Java integrates with the Virtual Reality Markup Language (VRML). You will see how you can add whole new dimensions to your Java programs, literally.
CONTENTS
G G G G
Java as a Web Programming Language Java as an Applications Programming Language New Features on the Horizon Java as an Embedded Systems Language
Because this book assumes that you already know how to program in Java, you already have a good idea of what features are in the Java language. This book will notteach you how to program in Java; it takes the next step byshowing you what you can do with Java and how to do it.
is closed after a server has sent a response back to the client, there is no notion of a session within HTTP. Clients and servers have had to come up with their own interesting ways of maintaining session information between requests. The Netscape Cookie protocol is one such method. The server puts Netscape cookies in a Web page when it sends information back to the browser. The pieces of information are tagged as being cookies, which the browser watches for and saves for later use. The next time the browser accesses that server, it sends the cookies back to the server. This allows the server to save information at the client-side and then receive the information at a later time. Cookies are discussed more fully in Chapter 6 "Communicating with a Web Server." When you are writing serious applications, however, you need the interaction between client and server to be much more flexible. A client should be able to send information to a server at any time, and the server should be able to send data back to the client at any time. Java's networking support allows you to do this by creating a socket connection between the client and the server. Look at an example of a real-world application and see how Java can improve yourapplications drastically. Suppose you work for an airline and you are creating a program to display the current position of any of the company's aircraft. You would like this program to run on any Web browser within the company. Your server will be gathering aircraft position data and sending the information out to the browsers. You obviously want this to be a graphical program-you don't just want to list coordinates. You want the president of the company to be able to see immediately that flight 1313 is halfway between Cleveland and Detroit, without having to estimate its distance based on the latitude and longitude shown on some chart. If you were to do this application using the traditional Web server and HTML forms, your server would have to generate entire images and send them to the client. Anytime a plane's position changed, you'd have to generate new images for each client that was watching that plane. Even if a plane's position changes once a minute, if you watch ten planes, you'll be receiving an average of one image every six seconds. That's an incredible burden to place on your server. Now, suppose you were to create the same application in Java. The Java applet would download a blank map from the server and then open up a socket connection to the server. Anytime the Java applet wanted to watch a new plane, or stop watching a plane, it would send a message to the server. The server would track what clients were watching what planes. One of the keys here is that the connection between the client and the server stays up. This allows the server to keep track of clients based on their sockets. Now, suppose the server receives a position update for a plane. It looks through its tables and finds every client that was watching that plane and sends the new position down to that client. It does not have to perform any image generation. The amount of data sent to the client is probably 100-1,000 times smaller than the image that would be sent under the previous architecture.
The Java applet is responsible for creating the new image of the aircraft. Although this may take a little longer to generate on the client than on the server, the server is able to handle many times more clients than it otherwise would, because it doesn't have to do as much work for each client. If you step back and take a look at this application, you'll see that the applet is really just implementing the user interface for the flight tracking system. The bulk of the work in gathering the flight data and analyzing it is done by the server. The interaction between the server and client is a clearly defined set of actions. The client starts watching a plane, the client stops watching a plane, the server sends a flight position to the client. That'sa pretty simply protocol! The client does what it does best-it interacts with the user. The server does what it does best-it gathers and analyzes information. Keep this in mind as you design and develop new applications. Don't heap all the work on the applet, just let it do what it does best-interact with the user. Realizing that applets are going to need a reasonable way to communicate with the actual applications, Sun added two important subsystems to Java. Remote Method Invocation (RMI) allows a Java object to invoke methods in another Java applet somewhere else on the network. You don't have to come up with your own way of transmitting data between the applet and the application on the server. The applet can simply invoke methods on the server using RMI. RMI is a nice feature, and is very easy to use since it blends into your applet and application almost seamlessly. There is another way to invoke methods remotely, however.It's called the Common Object Request Broker Architecture, or CORBA. There are many differences between RMI and CORBA. One of the biggest is that CORBA is a multi-language protocol. You can use CORBA in an applet to invoke methods in a C++ application running on your server. You will be able to choose between RMI and CORBA for your applets. They will both be supported as part of the core of Java. You can expect both mechanisms to be present in a Java-compliant Web browser, or any Java-compliant environment.
are. The Jigsaw WWW server, discussed in Chapter 25, "Writing Web Services for Jigsaw," is written entirely in Java-over 30,000 lines! It runs very well across all Java-enabled platforms. The big difference between a Java application and a Java applet is the lack of security restrictions. Java applications are given free reign over the system (although they can't get around the operating system's security). A Java application is free to open a socket connection to any host it wants, open any file, and create its own custom class loaders. If you have been banging your head against a wall because you couldn't do these things in an applet, you might be tempted to turn your applets into applications (in other words, make them stand-alone) so you can have all these features. That is, of course, your choice. But you should seriously consider keeping the user interface and the application separate. For some quick hack program that isn't very significant, it probably won't matter. However, if you're writing a big commercial application, it does matter. There are many advantages to being able to run applets in a browser; one of the biggest advantages is that the browser performs automatic software distribution for you. You don't have to install the applet on a system ahead of time in order for someone to use it. If you start writing everything as a stand-alone application, you fall back into the old trap of trying to maintain a program on a large number of machines. Java's database API, called JDBC, is a boon for application programming. You now have a standard interface for accessing a relational database. JDBC frees you from being tied to a specific database API, meaning you not only can create cross-platform applications, you can also create cross-database applications. Java is a great language for handling little ten-minute hack programs, as well. You have immediate access to an excellent set of libraries that handle many tedious functions that you won't find in the standard library set of C or C++. You can buy these libraries for other languages, of course, but why bother if you get them free with Java? You may soon find that you are writing Java programs when you previously wrote C programs or Perl scripts.
Windows, UNIX, or Mac. In the area of audio, you will be able to synchronize your audio a little better, allowing you to create animation that is in sync with the audio. You should also be able to support varying sample rates. Most important of all, you will be able to find out when an audio clip finishes playing. This is one of the most glaring omissions in the current API. You can't even create a simple music jukebox under the current API, because you never know when to start the next piece of music. The video API will allow you to display video clips in different formats, and even synchronize them with the audio. Rather than sticking to a single video format, the video API allows you to plug in different kinds of video handlers. You could support MPEG and QuickTime, for instance. The 2-D API provides a rich set of drawing routines that is badly needed. The Graphics class in the AWT provides only the most basic drawing features. You will be able to perform complex pattern fills with the new API, for example. There will also be an API for doing sprite animation. Sprites are essentially graphical objects that move around the screen. You can do something similar right now, but you have to write all the animation and redrawing code yourself. The sprite API will take care of that for you, and will do it in a much more efficient manner. This should result in a lot of neat new games for Java, many approaching the capabilities of some home gaming systems. You will be able to create interesting new effects with the 3-D API. There will be support for simple 3-D objects, as well as animated 3-D objects, and even some of the features you now find in VRML systems. Again, since these are part of the native Java environment, they should be very efficient. The telephony API addresses the growing integration between the telephone and the computer. Essentially, the telephony API is a mechanism for placing and receiving phone calls. You may need special hardware to interface with the actual phone equipment, but eventually you'll be able to redirect phone calls over your home network to whatever device you happen to be near, whether it is your WebTV, your PDA, or your old desktop computer. Network management is another important topic, especially at large companies. Right now, many operating systems and most network devices support the SNMP network management protocol. There are a number of tools available for configuring and monitoring SNMP-enabled devices. The JavaManagement API will allow you to create new programs for monitoring network devices. You will be able to monitor SNMP devices, or plug in your own protocols to manage devices using other network management protocols. The advantage here is that you will be able to take advantage of Java's ease of use, and create network management applications that will run on any Java-enabled platform. One of the most exciting new Java APIs to come along is the Beans API. Beans is an API for creating and using software components. One of the dreams in software development has always been that software components could be used like electronic components. When you buy electronic components, they have a standard interface. Many times, the same kind of component is offered by a number of
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch1.htm (5 of 7) [8/14/02 10:52:35 PM]
vendors, giving you freedom of choice. You can create an electronic board by looking at the specifications for the components and designing the board. Once you finally assemble the components, you have an excellent chance of things working as you had planned. In the software arena, this is rarely the case. Beans doesn't necessarily solve this problem, but it brings you one step closer. The philosophy behind the Beans API is that you have a nice development tool for creating new applications. In a way, it's like your workbench. You buy software components from different companies, and each component is a bean. You add the beans to your development tool, and the tool uses the Beans API to find out what interfaces each new bean supports. In addition, the Beans API defines mechanisms for customizing a bean. For example, you might buy a nifty new pushbutton bean and add it to your graphical development environment. Your graphical environment presents you with a visual toolkit of all the beans you have. You could select the pushbutton and drag it onto your new application. Next, you could pop up a configuration menu that allowed you to customize the pushbutton. The Beans API uses a new Java feature called reflection to discover the parts of the bean that can be customized. As an alternative, you could supply your own customizer for a bean. If you think you can do a lot with Java now, imagine what you'll be able to do when these new features become available.
Suppose the airline president handed you his Java-enabled organizer and said, "I want to see flights on this thing." Fortunately for you, you separated the application from the user interface, so all you have to do is create a special user interface for the organizer.If you had written the flight tracking system as a big stand-alone application, you would have already torn your hair out in big clumps trying to figure out how you were going to fit all that code into an itty-bitty living space. You may, in the future, have a completely different computing model at home than you do now. Right now, you probably have a single computer, a printer, a monitor, and a modem. Some of you even have your own ethernet networks now. In the future, you may have an application server on which all your favorite programs reside-your e-mail system, your word processor, and yes, your favorite games. This server may not even have a keyboard or a monitor, just a connection to your home network. On your desktop, you might have a Java-enabled monitor and keyboard that are also hooked to the network. In the living room, your Java-enabled television is also on the network. With the coming of digital TV and highspeed networking to the home, there may no longer be a difference between a computer monitor and a television. When you want to read your e-mail, you can access it from the computer monitor, your TV, or even your wireless digital assistant, all using your home network to access the e-mail application running on your home server. You may not even have a server at home-you might subscribe to an e-mail service over the network and access a server somewhere in Tuscaloosa. The point is that there are more and more ways for you to interact with computer systems, and in the future, one single way will no longer be sufficient. As you design your applications, keep the image of a cell phone or a personal digital assistant hovering like a dark cloud over you, whispering menacingly, "Will your application run on me?"
CONTENTS
G G G G G G G
Java and Web Servers Getting Files Using the URL Class Getting Files Using Sockets Performing a Query with GET Posting Data with the URL Class Posting Data Using Sockets Supporting the Cookie Protocol
This input stream will provide you with the contents of the file named by the URL. This method is most useful when you are only concerned with the file contents and not with any of the HTTP headers associated with the file. To get these, you need to use the URLConnection class. The URLConnection class represents a network connection to a WWW resource. When you open an input stream on an URL, it really opens an URLConnection and then calls the getInputStream in the URLConnection object. The following code fragment is the equivalent of the previous example: URL someURL = new URL("http://abcdef.com/mydocument.html"); URLConnection urlConn = someURL.openConnection(); InputStream inStream = urlConn.getInputStream(); The advantage of the URLConnection class is that it gives you much finer control over an URL connection. For example, you can retrieve the headers associated with the file. The two header fields that you will probably be most interested in are the content type and content length. You can fetch these with the getHeaderField and getHeaderFieldInt methods: String contentType = urlConn.getHeaderField("content-type"); int contentLength = urlConn.getHeaderFieldInt( "content-length", -1); // returns -1 if length isn't specified These header fields are so popular, in fact, that they have their own special methods that do the equivalent of the above code-getContentType and getContentLength: String contentType = urlConn.getContentType(); int contentLength = urlConn.getContentLength(); Listing 6.1 shows a sample applet that uses an URL class to read its own .class file.
Listing 6.1 Source Code for FetchURL.java import import import import java.applet.*; java.awt.*; java.net.*; java.io.*;
// This applet demonstrates the use of the URL and URLConnection // class to read a file from a Web server. The applet reads its // own .class file, because you can always be sure it exists. public class FetchURL extends Applet { byte[] appletCode; // Where to store the contents of the .class file public void init() { try {
// Open a URL to this applet's .class file. You can locate it by // using the getCodeBase method and the applet's class name. URL url = new URL(getCodeBase(), getClass().getName()+".class"); // Open a URLConnection to the URL URLConnection urlConn = url.openConnection(); // See if you can find out the length of the file. This allows you to // create a buffer exactly as large as you need. int length = urlConn.getContentLength(); // Because you can't be sure of the size of the .class file, use a // ByteArrayOutputStream as a temporary container. Once you are finished // reading, you can convert it to a byte array. ByteArrayOutputStream tempBuffer; // If you don't know the length of the .class file, use the default size if (length < 0) { tempBuffer = new ByteArrayOutputStream(); } else { tempBuffer = new ByteArrayOutputStream(length); } // Get an input stream to this URL InputStream instream = urlConn.getInputStream(); // Read the contents of the URL and copy it to the temporary buffer int ch; while ((ch = instream.read()) >= 0) { tempBuffer.write(ch); } // Convert the temp buffer to a byte array (you don't do anything with // the array in this applet other than take its size). appletCode = tempBuffer.toByteArray(); } catch (Exception e) { e.printStackTrace(); } } public void paint(Graphics g) { g.setColor(Color.black); if (appletCode == null) { g.drawString("I was unable to read my .class file", 10, 30); } else { g.drawString("This applet's .class file is "+ appletCode.length+" bytes long.", 10, 30);
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch6.htm (3 of 23) [8/14/02 10:52:42 PM]
} } }
Figure 6.1 shows the output from the FetchURL applet. Figure 6.1 : An applet can perform an HTTP GET using the URL class. The FetchURL applet is a typical example of an applet that opens an URL and reads data from it. For example purposes, the applet reads its own .class file. There is no advantage to reading a .class file, but for example purposes it is quite handy, because you know for sure that the .class file must be there. If the .class file wasn't there, the applet wouldn't run in the first place. The applet first opens the URL, and then gets an input stream for the URL. It tries to get the content length, which indicates how much data there is to retrieve. This value isn't always available, however, so the applet uses ByteArrayOutputStream as a temporary storage mechanism. Tip Vectors and byte array output streams are extremely handy storage containers when you don't know the size of the data you are storing. You should use a vector whenever you need to store an unknown number of objects. The byte array output stream is a handy alternative to the vector when you are storing bytes.
Once the applet has read its .class file, it simply displays a message telling how many bytes it read.
Listing 6.2 Source Code for FetchSockURL.java import java.net.*; import java.io.*; // This applet shows you how to open up a socket to an HTTP server // and read a file. The applet reads its own .class file, because // you can always be sure it exists. public class FetchSockURL extends Applet { byte[] appletCode; // Where to store the contents of the .class file public void init() { try { // If the port number returned for the code base is -1, use the // default http port of 80. int port = getCodeBase().getPort(); if (port < 0) port = 80; // Open up a socket to the Web server where this applet came from Socket sock = new Socket(getCodeBase().getHost(),port); // Get input and output streams for the socket connection DataInputStream inStream = new DataInputStream( sock.getInputStream()); DataOutputStream outStream = new DataOutputStream( sock.getOutputStream()); // // // // // // Send the GET request to the server The request is of the form: GET filename HTTP/1.0 In this case, the filename will be the applet's filename as returned by the getCodeBase method. Notice that you send two \r\n's The first one terminates the request line, the second indicates the end of the request header. outStream.writeBytes("GET "+ getCodeBase().getFile()+getClass().getName()+ ".class HTTP/1.0\r\n\r\n"); // Just to show you how it's done, look through the headers for // the content length. First, assume it's -1. int length = -1; String currLine; // Read the next line from the header, quit if you hit EOF
while ((currLine = inStream.readLine()) != null) { // if the length of the line is 0, you just hit the end of the header if (currLine.length() == 0) break; // See if it's the content-length header if (currLine.toLowerCase().startsWith( "content-length:")) { // "content-length:" is 15 characters long, so parse the length starting at // offset 15 (the 16th character). Catch any exceptions when parsing // this number - it's not so important that you have to quit. try { length = Integer.valueOf( currLine.substring(15)). intValue(); } catch (Exception ignoreMe) { } } } // Because you can't be sure of the size of the .class file, use a // ByteArrayOutputStream as a temporary container. Once you are finished // reading, you can convert it to a byte array. ByteArrayOutputStream tempBuffer; // If you don't know the length of the .class file, use the default size if (length < 0) { tempBuffer = new ByteArrayOutputStream(); } else { tempBuffer = new ByteArrayOutputStream(length); } // Read the contents of the URL and copy it to the temporary buffer int ch; while ((ch = inStream.read()) >= 0) { tempBuffer.write(ch); } // Convert the temp buffer to a byte array (you don't do anything with // the array in this applet other than take its size. appletCode = tempBuffer.toByteArray(); } catch (Exception e) { e.printStackTrace(); } } public void paint(Graphics g) { g.setColor(Color.black);
if (appletCode == null) { g.drawString("I was unable to read my .class file", 10, 30); } else { g.drawString("This applet's .class file is "+ appletCode.length+" bytes long.", 10, 30); } } }
Like the FetchURL applet, the FetchSockURL applet reads its own .class file from the Web server. FetchSockURL doesn't use the built-in URL class, however. Instead, it creates a socket connection to the Web server. Once this connection is made, the applet sends a GET request to the Web server to retrieve the .class file. The GET request usually looks something like this: GET /classes/FetchSockURL.class HTTP/1.0 This line is followed by a blank line, indicating the end of the HTTP headers. You can send your own headers immediately after the GET request if you like. Just make sure they appear before the blank line. The FetchSockURL applet actually writes out the blank line in the same statement where it writes out the GET request, so you'll need to remove the \r\n from the end of the writeBytes statement if you add your own headers. If you do that, don't forget to write out a blank line after your headers. Once the GET request has been sent to the server, the applet begins reading lines from the socket connection. The server will send a number of header lines, terminated by a blank line. This will be followed by the actual content of the page. The FetchSockURL applet scans through the headers looking for the content length header field, which usually looks like this: Content-length: 1234 Like the FetchURL applet, the FetchSockURL applet can handle situations where the content length is unknown. It uses the same technique of writing the data to a byte array output stream as it reads it. You can tell when you have reached the end of the content because you'll hit the end of file on the socket (the read method will return -1).
http://localhost/cgi-bin/find-people?occupation=engineer&age=30&name=smith Knowing this, you can easily write a class that takes an URL and a set of parameters and generates a query URL. Listing 6.3 shows just such a class.
Listing 6.3 Source Code for URLQuery.java import java.net.*; import java.util.*; // // // // This class provides a way to create an URL to perform a query against a Web server. The query takes the base URL of the the program you are sending the query to, and a set of properties that will be converted into a query string.
public class URLQuery extends Object { public static URL createQuery(URL originalURL, Properties parameters) throws MalformedURLException { // Queries have the file name followed by a ? String newFile = originalURL.getFile()+"?"; // Now append the query parameters to the filename Enumeration e = parameters.propertyNames(); boolean firstParameter = true; while (e.hasMoreElements()) { String propName = (String) e.nextElement(); // Parameters are separated by &'s, if this isn't the first parameter // append a & to the current query string (file name) if (!firstParameter) newFile += "&"; // Add the variable name to the query string newFile += URLEncoder.encode(propName); // Get the variable's value String prop = parameters.getProperty(propName); // If the variable isn't null, append "=" followed by the value if (prop != null) { newFile += "="+URLEncoder.encode(prop); } firstParameter = false; }
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch6.htm (8 of 23) [8/14/02 10:52:42 PM]
// // // //
Return the full URL consisting of the original protocol, host, and port and the new, enhanced filename, which contains all the query parameters. This URL is suitable for opening with showDocument or any other URL operation. return new URL(originalURL.getProtocol(), originalURL.getHost(), originalURL.getPort(), newFile); }
You retrieve the results of a query just like you retrieve any other file on the Web. You can open up a stream directly from the URL, you can get a URLConnection object, or you can open up a socket and speak directly to the server. Because queries frequently return Web pages, you may want to use the openDocument method in the Applet class. This enables you to see the results of the query all neatly formatted by the Web browser instead of the raw HTML codes that you get from an input stream. Listing 6.4 shows an applet that submits a query to the Lycos search engine (http://www.lycos.com) and displays the results using showDocument.
Listing 6.4 Source Code for LycosQuery.java import import import import java.applet.*; java.util.*; java.net.*; java.io.*;
// This applet performs a query against the Lycos search engine // and opens up the results as a new document. public class LycosQuery extends Applet { public void init() { try { // Create the base URL to the lycos query URL url = new URL( "http://www.lycos.com/cgi-bin/pursuit"); Properties queryProps = new Properties(); // // // // Fill in the query variables. These were determined by looking at the Lycos query form. You search on the terms "java" and "cgi" requesting a maximum of 20 entries. The minscore value of .5 is what Lycos calls a "good match". queryProps.put("query", "java cgi"); queryProps.put("matchmode", "and");
queryProps.put("maxhits", "20"); queryProps.put("minscore", ".5"); queryProps.put("terse", "standard"); // Create the query URL URL fullURL = URLQuery.createQuery(url, queryProps); // Open up the results as a new document getAppletContext().showDocument(fullURL); } catch (Exception e) { e.printStackTrace(); } } }
Figure 6.2 shows the results of the Lycos query generated by the LycosQuery applet. Figure 6.2 : You can create a query and then use showDocument to display the results.
You should set a content type for the data you are sending. A typical content type would be application/octetstream: myURLConnection.setRequestProperty("Content-type", "application/octet-stream"); You are required to send a content length in a POST message. You can set this the same way you set the content type: myURLConnection.setRequestProperty("Content-length", ""+stringToSend.length()); // cheap way to convert int to string Once you have the headers taken care of, you can open up an output stream and write the content to the stream: DataOutputStream outStream = new DataOutputStream( myURLConnection.getOutputStream()); outStream.writeBytes(stringToSend()); Make sure that the string you send is terminated with \r\n. Once you have sent the information for the post, you can open up an input stream and read the response back from the server just as you did with a GET. Listing 6.5 shows an application that sends a POST message to one of the NCSA's example CGI programs.
Listing 6.5 Source Code for URLPost.java import java.net.*; import java.io.*; public class URLPost extends Object { public static void main(String args[]) { try { URL destURL = new URL( "http://hoohoo.ncsa.uiuc.edu/cgi-bin/test-cgi/foo"); // The following request data mimics what the NCSA example CGI // form for this CGI program would send. String request = "button=on\r\n"; URLConnection urlConn = destURL.openConnection(); urlConn.setDoOutput(true); // we need to write urlConn.setDoInput(true); // just to be safe... urlConn.setUseCaches(false); // get info fresh from server // Tell the server what kind of data you are sending - in this case, // just a stream of bytes.
urlConn.setRequestProperty("Content-type", "application/octet-stream"); // Must tell the server the size of the data you are sending. This also // tells the URLConnection class that you are doing a POST instead // of a GET. urlConn.setRequestProperty("Content-length", ""+request.length()); // Open an output stream so you can send the info you are posting DataOutputStream outStream = new DataOutputStream( urlConn.getOutputStream()); // Write out the actual request data outStream.writeBytes(request); outStream.close(); // Now that you have sent the data, open up an input stream and get // the response back from the server DataInputStream inStream = new DataInputStream( urlConn.getInputStream()); int ch; // Dump the contents of the request to System.out while ((ch = inStream.read()) >= 0) { System.out.print((char) ch); } inStream.close(); } catch (Exception e) { e.printStackTrace(); } } }
Figure 6.3 shows the working of this application. Figure 6.3 : A Java applet or application can use the URL class to perform an HTTP POST.
to do a whole lot. It is basically the same method you used when you wrote a socket-based HTTP GET, but in addition to sending the GET command, you must also send the "Content-type," and "Content-length" messages, as well as the request data. Listing 6.6 shows the socket-based equivalent of the example program in Listing 6.5.
Listing 6.6 Source Code for PostSockURL.java import java.net.*; import java.io.*; // This applet shows you how to open up a socket to an HTTP server // and post data to a server. It posts information to one of the // example CGI programs set up by the NCSA. public class PostSockURL extends Object { public static void main(String args[]) { try { // Open up a socket to the Web server where this applet came from Socket sock = new Socket("hoohoo.ncsa.uiuc.edu", 80); // Get input and output streams for the socket connection DataInputStream inStream = new DataInputStream( sock.getInputStream()); DataOutputStream outStream = new DataOutputStream( sock.getOutputStream()); // This request is what is sent by the NCSA's example form String request = "button=on\r\n"; // Send the POST request to the server // The request is of the form: POST filename HTTP/1.0 outStream.writeBytes("POST /cgi-bin/test-cgi/foo "+ " HTTP/1.0\r\n"); // Next, send the content type (don't forget the \r\n) outStream.writeBytes( "Content-type: application/octet-stream\r\n"); // Send the length of the request outStream.writeBytes( "Content-length: "+request.length()+"\r\n"); // Send a \r\n to indicate the end of the header outStream.writeBytes("\r\n");
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch6.htm (13 of 23) [8/14/02 10:52:42 PM]
// Now send the information you are posting outStream.writeBytes(request); // Dump the response to System.out int ch; while ((ch = inStream.read()) >= 0) { System.out.print((char) ch); } // We're done with the streams, so close them inStream.close(); outStream.close(); } catch (Exception e) { e.printStackTrace(); } } }
Tip It is often useful to create a string representation of an object that can be used to recreate the object at a later time. While you can use object serialization to read and write objects to a file, a string representation can be edited with a simple text editor.
Listing 6.7 Source Code for Cookie.java import java.net.*; import java.util.*; // // // // This class represents a Netscape cookie. It can parse its values from the string from a Set-cookie: response (without the Set-cookie: portion, of course). It is little more than a fancy data structure.
public class Cookie { // Define the standard cookie fields public public public public public public // // // // String name; String value; Date expires; String domain; String path; boolean isSecure;
cookieString is the original string from the Set-cookie header. Just save it rather than trying to regenerate for the toString method. Note that since this class can initialize itself from this string, it can be used to save a persistent copy of this class! public String cookieString;
// Initialize the cookie based on the origin URL and the cookie string public Cookie(URL sourceURL, String cookieValue) { domain = sourceURL.getHost(); path = sourceURL.getFile(); parseCookieValue(cookieValue); } // Initialize the cookie based solely on its cookie string public Cookie(String cookieValue) { parseCookieValue(cookieValue);
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch6.htm (15 of 23) [8/14/02 10:52:42 PM]
} // Parse a cookie string and initialize the values protected void parseCookieValue(String cookieValue) { cookieString = cookieValue; // Separate out the various fields, which are separated by ;'s StringTokenizer tokenizer = new StringTokenizer( cookieValue, ";"); while (tokenizer.hasMoreTokens()) { // Eliminate leading and trailing white space String token = tokenizer.nextToken().trim(); // See if the field is of the form name=value or if it is just // a name by itself. int eqIndex = token.indexOf('='); String key, value; // If it is just a name by itself, set the field's value to null if (eqIndex == -1) { key = token; value = null; // Otherwise, the name is to the left of the '=', value is to the right } else { key = token.substring(0, eqIndex); value = token.substring(eqIndex+1); } isSecure = false; // convert the key to lowercase for comparison with the standard field names String lcKey = key.toLowerCase(); if (lcKey.equals("expires")) { expires = new Date(value); } else if (lcKey.equals("domain")) { if (isValidDomain(value)) { domain = value; } } else if (lcKey.equals("path")) { path = value; } else if (lcKey.equals("secure")) { isSecure = true; // If the key wasn't a standard field name, it must be the cookie's name
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch6.htm (16 of 23) [8/14/02 10:52:42 PM]
// You don't use the lowercase version of the name here. } else { name = key; this.value = value; } } } // // // // // // isValidDomain performs the standard cookie domain check. A cookie domain must have at least two portions if it ends in .com, .edu, .net, .org, .gov, .mil, or .int. If it ends in something else, it must have 3 portions. In other words, you can't specify .com as a domain, it has to be something.com, and you can't specify .ga.us as a domain, it has to be something.ga.us. protected boolean isValidDomain(String domain) { // Eliminate the leading period for this check if (domain.charAt(0) == '.') domain = domain.substring(1); StringTokenizer tokenizer = new StringTokenizer(domain, "."); int nameCount = 0; // just count the number of names and save the last one you saw String lastName = ""; while (tokenizer.hasMoreTokens()) { lastName = tokenizer.nextToken(); nameCount++; } // At this point, nameCount is the number of sections of the domain // and lastName is the last section. // More than 2 sections is okay for everyone if (nameCount > 2) return true; // Less than 2 is bad for everyone if (nameCount < 2) return false; // Exactly two, you better match one of these 7 domain types if (lastName.equals("com") || lastName.equals("edu") || lastName.equals("net") || lastName.equals("org") || lastName.equals("gov") || lastName.equals("mil") || lastName.equals("int")) return true; // Nope, you fail - bad domain! return false; } // Use the cookie string as originally set in the Set-cookie header // field as the string value of this cookie. It is unique, and if you write
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch6.htm (17 of 23) [8/14/02 10:52:42 PM]
// this string to a file, you can completely regenerate this object from // this string, so you can read the cookie back out of a file. public String toString() { return cookieString; } }
The Cookie class is basically a holder for cookie data. The only methods in the Cookie class deal with converting strings into cookies and vice versa. The parseCookieValue method in the Cookie class implements a crucial part of the cookie protocol. It takes a string containing the settings for a cookie. The settings are of the form name=value and are separated by semicolons. The settings include the name of the cookie, the cookie's value, its expiration date, and the path name for the cookie. The domain setting for a cookie specifies which hosts should receive the cookie. Whenever a URL in the cookie's domain is opened and the URL is in the cookie's path, the server for that URL is passed the cookie. For example, if you set the domain to mydomain.com and the path to /me/stuff, then the URL http://mydomain. com/me/stuff/mycgi will receive the cookie. An URL of http://mydomain.com/you/files would not receive the cookie, because the paths don't match. There are some restrictions on the cookie's domain, too. If the domain ends in .com, .edu, .org, .net, .gov, .mil, or .int, you only need two components in the domain. In other words, you need one other name in addition to the ending. For example, mydomain.com is a valid domain. If the domain ends with any other name, you must have at least three components in the domain. For example, mydomain.au would not be a valid cookie domain, but mydomain.outback.au would be valid. Because cookies are supposed to be persistent, you need a class to manage your cookies-preferably by storing them in a file or a database. Listing 6.8 presents a portion of the CookieDatabase class that maintains a table of known cookies. The full source to the class is available on the CD-ROM that comes with this book. It has methods to store the table in a file and retrieve the table from a file. It can also examine an URL and return a string of cookie values for that URL. The CookieDatabase class does not actually read cookies from a Web server or write them to the server. It simply keeps a table of known cookies. If presented with a host name and path name, the CookieDatabase class will determine which cookies are valid for that host name and path name and will return the appropriate cookie string. The getCookieString method from the CookieDatabase class, shown in Listing 6.8, performs the matching between an URL and a cookie. It decides what cookies should be sent for a particular URL and creates a string containing all the cookie values that need to be sent.
Listing 6.8 getCookieString Method from CookieDatabase // // // // getCookieString does some rather ugly things. First, it finds all the cookies that are supposed to be sent for a particular URL. Then it sorts them by path length, sending the longest path first (that's what Netscape's specs say to do - I'm only following orders).
public static String getCookieString(URL destURL) { if (cookies == null) { cookies = new Vector(); } // sendCookies will hold all the cookies you need to send Vector sendCookies = new Vector(); // currDate will be used to prune out expired cookies as we go along Date currDate = new Date(); for (int i=0; i < cookies.size();) { Cookie cookie = (Cookie) cookies.elementAt(i); // See if the current cookie has expired. If so, remove it if ((cookie.expires != null) && (currDate.after( cookie.expires))) { cookies.removeElementAt(i); continue; } // You only increment i if you haven't removed the current element i++; // If this cookie's domain doesn't match the URL's host, go to the next one if (!destURL.getHost().endsWith(cookie.domain)) { continue; } // If the paths don't match, go to the next one if (!destURL.getFile().startsWith(cookie.path)) { continue; } // Okay, you've determined that the current cookie matches the URL, now // add it to the sendCookies vector in the proper place (i.e. ensure // that the vector goes from longest to shortest path). int j; for (j=0; j < sendCookies.size(); j++) { Cookie currCookie = (Cookie) sendCookies. elementAt(j); // If this cookie's path is longer than the cookie[j], you should insert // it at position j. if (cookie.path.length() < currCookie.path.length()) { break; }
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch6.htm (19 of 23) [8/14/02 10:52:42 PM]
} // If j is less than the array size, j represents the insertion point if (j < sendCookies.size()) { sendCookies.insertElementAt(cookie, j); // Otherwise, add the cookie to the end } else { sendCookies.addElement(cookie); } } // Now that the sendCookies array is nicely initialized and sorted, create // a string of name=value pairs for all the valid cookies String cookieString = ""; Enumeration e = sendCookies.elements(); boolean firstCookie = true; while (e.hasMoreElements()) { Cookie cookie = (Cookie) e.nextElement(); if (!firstCookie) cookieString += "; "; cookieString += cookie.name + "=" + cookie.value; firstCookie = false; } // Return null if there are no valid cookies if (cookieString.length() == 0) return null; return cookieString; }
Finally, Listing 6.9 shows you an example application that fetches a Web page that contains a cookie. Whenever the application runs, it loads its cookie table from a file called cookies.dat. After you run the program, you can look at the cookies.dat file. It is printable text. The program accesses a Web page called "Andy's Netscape HTTP Cookie Page" (http://www.illuminatus.com/cookie), which is a great resource for learning about cookies and seeing them in action. Since the CookieDatabase class does not automatically look for cookies in a response from a Web server, and does not automatically send cookie data, you have to do that yourself. Cookies are sent to the server in the header portion of an HTTP command. Note You can set only a few specific header values in the URL class, and the cookie string is not one of them. This means that you have to use sockets to perform a GET or POST that supports cookies.
Whenever you open an URL, you can get the cookie string for the URL by calling getCookieString in the CookieDatabase class. When reading the response from the Web server, you must scan the header results for the Setcookie command. Whenever you find this command, you pass the cookie string from the Set-cookie command to the addCookie method in the CookieDatabase class. The method will extract all the important information from the cookie string.
Listing 6.9 Source Code for TestCookie.java import java.net.*; import java.io.*; // // // // // // // // // This application demonstrates the CookieDatabase and Cookie classes. It first loads the cookie database from cookies.dat, then it opens up Andy's Netscape HTTP Cookie Page, which happens to assign you a cookie. Because the Java URL classes do not let you set arbitrary header strings (GRR!!!), you have to do cookie stuff MANUALLY (double-GRR!!) Much of this code was taken from the example of doing a GET with raw sockets.
public class TestCookie extends Object { public static void main(String args[]) { try { CookieDatabase.loadCookies("cookies.dat"); } catch (IOException ignore) { } try { // URL to Andy's Netscape HTTP Cookie Page, it's quite helpful URL url = new URL("http://www.illuminatus.com/cookie"); int port = url.getPort(); if (port < 0) port = 80; // Open a socket to the server Socket socket = new Socket(url.getHost(), port); // Create an output stream so you can write out the request header DataOutputStream outStream = new DataOutputStream( socket.getOutputStream()); // Write the GET command outStream.writeBytes( "GET "+url.getFile()+" HTTP/1.0\r\n"); // See if there are any valid cookies for this URL
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch6.htm (21 of 23) [8/14/02 10:52:42 PM]
String cookieString = CookieDatabase. getCookieString(url); // If so, write out a cookie header if (cookieString != null) { outStream.writeBytes("Cookie: "+ cookieString+"\r\n"); } // Write out \r\n for the end of the header area outStream.writeBytes("\r\n"); // Now read the response from the server DataInputStream inStream = new DataInputStream( socket.getInputStream()); String line; // Read the header strings scanning for a set-cookie tag, which // means you have to update the cookie database while ((line = inStream.readLine()) != null) { if (line.length() == 0) break; // if you got a set-cookie, create a new cookie and add it to the database if (line.toLowerCase().startsWith( "set-cookie: ")) { CookieDatabase.addCookie( new Cookie(url, line.substring(12))); } } // Now that you've finished with the header, just dump out the // contents of the page. This won't look too pretty, it's all pure // HTML. int ch; while ((ch = inStream.read()) >= 0) { System.out.print((char) ch); } // Save the cookie database for later use CookieDatabase.saveCookies("cookies.dat"); } catch (Exception e) { e.printStackTrace(); } } }
CONTENTS
G G G G G G G
Smarter Forms Creating Forms with the AWT Checking for Errors on the Client Side Adding Context-Sensitive Help Creating Dynamic Forms Loading Another URL from an Applet Creating Image Maps with Hot Spots
Smarter Forms
In the beginning, Web pages were not very lively. You could read information, click certain words and pictures, and view other unlively pages. Then, the forms interface came along and added some degree of interaction with a page. You were able to enter data and then click a button and send your information to a server, which would analyze what you sent and return the results. Unfortunately, these forms were also lacking a certain "lively" quality. All the error checking was left up to the server, as was any other form of interaction such as context-sensitive help. Java enables you to spice up your old Web forms. You can perform error checking before you ever send data to the server, drastically improving response time to the user and cutting down on server usage. You can also add context-sensitive help. You can even create dynamic forms that change depending on the other information added.
This applet is meant for demonstration purposes only. While it will function with the real Lycos server, it does not display the advertisements from the normal Lycos search page. Although you may consider this a plus, it really isn't. Advertisements keep companies like Lycos in business and allow them to provide these wonderful services to you at no cost. Please do not use this applet or any other program to thwart a company's advertising displays. It hurts everyone in the long run.
Listing 7.1 Source Code for LycosForm.java import import import import // // // // java.awt.*; java.applet.*; java.net.*; java.util.*;
This applet demonstrates the use of AWT components as an alternative to the HTML forms interface. It creates a query for the Lycos search engine and displays the results using the showDocument method.
public class LycosForm extends Applet { protected TextField queryString; // the terms to search for protected Choice matchTerms; // how many terms to match String matchTermValues[] = { "and", "or", "2", "3", "4", "5", "6", "7" }; protected Choice matchStrength; // how good a match String matchStrengthValues[] = { ".1", ".3", ".5", ".7", ".9" }; protected Choice resultCount; // how many matches to show String resultCountValues[] = { "10", "20", "30", "40" }; protected Choice resultType; // how much information to display String resultTypeValues[] = { "terse", "standard", "verbose" }; protected Button searchButton; // perform the query public void init() { // Arrange the query form as a 3 horizontal grid elements setLayout(new GridLayout(3, 0)); // Create the element with the query string and submit button add(createQueryPanel()); // Create the element containing search options
The AWT layout managers provide a reasonable way to place components on the screen without putting them in fixed positions. This allows your applet to adapt to different screen sizes. Unfortunately, it is often difficult to arrange the components the way you want them. The GridBagLayout class provides the most flexible way to arrange components, but it is often rather cumbersome to use. As an alternative to the GridBagLayout class, or even in conjunction with it, you can use different panels to group your components, nesting some panels within others. The LycosQuery class uses this technique. It creates a main panel that uses a grid layout with three rows. The first row is another panel that uses a flow layout, while the last two rows use two-column grid layouts. Tip Grid layouts expand components to fill all available space. If you want to maximize a component's size, the grid layout is a good choice. Flow layouts, on the other hand, don't adjust the component size, so they tend to use the minimum required space. Grid bag layouts let you choose either of these options.
Listing 7.1 Source Code for LycosForm.java (continued) // createQueryPanel creates a panel containing a text field // for query terms and the button used to send the query to Lycos protected Panel createQueryPanel() { Panel panel = rEw Panel(); panel.setLayout(new FlowLayout(FlowLayout.LEFT)); panel.add(new Label("Query: ")); queryString = new TextField(30); panel.add(queryString); searchButton = new Button("Search"); panel.add(searchButton); return panel; } // createSearchOptionsPanel creates a panel containing the // choices for the number of terms to match and the strength // of the matches.
protected Panel createSearchOptionsPanel() { Panel panel = new Panel(); panel.setLayout(new GridLayout(0, 3)); panel.add(new Label("Search Options:")); matchTerms = new Choice(); matchTerms.addItem("match all terms (AND)"); matchTerms.addItem("match any term (OR)"); matchTerms.addItem("match 2 terms"); matchTerms.addItem("match 3 terms"); matchTerms.addItem("match 4 terms"); matchTerms.addItem("match 5 terms"); matchTerms.addItem("match 6 terms"); matchTerms.addItem("match 7 terms"); matchTerms.select(1); panel.add(matchTerms); // default on the OR option
matchStrength = new Choice(); matchStrength.addItem("loose match"); matchStrength.addItem("fair match"); matchStrength.addItem("good match"); matchStrength.addItem("close match"); matchStrength.addItem("strong match"); matchStrength.select(0); // default on the loose match panel.add(matchStrength); return panel; } // createDisplayOptionsPanel creates a panel containing the choices for // the number of matches returned and the amount of detail to return. protected Panel createDisplayOptionsPanel() { Panel panel = new Panel(); panel.setLayout(new GridLayout(0, 3)); panel.add(new Label("Display Options:")); resultCount = new Choice(); resultCount.addItem("10 results resultCount.addItem("20 results resultCount.addItem("30 results resultCount.addItem("40 results resultCount.select(0); panel.add(resultCount);
resultType.addItem("Standard Results"); resultType.addItem("Detailed Results"); resultType.select(1); panel.add(resultType); return panel; } // Default to Standard Results
The URLQuery class used in this next part of the LycosQuery class was introduced in the section, "Performing a Query with GET," in Chapter 6, "Communicating with a Web Server." It allows you to create an HTTP query from an URL and a properties table containing the query parameters. It would be nice if you could examine the data coming back from the query and still let the browser display the actual HTML codes returned, but on most browsers you can't. You can either examine the data coming back and display it yourself from the Java program, or use showDocument to display the data directly.
Listing 7.1 Source Code for LycosForm.java (continued) // sendRequest uses the URLGet class to create a CGI Query to Lycos. protected void sendRequest() { Properties queryProps = new Properties(); queryProps.put("query", queryString.getText()); queryProps.put("matchmode", matchTermValues[ matchTerms.getSelectedIndex()]); queryProps.put("minscore", matchStrengthValues[ matchStrength.getSelectedIndex()]); queryProps.put("maxhits", resultCountValues[ resultCount.getSelectedIndex()]); queryProps.put("terse", resultTypeValues[ resultType.getSelectedIndex()]); try { URL lycosURL = new URL( "http://www.lycos.com/cgi-bin/pursuit"); URL fullURL = URLQuery.createQuery(lycosURL, queryProps); getAppletContext().showDocument(fullURL); } catch (Exception e) { e.printStackTrace(); } }
public boolean action(Event evt, Object whichAction) { // If someone pressed the button, send the request if (evt.target == searchButton) { sendRequest(); return true; } return false; } }
Figure 7.1 shows the original version of the Lycos query form. Figure 7.1 : The Lycos search engine is a popular Web search tool. Figure 7.2 shows a mimic of an HTML form. Figure 7.2 : You can mimic any HTML form in Java. You may be wondering why you should go through the trouble of creating a Java applet that presents a form when it is easier to define one in HTML. If you are simply presenting a form, with no help facility and no error checking, go ahead and do it in HTML. The real advantage of Java comes when you need to do things beyond the basic form facilities in HTML.
if (evt.target == searchButton) { checkRequest(); return true; } return false; } protected void checkRequest() { if (queryString.getText().length() == 0) { OKDialog.createOKDialog( "Please enter a list of terms to search for"); return; } sendRequest(); } This is actually a pretty minor form of error checking. On more advanced forms, you may need to check to see that information entered in one section is consistent with information entered in another area. For example, you might have a "sex" field on your form and a "maiden name" field somewhere else. If sex was "male," the maiden name doesn't apply. Your error checking routine would check to make sure that if you entered something under "maiden name," you had better be female. You can avoid some situations such as this one by creating dynamic forms, which are discussed later in this chapter.
disappear. One thing to keep in mind when you want to create dialog boxes is that you must have a parent frame for the dialog box. When you are running an applet, you can't normally access the applet's parent frame. The HelpDialog class addresses this problem by creating its own frame. It saves the frame in a static variable so it doesn't have to create a new frame the next time it needs to create a dialog window. You can actually access the parent frame for an applet. Sometimes it will work exactly like you want. It usually works for dialogs, but it fails miserably on some platforms when you try to create a menu for the parent frame. You can use the getParent method from the component class to trace back up through the component hierarchy to find the applet's parent frame. The following code fragment finds an applet's parent frame: Component parentFrame = getParent(); while ((parentFrame != null) && !(parentFrame instanceof Frame)) { parentFrame = parentFrame.getParent(); } Frame myFrame = (Frame) parentFrame; At this point, myFrame would either contain the parent frame of the applet, or null if it couldn't find the parent frame.
Listing 7.2 Source Code for HelpDialog.java import java.awt.*; // The HelpDialog class is a variation on the OKDialog class. // It allows you to create an OK dialog with a textarea instead // of a label. You can use this to display help text. public class HelpDialog extends Dialog { protected Button okButton; protected static Frame createdFrame; public HelpDialog(Frame parent, String message) { super(parent, false); // Must call the parent's constructor // Create the OK button and the message to display okButton = new Button("OK"); TextArea helpInfo = new TextArea(message, 10, 40); helpInfo.setEditable(false); setLayout(new BorderLayout()); add("Center", helpInfo); add("South", okButton); resize(500, 300); }
// The action method just waits for the OK button to be clicked; // when it is, it hides the dialog, causing the show() method to return // back to whoever activated this dialog. public boolean action(Event evt, Object whichAction) { if (evt.target == okButton) { hide(); if (createdFrame != null) { createdFrame.remove(this); createdFrame.hide(); dispose(); return true; } } return true; } // Shortcut to create a frame automatically, the frame is a static variable // so all dialogs in an applet or application can use the same frame. public static void createHelpDialog(String helpText) { // If the frame hasn't been created yet, create it if (createdFrame == null) { createdFrame = new Frame("Help"); } // Create the dialog now HelpDialog helpDialog = new HelpDialog(createdFrame, helpText); // Shrink the frame to nothing createdFrame.resize(0, 0); // Show the dialog createdFrame.show(); helpDialog.show(); } }
In addition to the HelpDialog class, you need a way to assign help information directly to your AWT components. It would have been nice if Sun had built that right into the AWT, and maybe they will in the future, but for now you have to do it yourself. You could subclass all the AWT components to support help if you really had nothing better to do for a month or two, but there are easier ways. One simple way is just to store the components and their corresponding help text in a hash table. Whenever someone requests help from within an AWT component, look in the table and see if you have defined any help for that component. Listing 7.3 shows the HelpSystem class that enables you to assign help text to
AWT components. It also contains a method to display the help for a component, but it makes no assumptions on how you actually request the help in the first place.
Listing 7.3 Source Code for HelpSystem.java import java.awt.*; import java.util.*; // Help system is a container for help strings. You can add // and remove help strings for components. It also provides // a doHelp method that actually pops up the help dialog. public class HelpSystem extends Object { Hashtable helpTable; public HelpSystem() { helpTable = new Hashtable(); } public void addHelp(Component comp, String text) { helpTable.put(comp, text); } public void removeHelp(Component comp) { helpTable.remove(comp); } public boolean doHelp(Component comp) { if (comp == null) return false; String helpString = (String) helpTable.get(comp); if (helpString == null) { return false; } HelpDialog.createHelpDialog(helpString); return true; } }
Now that you have a way to display help and a way to map help strings to components, you need to add some sort of help
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch7.htm (10 of 27) [8/14/02 10:52:50 PM]
key to your applet. Going back to the Lycos search form applet, you can modify it to use F1 as the help key. The AWT components are polite enough to ignore keyDown events for keys they do not recognize, and they all leave the F1 key alone. You can trap the F1 key in your applet and display the appropriate help text. To add context-sensitive help to the LycosForm class, you need to create an instance of the help system. Since there are several methods that actually use the help system, you declare it as an instance variable: protected HelpSystem helpSystem = new HelpSystem(); Next, for each component that will have a help screen, you add the component to the help system. For example, once you create the queryString text field, you can add a help string for it with the following code fragment: helpSystem.addHelp(queryString, "QUERY HELP\nEnter the words you want to search\n"+ "for separated by spaces. Avoid common words like\n "+ "\"the\" or \"and\"."); The trickiest part of implementing the help system is grabbing the F1 key and figuring out which component the user wants help on. When you receive keyboard events, you are given an x and y coordinate where the keystroke occurred. Unfortunately, this does not really indicate where the mouse was when you pressed the key. The x and y coordinates are bounded by the component that currently has the keyboard focus. For context-sensitive help, you don't want the user to have to move the keyboard focus to another component before requesting help. If this were the case, they would have to click a button before they could get help for that button. What you must do, instead, is track the movement of the mouse all the time. You can do this very simply by creating two instance variables in your class, mouseX and mouseY: protected int mouseX; protected int mouseY; // the current X coord of the mouse // the current Y coord of the mouse
Next, you override the mouseMove method. This method is called whenever the mouse moves. You simply copy the x and y coordinates of the mouse and return: public { mouseX mouseY return } boolean mouseMove(Event evt, int x, int y) = x; = y; false;
Notice that you return false from the mouseMove method. This indicates that you haven't actually handled the mouse movement event, allowing the event to be passed to another component. If you do not want another component to see the mouse movement event, you should return true instead. The hardest part of implementing this context-sensitive help system is determining which component the user wants help on. The problem here is that you have to take the x and y coordinates of the mouse and locate the component at those coordinates. The locate method does this, sort of. The locate method takes an x and y coordinate and returns the component at those coordinates. It only looks one level deep in the component hierarchy, however. If you are using nested panels, as the LycosForm applet does, the locate method will only return the panel enclosing the component you really want.
The solution for this problem is simple. If the locate method returns a container, you use the locate method in that container. You keep repeating the process until locate returns a component that is not a container. There is one additional little sticking point here. The locate method expects the x and y coordinates to be relative to the container you are searching. The first time you call locate, everything is fine, since the mouse x and y coordinates are relative to your applet. After that, you have to adjust them to be relative to the container returned by locate. For example, suppose you had mouse coordinates of 100, 50 and the locate method returned an instance of the Panel class for those coordinates. Suppose that the panel's upper-left corner was at 65, 45. You would subtract the panel's coordinates from the original mouse coordinates, giving a new location of 35,5. Now you call the locate method in the panel with the new coordinates. You can use the location method to get the coordinates of the upper-left corner of any component. Listing 7.4 shows a keyDown method for the LycosForm applet that uses this technique to identify the component where the F1 key was pressed.
Listing 7.4 Source Code for the keyDown Method in LycosForm3.java public boolean keyDown(Event evt, int ch) { if (ch == Event.F1) { int x = mouseX; int y = mouseY; // Find out which component this x,y is inside Component whichComp = locate(x, y); // If the component is a container, descend into the container and // find out which of its components contains this x,y while (whichComp instanceof Container) { // If you have to search within a container, adjust the x,y to be relative // to the container. x -= whichComp.location().x; y -= whichComp.location().y; Component nextComp = whichComp.locate(x, y); // if locate returns the component itself, you're done if (nextComp == whichComp) break; whichComp = nextComp; } // Display any available help on the component helpSystem.doHelp(whichComp); } return false; }
Figure 7.3 shows the LycosForm3 applet in action with a Help dialog box displayed.
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch7.htm (12 of 27) [8/14/02 10:52:50 PM]
Figure 7.3 : Context-sensitive help screens make your applets easier to use.
Listing 7.5 Source Code for DynamicDisable.java import java.awt.*; import java.applet.*; // // // // // This applet demonstrates the technique of enabling and disabling components based on the values of other components. Specifically, it has a choice for sex of "Male" or "Female". It also has a maiden name field that is enabled only if sex is "Female".
public class DynamicDisable extends Applet { TextField maidenName; Choice sex; public void init() { // Create the sex choice sex = new Choice(); sex.addItem("Male"); sex.addItem("Female"); // Default to male sex.select(0); add(sex); // Create maiden name and disable it because sex defaults to male maidenName = new TextField(20);
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch7.htm (13 of 27) [8/14/02 10:52:50 PM]
maidenName.disable(); add(maidenName); } public boolean action(Event evt, Object whichAction) { // If you get an action event on sex, look at the current // value and enable or disable maiden name accordingly if (evt.target == sex) { // If the index is 0, "male" has been selected, so disable maiden name if (sex.getSelectedIndex() == 0) { maidenName.disable(); // otherwise, enable maiden name } else { maidenName.enable(); } return true; } return false; } }
This technique doesn't provide much of an improvement over paper forms, however. You could still be looking at a huge document full of components, some of which are enabled and some which are disabled. It would be a lot kinder to the user to show him only the items he actually needs to fill in. In other words, rather than just disabling a component, hide it-make it invisible. Hiding has its drawbacks, however. When you hide a component, the layout manager will change the layout of the components. If you aren't using a layout manager, this won't be a problem. If you are using a layout manager, pay special attention to how the form changes when you show and hide various components. You may want to perform a mixture of disabling and hiding. Listing 7.6 shows a very brief example of how to hide and show components dynamically, using the same components as the example in Listing 7.5. Notice that you must call the validate method after hiding or showing a component. This causes the layout manager to recompute the component positions.
Listing 7.6 Source Code for DynamicHide.java import java.awt.*; import java.applet.*; // // // // // This applet demonstrates the technique of hiding and showing components based on the values of other components. Specifically, it has a choice for sex of "Male" or "Female". It also has a maiden name field that is visible only if sex is "Female".
public class DynamicHide extends Applet { TextField maidenName; Choice sex; public void init() { // Create the sex choice sex = new Choice(); sex.addItem("Male"); sex.addItem("Female"); // Default to male sex.select(0); add(sex); // Create maiden name and hide it because sex defaults to male maidenName = new TextField(20); maidenName.hide(); add(maidenName); } public boolean action(Event evt, Object whichAction) { // If you get an action event on sex, look at the current // value and show or hide maiden name accordingly if (evt.target == sex) { // If the index is 0, "male" has been selected, so hide maiden name if (sex.getSelectedIndex() == 0) { maidenName.hide(); validate(); // otherwise, show maiden name } else { maidenName.show(); validate(); } return true; } return false; } }
The CardLayout layout manager is another good tool for dynamic form construction. It lets you create a stack of different containers (usually panels), only one of which is displayed at any time. By using a card layout, you can create all your panels ahead of time and add them to the card layout. Then, whenever you want to display a specific panel, you tell the card which panel to display. For example, you may have panels that display information on Moe, Larry, and Curly. Listing 7.7 shows a simple example program that uses a card layout and some buttons to select the specific card.
Listing 7.7 Source Code for CardExample.java import java.applet.*; import java.awt.*; // // // // // // This applet demonstrates how a card layout can be used to display different panels. The panels are given names when added to the card layout. There are buttons at the bottom of the screen with names corresponding to the panel names. When you press a button, it tells the card layout to display the card with the same name as the button.
public class CardExample extends Applet { CardLayout cards; Panel stoogePanel; public void init() { // Need a border layout to have the stooge panel in the center and // the buttons at the bottom. setLayout(new BorderLayout()); // Create the main display panel stoogePanel = new Panel(); // Give the main display panel a card layout cards = new CardLayout(); stoogePanel.setLayout(cards); // Create the panels for the different cards. For demo purposes, each // panel just has a label on it. Panel moePanel = new Panel(); moePanel.add(new Label("Moe")); Panel larryPanel = new Panel(); larryPanel.add(new Label("Larry")); Panel curlyPanel = new Panel(); curlyPanel.add(new Label("Curly"));
// Add the separate panels to the stoogePanel giving them their // own card names. stoogePanel.add("Moe", moePanel); stoogePanel.add("Larry", larryPanel); stoogePanel.add("Curly", curlyPanel); // Put the stoogePanel in the middle of the applet's border layout add("Center", stoogePanel); // Now create a row of buttons for selecting the different cards. The // button names must match the names used above. Panel selectorPanel = selectorPanel.add(new selectorPanel.add(new selectorPanel.add(new new Panel(); Button("Moe")); Button("Larry")); Button("Curly"));
// Put the row of buttons at the bottom part of the border layout add("South", selectorPanel); } public boolean action(Event evt, Object whichAction) { // If the action event is for a button, whichAction is the button's // label, which is also the name of a card in this program. We just // tell the card layout to show the appropriate card. if (evt.target instanceof Button) { cards.show(stoogePanel, (String) whichAction); return true; } return false; } }
Figure 7.4 shows the CardExample applet in action. The buttons along the bottom select the different card, which simply contain a single label. Figure 7.4 : A card layout enables you to display one of several panels. When you are creating dynamic forms, you can group sections of your forms onto different cards. You can create different methods for going from one card to the next, like having a master index of the different cards, or putting Next and Prev buttons on each card. If you want to disable a section of the form, don't make that section's card available. For example, suppose you have a part of the form for entering marriage information-date, place, witnesses, and so on. If a person is single, you don't want to present that part. You can remove it from the set of cards in your card layout.
Listing 7.8 Source Code for ImageRegion.java import java.awt.*; // ImageRegion is an abstract definition of the region // area supported by the ImageMap class. public abstract class ImageRegion extends Object { public ImageRegion() { }
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch7.htm (18 of 27) [8/14/02 10:52:50 PM]
// select is called when you click the mouse within a region public void select() { } // mouseEnter is called when the mouse enters a region public void mouseEnter() { } // mouseLeave is called when the mouse leaves a region public void mouseLeave() { } // getBoundingBox should return the smallest rectangle that // completely encloses this region. public abstract Rectangle getBoundingBox(); // inside returns true if x,y is within this region public abstract boolean inside(int x, int y); // paint is used to draw any hotspot popup information public void paint(Graphics g) { } }
Because the ImageRegion class is an abstract class, you need something concrete to actually implement a region. You will almost certainly need to define a rectangular region at some point. Actually, it is trivial to extend a rectangular region to be a polygon region. Listing 7.9 shows an implementation of ImageRegion that supports polygon regions.
Listing 7.9 Source Code for ImageRegionPoly.java import java.awt.*; // ImageRegionPoly implements a rectangular region for // use with the ImageMap class. public class ImageRegionPoly extends ImageRegion { Polygon boundary; public ImageRegionPoly() {
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch7.htm (19 of 27) [8/14/02 10:52:50 PM]
boundary = new Polygon(); } public ImageRegionPoly(Polygon p) { boundary = p; } public Rectangle getBoundingBox() { return boundary.getBoundingBox(); } public boolean inside(int x, int y) { return boundary.inside(x, y); } }
You may also have a need for a circular region. The ImageRegionCircle class in Listing 7.10 implements a circular region.
Listing 7.10 Source Code for ImageRegionCircle.java import java.awt.*; // ImageRegionCircle defines a circular region for use // with the ImageMap class. public class ImageRegionCircle extends ImageRegion { Point center; int radius; public ImageRegionCircle() { center = new Point(0, 0); radius = 0; } public ImageRegionCircle(Point center, int radius) { this.center = center; this.radius = radius; } public Rectangle getBoundingBox()
{ return new Rectangle(center.x - radius, center.y - radius, 2*radius, 2*radius); } // Use the distance formula to determine if a point is inside or not. // If the distance between x,y and the center of the region is <= the // radius of the circle, the point is within the region. public boolean inside(int x, int y) { int xd = center.x - x; int yd = center.y - y; int dist = (int) Math.sqrt(xd*xd+yd*yd); return dist <= radius; } }
Now that you have a method for defining a region in an image, you need a way to display an image, add these regions to it, and track the mouse to see when it hits a region. The ImageMap class in Listing 7.11 does just that. It also shows you how to define a canvas that displays an image.
Listing 7.11 Source Code for ImageMap.java import java.awt.*; import java.util.*; // // // // // // // The image map is a canvas that displays an image and supports hotspots. The hotspots are defined by subclasses of ImageRegion. There can only be one hotspot active at a time. Whenever a hotspot is active, its paint method is called so it can paint any popup information. You could display a little box of text saying what the hotspot does, for instance. The default paint method for a hotspot does nothing.
public class ImageMap extends Canvas { Image image; Vector regions; ImageRegion selectedRegion; boolean moved; public ImageMap(Image image) { this.image = image;
regions = new Vector(); moved = true; } // The size of the Canvas is defined by the size of the image. public Dimension minimumSize() { return new Dimension(image.getWidth(this), image.getHeight(this)); } public Dimension preferredSize() { return minimumSize(); } public Dimension size() { return minimumSize(); } public void addRegion(ImageRegion region) { regions.addElement(region); } public void removeRegion(ImageRegion region) { regions.removeElement(region); if (region == selectedRegion) { selectedRegion = null; } } // To repaint this canvas, redraw the image. Then, if there is a hotspot // active, call that hotspot's paint method. public void paint(Graphics g) { // Draw the image g.drawImage(image, 0, 0, this); if (selectedRegion != null) { // Find the bounding box for the current region (hotspot) Rectangle r = selectedRegion.getBoundingBox(); // Create a graphics context for the bounding box Graphics regionGraphics = g.create(r.x, r.y, r.width, r.height); // Let the region paint its little area
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch7.htm (22 of 27) [8/14/02 10:52:50 PM]
The next section of the ImageMap class demonstrates a very important concept in object-oriented design. The ImageMap class implements a framework that allows you to plug in different ImageRegion objects. You can add many new types of ImageRegion objects without changing the ImageMap class itself. It is very important to correctly assign class responsibilities in your design. In this case, the ImageMap class is responsible for displaying the master image, or background image. It is also responsible for tracking mouse movements and passing them on to affected regions. The ImageRegion class is responsible for displaying itself on the map if necessary, and for responding to a mouse click. Tip When designing classes for an application, you want to be able to add functionality by adding new classes, and not by changing existing classes. Try to identify things that may change and let those things be implemented by a separate class.
Listing 7.11 Source Code for ImageMap.java (continued) // Need to watch the mouse movement to see if the mouse hits // a hotspot or not. public boolean mouseMove(Event evt, int x, int y) { moved = true; // kludge to handle mouse-click problem // Quick shortcut here, see if you're still in the current region if ((selectedRegion != null) && selectedRegion.inside(x, y)) { return true; } // If there's a current region and you're not in it, tell the old // region that the mouse left it. if (selectedRegion != null) { selectedRegion.mouseLeave(); selectedRegion = null; }
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch7.htm (23 of 27) [8/14/02 10:52:50 PM]
// Check all the regions to see if the mouse is within any of them. // If two overlap, it's on a first come, first served basis - that is, // the first region that was added has priority. Enumeration e = regions.elements(); while (e.hasMoreElements()) { ImageRegion r = (ImageRegion) e.nextElement(); // See if the mouse's x,y is within the region's area if (r.inside(x, y)) { selectedRegion = r; r.mouseEnter(); break; } } repaint(); return true; } // Mouse down handles mouse clicks, and also will keep track // of mouse movement public boolean mouseDown(Event evt, int x, int y) { // The moved flag is a kludge. Sometimes you'll get more than // one mouse click. Assume that if the mouse doesn't move // between clicks, the user doesn't want more than one click. if (!moved) return true; moved = false; // Quick shortcut here if ((selectedRegion != null) && selectedRegion.inside(x, y)) { selectedRegion.select(); return true; } if (selectedRegion != null) { selectedRegion.mouseLeave(); selectedRegion = null; } Enumeration e = regions.elements(); while (e.hasMoreElements()) { ImageRegion r = (ImageRegion) e.nextElement(); if (r.inside(x, y)) { selectedRegion = r; r.mouseEnter(); r.select();
You may have noticed that the implementations of the image regions were incredibly small and didn't really seem to do anything. You are correct on both counts. To get any benefit out of the regions, you have to create subclasses that actually do something. Suppose you want to create a map that has a set of circular hotspots that light up with the name of the city in that section of the map. You need to keep track of the name of the city and also implement a paint method that displays the city name. Because an image map isn't very useful if you can't select items, your city hotspot should also do something when you click it. Listing 7.12 shows a circular region that represents a city. When you click the region, it pops up an OK dialog box telling you which city you clicked.
Listing 7.12 Source Code for CityRegion.java import java.awt.*; // // // // // This class implements a special version of the ImageRegionCircle class to represent cities on a map. When the mouse gets within range of a city, the city name is displayed. When you click the city, it pops up a dialog box telling you what city you clicked.
public class CityRegion extends ImageRegionCircle { String name; public CityRegion() { } // You can specify either x,y to create a CityRegion or a Point public CityRegion(String name, int x, int y) { // Set up the region as a circle with a radius of 30 pixels super(new Point(x, y), 30); this.name = name; } // radius of 30
public CityRegion(String name, Point p) { // Set up the region as a circle with a radius of 30 pixels super(p, 30); // radius of 30 this.name = name; } // // // // // // Paint is called when the mouse is within this city's bounding area - for this class, defined as a circle of radius 30 We just draw the city's name in blue. Note that the graphics area is bounded by the bounding box for the region (actually, the smallest rectangle that will enclose the area because the regions can be non-rectangular). public void paint(Graphics g) { g.setColor(Color.blue); g.drawString(name, 0, 35); } // If you click a city, you'll get a dialog box public void select() { OKDialog.createOKDialog("You selected the city of "+name); } }
Now that all the pieces of the puzzle are in, you can create an the image map for displaying these cities. Listing 7.13 shows the CityApplet class.
Listing 7.13 Source Code for CityApplet.java import java.awt.*; import java.applet.*; // // // // // This applet demonstrates the use of the ImageMap class It loads a map of the U.S.A. and creates a set of regions for the map. The regions are implemented in the CityRegion class. The numbers for the city coordinates are approximate, and were determined through ocular analysis (I eyeballed the map).
// Load the map image Image usaImage = getImage(getDocumentBase(), "usa.gif"); // Be naughty and use the MediaTracker to make sure the map is loaded MediaTracker mt = new MediaTracker(this); mt.addImage(usaImage, 0); try { mt.waitForAll(); } catch (Exception ignore) { } // Create an image map object for the image ImageMap imageMap = new ImageMap(usaImage); // Add city regions to the image imageMap.addRegion(new imageMap.addRegion(new imageMap.addRegion(new imageMap.addRegion(new imageMap.addRegion(new imageMap.addRegion(new imageMap.addRegion(new imageMap.addRegion(new imageMap.addRegion(new map CityRegion("Atlanta", 323, 202)); CityRegion("New York", 377, 118)); CityRegion("L.A.", 45, 196)); CityRegion("San Fran", 34, 164)); CityRegion("Seattle", 52, 74)); CityRegion("Dallas", 218, 236)); CityRegion("Chicago", 277, 123)); CityRegion("Miami", 367, 270)); CityRegion("Denver", 102, 143));
Figure 7.5 shows the output from this applet. Figure 7.5 : Image maps in Java can implement hot spots.
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f7-1.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f7-2.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f7-3.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f7-4.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f7-5.gif
CONTENTS
G G
G G
Applets and Files Using the JFS Filesystem for Applets H Printing Files Using JFS H Accessing Other Web Servers from JFS Saving Files Using HTTP Post Storing and Retrieving Files with FTP H Sending FTP Commands H Establishing an FTP Session H Sending Simple FTP Commands H Establishing a Data Connection
directories, print files, and open URLs and socket connections to hosts other than the ones the applets came from. JFS isn't some sort of cheap hack around Java's security model; it is a full-featured server system. This means, of course, that you have to run the JFS server, which is written in Java, to use JFS. The JFSclient class is the applet's interface to the JFS server. You create an instance of JFSclient by passing the host name of the JFS server to the constructor. If you are doing this from an applet, the host must be the host that the applet was loaded from, which means your Web server has to run JFS. Once you create a JFSclient, you must send a user name and password to the JFS server. JFS has its own set of user names and passwords; these are not the operating system's user names. This is quite important because the applet must contain the user name and password in order to perform the logon. Anyone with evil intentions and a little patience can find out the user name and password that the applet sends. If these were logon IDs for your Web server, it would be simple for someone to log on to your Web server and wreak all kinds of havoc. If you forget to send the authentication information, the other methods in the JFSclient will simply hang, which may not be quite the result you were looking for. Listing 8.1 shows a very simple example that retrieves a file stored in the JFS file system.
Listing 8.1 Source Code for JFSGet.java import java.applet.*; // This program demonstrates the use of the JFSclient // class to fetch a file. public class JFSGet extends Object { public static void main(String args[]) { try { // Create a JFS client to host 192.0.0.3 JFSclient jfs = new JFSclient("192.0.0.3"); // Log on as root, with no password jfs.auth("root", ""); // Fetch the file called "volcano" byte[] volcfile = jfs.get("/home/root/volcano", 0); // Dump it to the screen for (int i=0; i < volcfile.length; i++) { System.out.print((char)volcfile[i]); } System.out.println(); } catch (Exception e) { e.printStackTrace(); } } }
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch8.htm (2 of 16) [8/14/02 10:53:03 PM]
The JFSGet program is very straightforward. It creates a JFSclient object that is connected to a JFS server whose IP address is 192.0.0.3, then it authenticates itself using the name root with no password. Next, it uses the get method to retrieve a file as an array of bytes. Finally, it loops through the array of bytes and prints them to the System.out. Caution This example does not use an authentication password. In practice, you should always use a password in JFS authentication. Otherwise, you may open your system up to possible corruption from other people on the Internet.
JFSclient myClient = new JFSClient(getDocumentBase().getHost()); Message deviceInfo = new Message(); deviceInfo.add("URL", "http://www.mcp.com"); Message response = myClient.devget("/dev/Web", deviceInfo); byte[] responseData = response.getdata();
Listing 8.2 Source Code for PostPutFile.java import java.net.*; import java.io.*; // // // // This class provides a static method to post a file to the putfile script, which takes a filename as a parameter passed in the POST request itself, and then receives the bytes as the posted data.
// // // // //
Put sends the named file to a specific URL. The URL should contain the path name of the putfile script. This method will append the ?filename to the script name. It returns 0 if the put was successful, or a non-zero number if it failed for some reason.
public static int put(URL url, String filename, byte[] bytes) throws IOException, MalformedURLException { // Run the putfile script and ask it to store the data in a file called "putme" URL destURL = new URL(url.getProtocol(), url.getHost(), url.getPort(), url.getFile()+"?"+filename); // Define the data that you want stored in the file. URLConnection urlConn = destURL.openConnection(); urlConn.setDoOutput(true); // we need to write urlConn.setDoInput(true); // just to be safe... urlConn.setUseCaches(false); // get info fresh from server // Tell the server what kind of data we are sending - in this case, // just a stream of bytes. urlConn.setRequestProperty("Content-type", "application/octet-stream"); // Must tell the server the size of the data we are sending. This also // tells the URLConnection class that we are doing a POST instead // of a GET. urlConn.setRequestProperty("Content-length", ""+bytes.length); // Open an output stream so we can send the info we are posting OutputStream outStream = urlConn.getOutputStream(); // Write out the actual request data outStream.write(bytes); outStream.close(); // Now that we have sent the data, open up an input stream and get // the response back from the server DataInputStream inStream = new DataInputStream( urlConn.getInputStream()); String line = inStream.readLine(); inStream.close();
try { int result = Integer.valueOf(line).intValue(); return result; } catch (Exception parseError) { return -1; } } }
Listing 8.3 shows a simple example applet that stores a file using the PostPutFile class.
Listing 8.3 Source Code for TestPutFile.java import java.net.*; import java.applet.*; public class TestPutFile extends Applet { public void init() { try { URL destURL = new URL(getDocumentBase(), "/cgi-bin/putfile"); // Define a string we want to send String dataToSend = "This is a string that I want \n"+ "to store in the file.\n"; // The PostPutFile class wants a byte array, however, so we convert // the string to a byte array. byte[] bytes = new byte[dataToSend.length()]; dataToSend.getBytes(0, dataToSend.length(), bytes, 0); PostPutFile.put(destURL, "/home/mark/putme", bytes); } catch (Exception e) { e.printStackTrace(); } } }
Note
The new version of HTTP (HTTP 1.1) includes a PUT command that allows you to store a file without creating a separate CGI program to save the file. Some HTTP servers already support this new option. If you have a server that supports PUT and you want to save files from Java, you won't be able to use the URL class to send the file (until the URL class supports POST). You can, however, use the PostSockURL class from Chapter 6 "Communicating with a Web Server," with a little modification (change POST to PUT when it sends the HTTP command).
G G
1xx means that the command has been started successfully, and is in the process of running. You will get another response when the command completes. When the server starts transmitting a file it sends a 1xx response, and then sends a 2xx response when the file has been sent. 2xx indicates that the command has been completed successfully. 3xx is sent when the server accepts your command, but needs more information from you in order to proceed. This often occurs when you send a USER command and the system wants you to send a password. 4xx means that the command cannot be completed due to some temporary problem. If you send the same command again, it may be accepted. 5xx indicates that the command cannot be completed. If you try the same command again, it will be rejected again.
FTP responses can span more than one line. Whenever the server sends a multiline response, each line begins with the
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch8.htm (7 of 16) [8/14/02 10:53:03 PM]
response code followed by a dash (-). The last line of the response does not contain a dash. All you have to do when reading responses is look for a dash as the fourth character. If there is a dash, you need to read another line. Listing 8.4 shows the doCommand and getResponse methods from the FTPSession class which are included on the CD for this book. These methods are responsible for sending commands and receiving responses. The getResponse method checks for a dash to see if the response is a multiline response. You could use these same methods for other Internet protocols that use this same request-response format, like SMTP. Note The FTPSession class uses DataInputStream and DataOutputStream filters on top of the normal socket input and output streams. This allows FTPSession to send and receive whole lines of data rather than reading and writing one character at a time.
Listing 8.4 doCommand and getResponse Methods from FTPSession.java // Send a command and wait for a response public String doCommand(String commandString) throws IOException { outStream.writeBytes(commandString+"\n"); String response = getResponse(); return response; } // Get a response back from the server. Handles multi-line responses // and returns them as part of the string. public String getResponse() throws IOException { String response = ""; for (;;) { String line = inStream.readLine(); if (line == null) { throw new IOException( "Bad response from server."); } // FTP response lines should at the very least have a 3-digit number if (line.length() < 3) { throw new IOException( "Bad response from server."); } response += line + "\n";
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch8.htm (8 of 16) [8/14/02 10:53:03 PM]
// If there isn't a '-' immediately after the number, we've gotten the // complete response. ('-' is the continuation character for FTP responses) if ((line.length() == 3) || (line.charAt(3) != '-')) return response; } }
Tip If you already had a class that implemented the SMTP protocol, you might consider moving the methods for sending and receiving commands into a new superclass for the SMTP class. Then the FTP and SMTP classes would be subclasses of this new class. This kind of situation occurs often in object-oriented programming. You discover that there are parts of a class that can be used by other classes, so you split out the reusable parts into a separate class. Obviously, it would have been better if you could have anticipated that the parts would need to be reused, but you don't always realize these things ahead of time.
You would then be required to send a password with the PASS command: PASS password If mark's password is Shh!!!!!, the appropriate PASS command is: PASS Shh!!!!! The response to the PASS command is usually something like this: 230 User mark logged in. The FTP protocol allows for a third login parameter called the account, which is sent using the ACCT command. If you get a response with a response code of 332 (need account for login) after sending the PASS command, you need to send an ACCT command: ACCT account The account parameter is rarely used on UNIX systems, and is not restricted to the login sequence. You could receive a 332 response code for any operation, meaning that you must supply an account parameter when performing that operation. For instance, your server may password-protect files, and could require you to send the password to a file with the ACCT command before you can retrieve the file.
A simple command is one that does not require a data connection. Some FTP commands require you to set up a second connection, either to send raw data to the server, or receive raw data from the server. Table 8.2 shows you the commands that require a data connection.
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch8.htm (10 of 16) [8/14/02 10:53:03 PM]
Table 8.2 FTP Commands that Require a Data Connection Command LIST LIST directory RETR filename STOR filename Function Gets a list of all the files in the current directory Gets a list of all the files in a specific directory Retrieves a file from the FTP server Sends a file to the FTP server
227 Entering Passive Mode (127,0,0,1,6,114) Once the server returns the response, the client can establish the data connection. Figure 8.3 shows the typical interaction sequence between an FTP server and a client performing a STOR command, using PASV to set up the data connection. Figure 8.3 : The PASV command forces the server to create the listen socket for the data connection. Listing 8.5 shows the doPasvPort method from the FTPSession class. It sends a PASV command, parses the response, and then establishes a socket connection with the server.
Listing 8.5 doPasvPort Method from FTPSession.java protected synchronized Socket doPasvPort() throws IOException { // Send the PASV command String response = doCommand("PASV"); // If it wasn't in the 200s, there was an error if (response.charAt(0) != '2') { throw new IOException(response); } // The pasv response looks like: // 227 Entering Passive Mode (127,0,0,1,4,160) // We'll look for the ()'s at the end first int parenStart = response.lastIndexOf('('); int parenEnd = response.lastIndexOf(')'); // Make sure they're both there and that the ) comes after the ( if ((parenStart < 0) || (parenEnd < 0) || (parenStart >= parenEnd)) { throw new IOException("PASV response format error"); } // Extract the address bytes String pasvAddr = response.substring(parenStart+1, parenEnd); // Create a tokenizer to parse the bytes StringTokenizer tokenizer = new StringTokenizer(pasvAddr, ","); // Create the array to store the bytes int[] addrValues = new int[6]; // Parse each byte for (int i=0; (i < 6) && tokenizer.hasMoreTokens(); i++) { try {
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch8.htm (12 of 16) [8/14/02 10:53:03 PM]
addrValues[i] = Integer.valueOf( tokenizer.nextToken()).intValue(); } catch (Exception e) { throw new IOException( "PASV response format error"); } } // We ignore the host addresses, assuming that the host address is // the same as the host address we used to connect the first time. Socket newSock = new Socket(host, (addrValues[4] << 8) + addrValues[5]); return newSock; }
Listing 8.6 shows the put method from the FTPSession class. It uses the doPasvPort command to set up a data connection, then sends a STOR command to the FTP server. The STOR command should return a response code in the 100199 range, indicating that the STOR may proceed. When you finish sending the file to the FTP server, you must close down the data connection. This tells the server that you have finished. You should then receive another response from the server over the command connection, which should have a response code in the 200-299 range.
Listing 8.6 put Method from FTPSession.java public synchronized void put(String remoteFile, byte[] data, boolean doBinary) throws IOException { // If transferring in binary mode, send a type command for type I (IMAGE) if (doBinary) { String response = doCommand("TYPE I"); if (response.charAt(0) != '2') { throw new IOException(response); } // If transferring in ASCII mode, send a type command for type A (ASCII) } else { String response = doCommand("TYPE A"); if (response.charAt(0) != '2') { throw new IOException(response); } } // Open up a data connection Socket putSock = doPasvPort();
// Tell the server where we want it to store the data we are sending String response = doCommand("STOR "+remoteFile); // If the request is successful, the server should send a response // in the 100s and then start receiving the bytes. Once the data // connection is closed, it should send a response in the 200s. if (response.charAt(0) != '1') { putSock.close(); throw new IOException(response); } // If binary mode, just write all the bytes if (doBinary) { OutputStream out = putSock.getOutputStream(); out.write(data); // If ASCII mode, write the data a line at a time } else { DataInputStream in = new DataInputStream( new ByteArrayInputStream(data)); DataOutputStream out = new DataOutputStream( putSock.getOutputStream()); String line; while ((line = in.readLine()) != null) { out.writeBytes(line+"\r"); } } putSock.close(); response = getResponse(); // Make sure we got a 200 response if (response.charAt(0) != '2') { throw new IOException(response); } }
The FTPSession class is quite simple to use. You just create an instance of FTPSession by passing the destination host name, the user name, and the password to the constructor, and then using the get and put methods to retrieve and send files, respectively. Listing 8.7 shows an example applet that copies a file by retrieving it and then storing it under a new name.
Listing 8.7 Source Code for TryFTP.java import java.applet.*; import java.io.*; // This applet demonstrates the use of the FTPSession class. // It copies a file called "volcano" to a file called "vol.ftp" // by fetching the file and then storing it with a new name. public class TryFTP extends Applet { public void init() { try { // Create the session to host 192.0.0.3, using a user name of anonymous // and a password of mark@localhost FTPSession sess = new FTPSession( "192.0.0.3", "anonymous", "mark@localhost"); // Fetch the file byte[] file = sess.get("/home/mark/volcano", true); // Store the file sess.put("/home/mark/vol.ftp", file, true); } catch (Exception e) { e.printStackTrace(); } } }
Caution Be extremely careful when using the FTPSession class with respect to the user name and password. Even though your applet is compiled, it is fairly trivial to look through the code and find the user name and password that are sent. You should either use anonymous FTP, or set up a user account that is not allowed to log on to your system and is allowed to use only FTP. Otherwise, you are broadcasting a free user account all over the Internet whenever you put your applet out there.
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f8-1.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f8-2.gif
CONTENTS
G G
Reusable Components The Command Pattern H Invoking Commands from a Menu Creating a Reusable Image Button H Setting the Size of a Canvas H Handling Input Events H Painting the Canvas H Watching for Image Updates H Creating a CommandImageButton Using the Observer Interface H The Model-View-Controller Paradigm H Observables and the Model-View-Controller Paradigm Using Observables for Other Classes
Reusable Components
Amid all the excitement and debate over Java's crossplatform abilities, Java's features as an object-oriented programming language tend to get lost. Java falls somewhere between C++ and Smalltalk on the object-oriented scale. The general structure of Java's classes are similar to C++, but it adds a few more capabilities that are closer to Smalltalk. For example, the interface mechanism allows you to invoke methods in an object without knowing the object's class hierarchy. In C++, you must have a reference to the object's class or one of its superclasses to invoke a method. Smalltalk, of course, allows you the ultimate freedom of invoking any method in any object. This gives Smalltalk GUI designers a huge advantage in creating reusable components. In Smalltalk, when you add a button to your application, you can tell the button to invoke a specific method in a specific object whenever the button is pressed. If you were allowed to do this in Java, it would look something like this: public class MyClass extends Applet { public void handleButtonPress() { // code to handle a button being pressed
} public void init() { Button myButton = new Button("Press Me", this, handleButtonPress); } } Unfortunately, you can't do this in Java because it doesn't support pointers or references to functions. You can't even do it effectively in C++, and it supports function pointers. This problem is solved in C++ by something called a functor, also known as the Command pattern in design pattern lingo.
Listing 9.1 Source Code for Command.java public interface Command { public void doCommand(); }
Now, to be able to use this interface with a button, you need a subclass of Button that invokes doCommand whenever the button is pressed. Listing 9.2 shows an implementation of a CommandButton object that does this.
Listing 9.2 Source Code for CommandButton.java import java.awt.Button; import java.awt.Event; // This class implements a Button that supports the // Command interface. When the button is pressed, it // invokes the doCommand method in the Command interface. public class CommandButton extends Button
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch9.htm (2 of 28) [8/14/02 10:53:12 PM]
{ // The interface where we will invoke doCommand protected Command buttonCommand; // It's always polite to implement the empty constructor if // you can get away with it. public CommandButton() { } // Allow a CommandButton with a command but no label public CommandButton(Command command) { buttonCommand = command; } // Allow a CommandButton to use the typical Button constructor public CommandButton(String label) { super(label); } // The most useful constructor allows a label and a command public CommandButton(String label, Command command) { super(label); buttonCommand = command; } // When we get an action event, invoke doCommand in buttonCommand public boolean action(Event evt, Object which) { // Make sure the action event is for this object if (evt.target != this) return false; // Make sure we have a buttonCommand defined! if (buttonCommand == null) return false; buttonCommand.doCommand(); return true; } // Since you can create a CommandButton without passing it a // Command interface, you need to be able to set the command later.
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch9.htm (3 of 28) [8/14/02 10:53:12 PM]
Now, suppose you want to pass parameters when the command is invoked. For example, suppose you want to assign numbers to buttons and pass the number as part of the command. You do not need to change the Command interface or the CommandButton class for this. The key to the Command pattern is the creation of small command objects that invoke the real methods. Listing 9.3 shows a small command object implementation whose doCommand method turns around and invokes changeNumber in a NumberApplet object.
Listing 9.3 Source Code for ChangeNumberCommand.java // This class implements a simple command object // that invokes a method called changeNumber in a // NumberApplet object whenever doCommand is called public class ChangeNumberCommand extends Object implements Command { protected NumberApplet applet; protected int number; public ChangeNumberCommand(NumberApplet applet, int number) { this.applet = applet; this.number = number; } public void doCommand() { applet.changeNumber(number); } }
The ChangeNumberCommand object illustrates the key feature of the Command pattern.It acts as an intermediary between the CommandButton and the NumberApplet. The CommandButton says doCommand(), the NumberApplet wants to hear changeNumber(5), the ChangeNumberCommand object performs the translation. Listing 9.4 shows the implementation of the NumberApplet class.
import java.applet.Applet; import java.awt.Label; // // // // // This applet displays a label containing a number, followed by three buttons which change the number. It uses the ChangeNumberCommand to translate the doCommand method in the CommandButton to the changeNumber method in this object.
public class NumberApplet extends Applet { Label number; public void init() { // Start the label out at 0 number = new Label("0"); add(number); // Create the object to change the label to 1 add(new CommandButton("1", new ChangeNumberCommand(this, 1))); // Create the object to change the label to 2 add(new CommandButton("2", new ChangeNumberCommand(this, 2))); // Create the object to change the label to 3 add(new CommandButton("3", new ChangeNumberCommand(this, 3))); } // changeNumber actually performs the change public void changeNumber(int newNumber) { number.setText(""+newNumber); } }
Figure 9.1 shows the NumberApplet applet in action. Figure 9.1 : The Command pattern makes it easy to use components without subclassing them. You can also cascade commands, creating command objects that translate from one command into another. For example, the ChangeNumberCommand object invokes a changeNumber method in a NumberApplet that takes a number. It is a shame that the object is restricted to NumberApplets. You will probably have many situations where you want to do a numeric command-a command that takes a number. You should go ahead and define a NumberCommand interface to handle such situations, as shown in Listing 9.5:
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch9.htm (5 of 28) [8/14/02 10:53:12 PM]
Listing 9.5 Source Code for NumberCommand.java public interface NumberCommand { public void doCommand(int number); }
Now, because you have already seen an example where you want to take an object that invokes a regular Command and turn that into a NumberCommand, you can feel pretty confident that an object that converts from one to the other will get a lot of use. Listing 9.6 shows the class to do this.
Listing 9.6 Source Code for CommandToNumberCommand.java // This class translates a Command.doCommand() method into // a NumberCommand.doCommand(int) method. public class CommandToNumberCommand extends Object implements Command { protected NumberCommand numberCommand; protected int number; public CommandToNumberCommand(NumberCommand command, int number) { this.numberCommand = command; this.number = number; } public void doCommand() { numberCommand.doCommand(number); } }
You will soon end up with a library of commands and command conversions, such as StringCommand, StringCommandToNumberCommand, BooleanCommandToStringCommand, etc. You'll also have new components that invoke these commands, like CommandTextField, which would invoke a doCommand method in a StringCommand object. These objects take only minutes to write, but they can save you hours of coding. Figure 9.2 illustrates how some of these objects might be connected. Figure 9.2 : Different command objects can be linked together like building blocks.
Listing 9.7 Source Code for CommandMenuItem.java import java.awt.*; // This is a menu item that supports the command // interface. Whenever an ACTION_EVENT is posted // to it, it invokes the doCommand method. public class CommandMenuItem extends MenuItem { // The Command interface to invoke protected Command whichCommand; public CommandMenuItem(String label, Command whichCommand) { super(label); this.whichCommand = whichCommand; } public boolean postEvent(Event evt) { // If we get an ACTION_EVENT event, call doCommand if (evt.id == Event.ACTION_EVENT) { whichCommand.doCommand(); return true; } // Otherwise, let the super class handle the postEvent return super.postEvent(evt); } }
The AWT Button class does not allow you to display an image in the button, only text. This is really annoying for people who would like to make toolbars and other useful GUI components. You can implement an image button pretty easily by creating a subclass of Canvas. When creating a custom component using the Canvas class, there are basically two things you have to do-draw the component and handle input events. For an image button, you also have the added burden of waiting for the image to be downloaded.
The ImageButton class computes its preferred size from the size of the image. The minimum size is the size of the button when there is no image (4x4, which leaves room for the shadowing effects). The ImageButtonClass also implements its own size method. The size method is used when the button size is fixed. In other words, you can specify a fixed size for the button that will never be changed by the layout manager. Listing 9.8 shows the sizing methods for the ImageButton class.
Listing 9.8 Image Sizing Methods from ImageButton.java // The minimum size is the amount of space for the shading around the // edges, plus one pixel for the image itself. public Dimension minimumSize() { return new Dimension(4, 4); } // We'd prefer to have just enough space for the shading (shading takes // 3 pixels in each direction) plus the size of the image. public Dimension preferredSize() { return new Dimension(buttonImage.getWidth(this)+3, buttonImage.getHeight(this)+3); }
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch9.htm (8 of 28) [8/14/02 10:53:12 PM]
public Dimension size() { // If the sized isn't fixed, just say super-size it! (har har) if (!fixedSize) return super.size(); return preferredSize(); }
Caution If an image hasn't been downloaded yet, the getWidth and getHeight methods will return -1. This can cause unpleasant exceptions if you use these values to set the width or height of a canvas. The ImageButton class does not have that problem because it leaves space on either size of the image. If the image hasn't been downloaded, the preferredSize method will return a dimension of 22. If it hadn't added 3 to both width and height, it would be returning a dimension of -1-1, which would surely cause an error.
Listing 9.9 Mouse Handling Methods from ImageButton.java // If we get a mouse click, make a note it and push the button down public boolean mouseDown(Event evt, int x, int y) { isDown = true; repaint(); return true; } // If we get mouseUp, see if we thought the mouse was down. If so, // the button has been clicked. Generate an action event so this button // behaves just like a real button. public boolean mouseUp(Event evt, int x, int y) { if (isDown) { Event newEvt = new Event(this, evt.when, Event.ACTION_EVENT, x, y, 0, 0, buttonImage); this.postEvent(newEvt); } isDown = false; repaint(); return true; } // If the mouse leaves the area, move the button up. public boolean mouseExit(Event evt, int x, int y) { if (isDown) { isDown = false; repaint(); } return true; }
light on its upper left side, and the lower left side would be in shadow. Anything that is lowered (going inside the screen, away from you) would catch light on the lower-right corner, and the upper-left would be in shade. Also, when a button is lowered, the image or text in the image is usually shifted a small amount to the right and down. Figure 9.3 shows the image button in the raised position. Notice that the top and left edges are lighter, while the bottom and right edges are darker. Figure 9.3 : By lightening the upper-left side of an image, and darkening the lower-right, you create a raised effect. Figure 9.4 shows the image button in the lowered position. Notice that the top and left edges are now darker and the bottom and right edges are lighter. Figure 9.4 : By darkening the upper-left side of an image, and lightening the lower-right, you create a lowered effect. Listing 9.10 shows the paint method for the ImageButton class. It draws the shading for the upper-left part of the button, then draws the image before adding the shading for the lower-right.
Listing 9.10 paint Method from ImageButton.java // paint displays the shading and the image public void paint(Graphics g) { Dimension currSize = size(); int width = currSize.width; int height = currSize.height; int imgHeight = buttonImage.getHeight(this); int imgWidth = buttonImage.getWidth(this); // Display the shading in the upper left. If the button is up, the // upperleft shading is white, otherwise it's black if (isDown) { g.setColor(Color.black); } else { g.setColor(Color.white); } g.drawLine(0, 0, width-1, 0); g.drawLine(0, 0, 0, height-1); // If the button is up, we draw the image starting at 1,1 int imgX = 1; int imgY = 1; // If the button is down, move the image right and down one pixel and // draw gray shading at 1,1
if (isDown) { g.setColor(Color.gray); g.drawLine(1, 1, width-2, 1); g.drawLine(1, 1, 1, height-2); imgX++; imgY++; // If the button is up, draw gray shading just inside the bottom right shading } else { g.setColor(Color.gray); g.drawLine(1, height-2, width-1, height-2); g.drawLine(width-2, 1, width-2, height-2); } // Compare the width of the button to the width of the image, if // the button is wider, move the image over to make sure it's centered. int xDiff = (width - 3 - imgWidth) / 2; if (xDiff > 0) imgX += xDiff; // Compare the height of the button to the height of the image, if // the button is taller, move the image down to make sure it's centered. int yDiff = (height - 3 - imgHeight) / 2; if (yDiff > 0) imgY += yDiff; g.drawImage(buttonImage, imgX, imgY, this); // Draw the bottom right shading. If the button is up, the shading is // black, otherwise it's white. if (isDown) { g.setColor(Color.white); } else { g.setColor(Color.black); } g.drawLine(1, height-1, width-1, height-1); g.drawLine(width-1, 1, width-1, height-1); }
redraw itself with the completed image. Listing 9.11 shows the imageUpdate method for the ImageButton class.
Listing 9.11 imageUpdate Method from ImageButton.java public boolean imageUpdate(Image img, int flags, int x, int y, int width, int height) { // If we have a complete image, resize the button and ask the parent // to recompute all the component positions. Good thing this only // gets called once! if ((flags & ImageObserver.ALLBITS) != 0) { resize(img.getWidth(this)+3, img.getHeight(this)+3); getParent().validate(); } // Let the canvas class handle any other information it was looking for return super.imageUpdate(img, flags, x, y, width, height); }
Tip Any time a component resizes itself, it should call the validate method in the parent container. This causes the container to reposition its components based on the updated size.
Creating a CommandImageButton
Now that you have created your own custom component, you can see how easily you can fit it into the Command interface scheme. You only need to create a subclass of ImageButton called CommandImageButton. Listing 9.12 shows you how to do this.
Listing 9.12 Source Code for CommandImageButton.java import java.awt.Event; import java.awt.Image; // This class implements a ImageButton that supports the // Command interface. When the button is pressed, it // invokes the doCommand method in the Command interface. public class CommandImageButton extends ImageButton
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch9.htm (13 of 28) [8/14/02 10:53:12 PM]
{ // The interface where we will invoke doCommand protected Command buttonCommand; // Allow a CommandButton to use the typical ImageButton constructor public CommandImageButton(Image image) { super(image); } // Allow a CommandButton to use the typical ImageButton constructor public CommandImageButton(Image image, boolean fixedSize) { super(image, fixedSize); } // The most useful constructor allows an Image and a command public CommandImageButton(Image image, Command command) { super(image); buttonCommand = command; } // The most useful constructor allows an Image and a command public CommandImageButton(Image image, boolean fixedSize, Command command) { super(image, fixedSize); buttonCommand = command; } // When we get an action event, invoke doCommand in buttonCommand public boolean action(Event evt, Object which) { // Make sure the action event is for this object if (evt.target != this) return false; // Make sure we have a buttonCommand defined! if (buttonCommand == null) return false; buttonCommand.doCommand(); return true; }
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch9.htm (14 of 28) [8/14/02 10:53:12 PM]
// Since you can create a CommandImageButton without passing it a // Command interface, you need to be able to set the command later. public void setCommand(Command command) { buttonCommand = command; } }
Now you can use the CommandImageButton in all your applications where you were using the CommandButton. The number button application earlier in this chapter needs only a few changes. Listing 9.13 shows a version of the number applet that uses image buttons instead of regular buttons and also uses the CommandToNumberCommand object to convert from a Command to a NumberCommand interface.
Listing 9.13 Source Code for NumberApplet2.java import java.applet.Applet; import java.awt.Label; import java.awt.Color; // // // // // This applet displays a label containing a number, followed by three buttons which change the number. It uses the ChangeNumberCommand to translate the doCommand method in the CommandButton to the changeNumber method in this object.
public class NumberApplet2 extends Applet implements NumberCommand { Label number; public void init() { setBackground(Color.gray); // Start the label out at 0 number = new Label("0"); add(number); // Create the object to change the label to 1 add(new CommandImageButton( getImage(getDocumentBase(), "one.gif"), new CommandToNumberCommand(this, 1))); // Create the object to change the label to 2 add(new CommandImageButton( getImage(getDocumentBase(), "two.gif"),
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch9.htm (15 of 28) [8/14/02 10:53:12 PM]
new CommandToNumberCommand(this, 2))); // Create the object to change the label to 3 add(new CommandImageButton( getImage(getDocumentBase(), "three.gif"), new CommandToNumberCommand(this, 3))); } // changeNumber actually performs the change public void doCommand(int newNumber) { number.setText(""+newNumber); } }
Figure 9.5 shows the NumberApplet2 applet in action. Figure 9.5 : It is much easier to integrate new components when you use the Command pattern.
from the modem and passing them to another routine that parses the information out of the message. It is very easy to think of the code that reads the messages as the controller, and put the parsing mechanism in the model. It is also wrong. The parsing routine is also part of the controller. The model should have absolutely no dependence on the external representation of information. This is an extremely important point, because it greatly affects the reusability of your code. You should be able to change input sources and change output formats without touching the model. In other words, the model deals with pure information that has no external meaning attached to it. Figure 9.6 shows the conceptual relationship between the model, the view, and the controller. It also shows how the aircraft tracking system fits into this model. Figure 9.6 : The model-view-controller paradigm is a good, object-oriented way of designing applications.
ObservableInt - an integer Observable This class implements the Observable mechanism for a simple int variable. You can set the value with setValue(int) and int getValue() returns the current value.
public class ObservableInt extends Observable { int value; // The value everyone wants to observe public ObservableInt() { value = 0; // By default, let value be 0 } public ObservableInt(int newValue) { value = newValue; // Allow value to be set when created
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch9.htm (17 of 28) [8/14/02 10:53:12 PM]
} public synchronized void setValue(int newValue) { // // Check to see that this call is REALLY changing the value // if (newValue != value) { value = newValue; // Mark this class as "changed" setChanged(); // Tell the observers about it, pass the new value as an Integer object // This saves the observers time because they don't have to ask what // the new value is. notifyObservers(new Integer(value)); } } public synchronized int getValue() { return value; } }
On the Observer side of things, you can create components that redisplay themselves when the Observable changes. Listing 9.15 shows an IntLabel class that observes an ObservableInt and displays its current value.
Listing 9.15 Source Code for IntLabel.java import java.awt.*; import java.util.*; // IntLabel - a Label that displays the value of // an ObservableInt. public class IntLabel extends Label implements Observer { private ObservableInt intValue; // The value we're observing public IntLabel(ObservableInt theInt) { intValue = theInt; // Tell intValue we're interested in it
intValue.addObserver(this); // Initialize the label to the current value of intValue setText(""+intValue.getValue()); } // Update will be called whenever intValue is changed, so just update // the label text. public void update(Observable obs, Object arg) { setText(((Integer) arg).toString()); } }
Listing 9.16 shows an IntTextField that allows you to change the value of an ObservableInt. It will also act as an Observer in case another object changes the value.
Listing 9.16 Source Code for IntTextField.java import java.awt.*; import java.util.*; // // // // // IntTextField - a TextField that reads in integer values and updates an Observable int with the new value. This class is both a "view" of the Observable int, since it displays its current value, and a "controller" since it updates the value.
public class IntTextField extends TextField implements Observer { private ObservableInt intValue; public IntTextField(ObservableInt theInt) { // Initialize the field to the current value, allow 3 input columns super(""+theInt.getValue(), 3); intValue = theInt; intValue.addObserver(this); // Express interest in value } // The action for the text field is called whenever someone presses "return" // We'll try to convert the string in the field to an integer, and if // successful, update the observable int.
public boolean action(Event evt, Object whatAction) { Integer intStr; // to be converted from a string try { // The conversion can throw an exception intStr = new Integer(getText());
// If we get here, there was no exception, update the observable intValue.setValue(intStr.intValue()); } catch (Exception oops) { // We just ignore the exception } return true; } // The update action is called whenever the observable int's value changes. // We just update the text in the field with the new int value public void update(Observable obs, Object arg) { setText(((Integer)arg).toString()); } }
Putting these objects together in a working applet is trivial. Listing 9.17 shows an applet that demonstrates these objects.
Listing 9.17 Source Code for ObservableApplet.java import java.applet.*; // This class demonstrates the ObservableInt, IntTextField // and IntLabel classes. public class ObservableApplet extends Applet { public void init() { ObservableInt intValue = new ObservableInt(0); add(new IntTextField(intValue)); add(new IntLabel(intValue)); } }
Listing 9.18 shows an example Observable class that represents an aircraft for a flight tracking system. Notice that none of its instance variables are public. You need to be able to notice when a variable changes value. If your instance variables are all public, any object can come along and change the value without you being notified. By restricting all the variable manipulation to accessor functions (get/set functions), you maintain the ability to notice when a variable changes. Tip When you call notifyObservers, you may pass it an object that will be passed to the update method in all observers. You can use this object to pass specific information about the update. If you plan to handle multiple types of updates, you should create an event class, similar to the AWT's Event class, which tells the update method what kind of update it is and which objects are involved.
Listing 9.18 Source Code for Aircraft.java import java.util.*; // // // // // // // // // This class demonstrates an observable object with multiple values that can change. If an individual value is changed, it calls notifyObservers. You can also change the values in bulk using setAll. When you create observable classes like this, you can't have public variables if you need to know when those varables change. Otherwise, if altitude was public, anyone could say: aircraft.altitude = 10000;
// and no observers would be notified. public class Aircraft extends Observable { protected String id; protected protected protected protected double double double double latitude; longitude; altitude; speed;
public Aircraft(String id) { this.id = id; } public Aircraft(String id, double lat, double lon, double alt, double speed) { this.id = id; this.latitude = lat; this.longitude = lon; this.altitude = alt; this.speed = speed; } public String getID() { return id; } public double getAltitude() { return altitude; } public void setAltitude(double newAlt) { altitude = newAlt; setChanged(); notifyObservers(this); } public double getLatitude() { return latitude; } public void setLatitude(double newLat) { latitude = newLat; setChanged(); notifyObservers(this); } public double getLongitude() { return longitude; } public void setLongitude(double newLon) { longitude = newLon; setChanged(); notifyObservers(this);
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch9.htm (22 of 28) [8/14/02 10:53:13 PM]
} public double getSpeed() { return speed; } public void setSpeed(double newSpeed) { speed = newSpeed; setChanged(); notifyObservers(this); } public void setAll(double lat, double lon, double alt, double speed) { this.latitude = lat; this.longitude = lon; this.altitude = alt; this.speed = speed; setChanged(); notifyObservers(this); } }
Listing 9.19 shows an example module that watches an Aircraft for changes and prints a warning when the altitude is too high.
Listing 9.19 Source Code for AltitudeMonitor.java import java.util.*; // // // // // // This class demonstrates how you can add new features to an application without rewriting a lot of code. In this case, this is a module that monitors aircraft altitudes and prints out a warning if one gets too high. The aircraft class doesn't know anything about this class, their only interaction is through the Observer-Observable interface.
public class AltitudeMonitor extends Object implements Observer { double maxAltitude; public AltitudeMonitor(double maxAlt) { this.maxAltitude = maxAlt; } // Somewhere in your application you will have to add code
// to tell this object about new aircraft. public void addAircraft(Aircraft newAircraft) { newAircraft.addObserver(this); } public void update(Observable obs, Object arg) { // Make sure this update is for an aircraft if (!(obs instanceof Aircraft)) return; Aircraft ac = (Aircraft) obs; if (ac.getAltitude() > maxAltitude) { System.out.println("Warning! Aircraft too high!"); return; } } }
Figure 9.7 shows the relationship between an altitude monitor and an aircraft. Figure 9.7 : The altitude monitor registers itself as an observer of an aircraft and then watches the aircraft's altitude. One of the advantages of designing things this way is that AltitudeMonitor could be an add-on feature to your tracking system. You could create whole sets of monitors similar to this that your customer could pick from. Notice, however, there's still one little flaw here. When you add a new aircraft, you have to call addAircraft in the AircraftMonitor object. If you wanted to add new types of monitors, you'd have to call a similar method in the new monitor. This is not good. You want to be able to add a new monitor without adding even one line of code. You can do it, too! You can create an AircraftRegistry class that is an Observable. Its job in life is to notify its observers whenever a new aircraft is added. Instead of calling addAircraft for each different monitor you have in your system, you just call addAircraft in the registry, and it notifies its observers of the new aircraft. Listing 9.20 shows an implementation of an AircraftRegistry class. It is implemented as a singleton class, which means there is only one in the entire system. A singleton class is implemented by keeping a protected static pointer to the lone instance of the class. You also hide the constructor so no one can create their own instance. Then, you create a static method that returns the lone instance of the class, creating a new one if there wasn't one already.
Listing 9.20 Source Code for AircraftRegistry.java import java.util.*; // This class provides a way for aircraft monitors to find out
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch9.htm (24 of 28) [8/14/02 10:53:13 PM]
// // // //
about new aircraft. It is implemented as a singleton class, which means there is only one. Its constructor is protected, so you can't create a new AircraftRegistry manually. Any time you need the registry, you access it through: AircraftRegistry.instance()
public class AircraftRegistry extends Observable { // reference to the single instance of AircraftRegistry in the system protected static AircraftRegistry registry; protected AircraftRegistry() { } // Return the lone instance of this class. If there isn't one, create it. public synchronized static AircraftRegistry instance() { if (registry == null) { registry = new AircraftRegistry(); } return registry; } // When an aircraft is added to the system, notify all the interested parties public void addAircraft(Aircraft aircraft) { setChanged(); // Pass the new aircraft to the interested parties notifyObservers(aircraft); } }
Now, the AltitudeMonitor class no longer needs the AddAircraft method. Instead, its update method has to be smart enough to know whether the update came from an Aircraft or from the AircraftRegistry. Listing 9.21 shows the updated AltitudeMonitor class.
Listing 9.21 Source Code for AltitudeMonitor2.java import java.util.*; // This class demonstrates how you can add new features to an // application without rewriting a lot of code. In this case,
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch9.htm (25 of 28) [8/14/02 10:53:13 PM]
// // // // // //
this is a module that monitors aircraft altitudes and prints out a warning if one gets too high. The aircraft class doesn't know anything about this class, their only interaction is through the Observer-Observable interface. This class uses the AircraftRegistry to learn about new aircraft.
public class AltitudeMonitor2 extends Object implements Observer { double maxAltitude; public AltitudeMonitor2() { maxAltitude = 40000.0; AircraftRegistry.instance().addObserver(this); } public AltitudeMonitor2(double maxAlt) { this.maxAltitude = maxAlt; AircraftRegistry.instance().addObserver(this); } public void update(Observable obs, Object arg) { // See if this update is for an aircraft if (obs instanceof Aircraft) { Aircraft ac = (Aircraft) obs; if (ac.getAltitude() > maxAltitude) { System.out.println( "Warning! Aircraft too high!"); return; } // If this update is from the registry, it is telling us about // a new aircraft, so start observing the new aircraft } else if (obs instanceof AircraftRegistry) { Aircraft ac = (Aircraft) arg; ac.addObserver(this); } } }
This may seem like a lot of fuss to you, but it makes your software much more modular. The AircraftRegistry class provides just the extra level of abstraction to really make this system modular. Now you can add new monitors without
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch9.htm (26 of 28) [8/14/02 10:53:13 PM]
changing a line of code anywhere in the program. You can dynamically load new monitors on-the-fly, thanks to Java's class loading interface. Figure 9.8 shows the relationship between an aircraft, the aircraft registry, and the altitude monitor. Figure 9.8 : The aircraft registry sends out updates when a new aircraft is created. Listing 9.22 shows a simple program that tests the interaction between an Aircraft, the AircraftRegistry, and the AltitudeMonitor classes. The monitors array contains a list of the aircraft monitors to be loaded, currently just the AltitudeMonitor2 class. This list could be read in from a file just as easily, so you wouldn't have to recompile even the test program to add new monitors; however, for this demonstration, a static array is sufficient.
Listing 9.22 Source Code for TestMonitor.java // This class demonstrates the highly dynamic nature of the // Aircraft, AircraftRegistry, and AltitudeMonitor classes. public class TestMonitor extends Object { // The list of monitors to dynamically load. static String monitors[] = { "AltitudeMonitor2" }; // Load the monitors dynamically public static void createMonitors() { for (int i=0; i < monitors.length; i++) { try { // Use the class loader. If the load fails, print an error message, but // keep running. Class monClass = Class.forName(monitors[i]); monClass.newInstance(); } catch (Exception e) { System.err.println("Got error creating class "+ monitors[i]); System.err.println(e); } } } public static void main(String[] args) { // Dynamically load the aircraft monitors createMonitors(); // Create a dummy aircraft
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch9.htm (27 of 28) [8/14/02 10:53:13 PM]
Aircraft ac = new Aircraft("MW1234NA", 0.0, 0.0, 10000.0, 400.0); // Add the dummy aircraft to the system AircraftRegistry.instance().addAircraft(ac); // Play with the altitudes and see if the monitor catches it. System.out.println("Setting to 12000"); ac.setAltitude(12000.0); System.out.println("Setting to 48000"); ac.setAltitude(48000.0); } }
The only disadvantage of this program dynamically loading the monitors is that the dynamic loading process can only call the empty constructor for the monitor. You would have to find alternate means of the monitors getting their configuration. Although these examples are fairly specific to a particular application, the concepts apply to a wide range of applications. Use the Observer-Observable interface to separate components as much as possible. You will find that it is much easier to plug in new components.
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f9-1.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f9-2.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f9-3.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f9-4.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f9-5.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f9-6.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f9-7.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f9-8.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch10.htm
CONTENTS
G G G G
Locating Other Applets Exchanging Data Using Piped Streams Creating Multi-Client Pipes Sharing Information with Singleton Objects
This chapter deals with the communication between applets running within the same browser. It does not address the problem of sending information between applets running in separate browsers. The two problems are completely different. When two applets are in the same browser, they can communicate using any form of inter-object communication, including direct method calls. Communication between applets in different browsers requires some form of networking and usually an intermediate server. One common form of network communication is Remote Method Invocation (RMI), which is discussed in Chapter 16, "Creating 3-Tier Distributed Applications with RMI."
Listing 10.1 Source Code for ListApplets.java import java.applet.*; import java.awt.*; import java.util.*; // This applet demonstrates the use of the getApplets method to // get an enumeration of the current applets. public class ListApplets extends Applet
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch10.htm (1 of 19) [8/14/02 10:53:25 PM]
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch10.htm
{ public void init() { // Get an enumeration all the applets in the runtime environment Enumeration e = getAppletContext().getApplets(); // Create a scrolling list for the applet names List appList = new List(); while (e.hasMoreElements()) { // Get the next applet Applet app = (Applet) e.nextElement(); // Store the name of the applet's class in the scrolling list appList.addItem(app.getClass().getName()); } add(appList); } }
Figure 10.1 shows the ListApplets applet on a page with a number of other applets. Figure 10.1 : An applet can see what other applets are running on the same page. Note Be prepared to check for an applet again if you can't find it the first time. Your browser may not load applets all at once, or you might dynamically load an applet. You may receive a NullPointerException if you try to get an applet that doesn't exist-be prepared to catch it. You may have difficulty distinguishing when an applet hasn't been loaded yet from error situations in which it can't be loaded. Try picking a maximum amount of time you'll wait for an applet to be loaded, and then assume that there's a problem if the applet you want hasn't been loaded after that time.
If you already know the name of the applet you want to access, you can locate it with the getApplet method. The following code fragment locates an applet named findme: Applet findme = getAppletContext().getApplet("findme"); if (findme != null) { // do something with findme }
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch10.htm
You might think that the applet name you use in getApplet is the class name of the applet. This is not the case. You set the applet's name in your <APPLET> HTML tag. For example, here is the <APPLET> tag for an applet class called FindMe, which has an applet name of findme: <APPLET codebase="." code="FindMe.class" name="findme">]
Tip Only use lowercase names for applets. Some versions of Netscape convert the applet name to lowercase. The name of the applet is separate from the name of the applet's class, so you can still use uppercase letters in the class name.
Stream pipes are useful for doing sequenced messaging between objects. Sometimes one object needs to tell another object to perform several tasks in sequence. If some of the tasks take a long time, you don't want the requesting object to have to wait for all the tasks to be performed, yet you want to ensure that they are done in the proper sequence. If the requests are made by sending messages over a stream pipe, the sequencing problem is solved, as is the waiting problem. The requesting object can write all its requests to the PipedOutputStream and continue on. The object performing the tasks reads each message from its PipedInputStream, performs the requested task, and then reads the next message from the pipe. The messages are guaranteed to be read in the same sequence they were written. Listing 10.2 shows an applet that creates a stream pipe and passes one end of the pipe to another applet. It demonstrates how to create a pipe and how to wait for an applet to appear. The SenderApplet first looks for its companion appletReaderApplet. Since the sender may be loaded before the reader, it must retry the search if it can't find the reader the first time it checks. It tries once every second for 30 seconds before deciding that the reader isn't going to be loaded.
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch10.htm (3 of 19) [8/14/02 10:53:25 PM]
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch10.htm
Once the sender finds the reader, it passes one end of the stream pipe to the reader through a simple method call. If you find that you frequently need to pass stream pipes this way, you should define an interface line this: public interface StreamPipeClient { public void setInputStream(InputStream); } This frees the sender from having to know the exact class name of the reader. As you can see in Listing 10.2, the sender knows that the reader is an instance of ReaderApplet.
Listing 10.2 Source Code for SenderApplet.java import java.applet.*; import java.io.*; // // // // This applet creates a stream pipe and uses it to pass data to another applet. It waits for the other applet to be loaded, then invokes a method on that applet to pass it the input side of the pipe.
public class SenderApplet extends Applet implements Runnable { protected PipedInputStream inStream; protected PipedOutputStream outStream; Thread appletThread; public void init() { // Create the pipe. It doesn't matter which end you create first, you just // pass the first end to the constructor of the other end. try { inStream = new PipedInputStream(); outStream = new PipedOutputStream(inStream); } catch (Exception e) { e.printStackTrace(); } } public void run() { Applet app = null; AppletContext context = getAppletContext(); int tries = 0; // how many times we've looked
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch10.htm
// Start looking for the reader applet while (app == null) { // Try to locate an applet named "reader" try { app = context.getApplet("reader"); // If we get here and app isn't null, we've found it, break out // of this while loop if (app != null) break; } catch (Exception e) { } // We couldn't find the applet. If we've tried 30 times (at once per second) // we assume it isn't coming up. tries++; if (tries > 30) { return; // time out after 30 seconds } // Sleep for a second before looking again try { Thread.sleep(1000); } catch (Exception insomnia) { } } // Now that we found the applet, cast it to a ReaderApplet so // we can invoke setInputStream ReaderApplet reader = (ReaderApplet) app; // Give the ReaderApplet the input end of the stream reader.setInputStream(inStream); while (true) { // Write byte values of 0-9 to the stream pipe over and over for (int i=0; i < 10; i++) { try { outStream.write(i); Thread.sleep(1000); } catch (Exception ignore) { }
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch10.htm
} } } public void start() { appletThread = new Thread(this); appletThread.start(); } public void stop() { appletThread.stop(); appletThread = null; } }
Listing 10.3 shows the reader portion of the pipe demonstration. The sender applet had to perform a loop to wait for the reader to become active. The reader has a similar problem-it has to wait for the sender to give it the input end of the pipe. It looks at the input stream once every second, and once the input stream is no longer null, it starts reading data. Tip Rather than continually polling to see when the input stream is no longer null, the reader could use the wait/notify mechanism. Basically, if the input stream is null, the run method calls wait,which puts its thread to sleep. Then, the setInputStream method could call notify to wake the run method back up so it can start reading again.
Listing 10.3 Source Code for ReaderApplet.java import java.applet.*; import java.awt.*; import java.io.*; // // // // This applet is the companion to the SenderApplet. It receives an input stream from the sender and begins reading one byte at a time, changing a label on the screen to the string representation of each byte read so you can see it in action.
public class ReaderApplet extends Applet implements Runnable { protected InputStream inStream; protected Thread appletThread;
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch10.htm
protected Label label; public void init() { label = new Label("X"); add(label); } // This method will be called by the SenderApplet when it locates this // applet. public void setInputStream(InputStream inStream) { this.inStream = inStream; } public void run() { // Wait for the input stream while(inStream == null) { try { Thread.sleep(1000); } catch (Exception insomnia) { } } // Start reading bytes while (true) { try { int ch = inStream.read(); // If ch < 0, we hit EOF, indicating some type of shutdown if (ch < 0) return; // Update the label with the byte we just read label.setText(""+ch); } catch (Exception e) { return; } } } public void start() { appletThread = new Thread(this); appletThread.start(); }
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch10.htm
Listing 10.4 Source Code for MultiClientOutputStream import java.io.*; import java.util.*; // // // // This class implements an output stream that sends its output to any number of client output streams. It allows an object to make one write request that gets forwarded to all streams connected to this one.
public class MultiClientOutputStream extends OutputStream { protected Vector clients; // The streams connected to this one public MultiClientOutputStream() { clients = new Vector(); } public synchronized void write(int ch) { Enumeration e = clients.elements(); // It is bad medicine to remove elements from a vector while you are // still enumerating through it, but we need to remove output streams // from the client vector when we get write errors on them. We create
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch10.htm (8 of 19) [8/14/02 10:53:26 PM]
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch10.htm
// // // //
a vector of outputstreams that need to be removed, but it only gets created if at least one stream needs to be removed. Once we finish iterating through the output streams, we remove the dead ones. Vector deadElements = null; while (e.hasMoreElements()) { OutputStream out = (OutputStream) e.nextElement(); try { out.write(ch); } catch (IOException deadStream) {
// If we haven't created the deadElements vector yet, do it now if (deadElements == null) { deadElements = new Vector(); } // Flag this stream as needing to be deleted deadElements.addElement(out); } } // If we had any dead elements, remove them from the vector of clients if (deadElements != null) { e = deadElements.elements(); while (e.hasMoreElements()) { clients.removeElement(e.nextElement()); } } } // addOutputStream connects a new stream up to the set of clients public void addOutputStream(OutputStream out) { if (!clients.contains(out)) { clients.addElement(out); } } // removeOutputStream removes a stream from the set of clients (we no // longer send output to it). public void removeOutputStream(OutputStream out) { clients.removeElement(out); }
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch10.htm (9 of 19) [8/14/02 10:53:26 PM]
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch10.htm
Listing 10.5 shows a small test program that illustrates the use of the MultiClientOutputStream class.
Listing 10.5 Source Code for TestMulti.java import java.io.*; // // // // // This class demonstrates the use of the multi-client output stream. It hooks both System.out and System.err to the multi-client stream. It then writes information to the multi-client stream, which causes the information to appear twice - once when it is copied to System.out, the other time when it is copied to System.err.
public class TestMulti extends Object { public static void main(String[] args) { try { // Create the multi-client stream MultiClientOutputStream out = new MultiClientOutputStream(); // Connect System.out and System.err to the multi-client stream out.addOutputStream(System.out); out.addOutputStream(System.err); // Use a PrintStream to write so we can use print and println PrintStream printme = new PrintStream(out); // Write out some test data, it should appear twice printme.println("Hello there!"); printme.println("Is there an echo in here?"); // Test out the fact that if you add a duplicate streams, it still // only gets one copy of the data. out.addOutputStream(System.out); printme.println("You should still be seeing double"); // Test the disconnection of an output stream (stop writing to System.err) out.removeOutputStream(System.err);
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch10.htm (10 of 19) [8/14/02 10:53:26 PM]
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch10.htm
Figure 10.2 shows the output from the TestMulti application. Figure 10.2 : Using the MultiClientOutputStream, you can write data on multiple streams with a single write call.
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch10.htm
Listing 10.6 Source Code for AppletRegistry.java import java.applet.Applet; import java.util.*; // // // // // // // // This class implements an applet registry where applets can locate each other. It is an observable, so if you want to wait for a particular class, you can be an observer. This is better than the polling you have to do with getApplet. This class is implemented as a singleton, which means there is only one. The single instance is kept in a protected static variable and returned by the instance() method.
public class AppletRegistry extends Observable { // The single copy of the registry protected static AppletRegistry registry; // The table of applets protected Hashtable applets; // Used for generating unique applet names protected int nextUnique; protected AppletRegistry() { applets = new Hashtable(); nextUnique = 0; } // Returns the long instance of the registry. If there isn't a registry // yet, it creates one. public synchronized static AppletRegistry instance() { if (registry == null) { registry = new AppletRegistry(); } return registry; } // Adds a new applet to the registry - stores it in the table and // sends a notification to its observers. public synchronized void addApplet(String name, Applet newApplet) { applets.put(name, newApplet); setChanged(); notifyObservers(new AppletRegistryEvent(
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch10.htm (12 of 19) [8/14/02 10:53:26 PM]
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch10.htm
AppletRegistryEvent.ADD_APPLET, name, newApplet)); } // // // // // Adds a new applet to the registry - stores it in the table and sends a notification to its observers. If uniqueName is false, the applet's name is non-unique. Store the applet in a table with a unique version of the name (appends <#> to the name where # is a constantly increasing number). public synchronized void addApplet(String name, Applet newApplet, boolean uniqueName) { if (!uniqueName && (applets.get(name) != null)) { name = name + "<"+nextUnique+">"; nextUnique++; } applets.put(name, newApplet); setChanged(); notifyObservers(new AppletRegistryEvent( AppletRegistryEvent.ADD_APPLET, name, newApplet)); } // removes an applet from the table and notifies the observers public synchronized void removeApplet(Applet applet) { Enumeration e = applets.keys(); while (e.hasMoreElements()) { Object key = e.nextElement(); if (applets.get(key) == applet) { applets.remove(key); setChanged(); notifyObservers(new AppletRegistryEvent( AppletRegistryEvent.REMOVE_APPLET, (String)key, applet)); return; } } } // removes an applet from the table and notifies the observers public synchronized void removeApplet(String name) { Applet applet = (Applet) applets.get(name); if (applet == null) return;
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch10.htm (13 of 19) [8/14/02 10:53:26 PM]
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch10.htm
applets.remove(name); setChanged(); notifyObservers(new AppletRegistryEvent( AppletRegistryEvent.REMOVE_APPLET, name, applet)); } // finds an applet by name, or returns null if not found public Applet findApplet(String name) { return (Applet) applets.get(name); } // lets you see all the applets in the registry public Enumeration getApplets() { return applets.elements(); } }
Listing 10.7 shows the implementation of the AppletRegistryEvent object used by the AppletRegistry. This is an example of how to set up an event that is passed to the update method in an observable's observers.
Listing 10.7 Source Code for AppletRegistryEvent.java import java.applet.Applet; public class AppletRegistryEvent extends Object { public final static int ADD_APPLET = 1; public final static int REMOVE_APPLET = 2; public int id; public String appletName; public Applet applet; public AppletRegistryEvent() { } public AppletRegistryEvent(int id, String appletName, Applet applet) { this.id = id;
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch10.htm (14 of 19) [8/14/02 10:53:26 PM]
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch10.htm
Figure 10.3 shows the relationship between applets, the applet registry, and the observers of the registry. Figure 10.3 : Applets add themselves to the registry, which tells its observers about the new applets. To take advantage of the AppletRegistry, a new applet must register itself by calling the addApplet method. Listing 10.8 shows a new version of the SenderApplet example from earlier in this chapter. The corresponding ReaderApplet only needs to call addApplet("reader", this) in its init method to support the registry. Because the new sender applet uses the applet registry, it doesn't have to contin-ually check to see when the reader is loaded. Instead, it waits until it receives an AppletRegistryEvent that tells it that the reader applet has been added. Once it learns that the reader has been added, it passes one end of the pipe stream to the reader and starts a thread that writes data to the pipe.
Listing 10.8 Source Code for SenderApplet2.java import java.applet.*; import java.io.*; import java.util.*; // // // // // // // // // This applet creates a stream pipe and uses it to pass data to another applet. It waits for the other applet to be loaded, then invokes a method on that applet to pass it the input side of the pipe. Rather than using the standard getApplet method to check for the other applet being loaded, it uses the AppletRegistry. Also, it doesn't start its thread until the applet has been started and an update has been received stating that the other applet has been added.
public class SenderApplet2 extends Applet implements Runnable, Observer { protected PipedInputStream inStream; protected PipedOutputStream outStream; protected boolean started = false; Thread appletThread = null; public synchronized void init()
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch10.htm
{ // Create the pipe. It doesn't matter which end you create first, you just // pass the first end to the constructor of the other end. try { inStream = new PipedInputStream(); outStream = new PipedOutputStream(inStream); } catch (Exception e) { e.printStackTrace(); } // Add this applet to the registry AppletRegistry.instance().addApplet("sender", this); // Start watching the registry AppletRegistry.instance().addObserver(this); // If the reader applet is already in the registry, set it up Applet applet = AppletRegistry.instance().findApplet("reader"); if (applet != null) { initReader(applet); } } // update is called by the registry whenever an applet is added or removed. public synchronized void update(Observable obs, Object ob) { if (!(ob instanceof AppletRegistryEvent)) return; AppletRegistryEvent evt = (AppletRegistryEvent) ob; if (evt.appletName.equals("reader")) { initReader(evt.applet); } } public void initReader(Applet applet) { // Now that we found the applet, cast it to a ReaderApplet so // we can invoke setInputStream ReaderApplet2 reader = (ReaderApplet2) applet; // Give the ReaderApplet the input end of the stream reader.setInputStream(inStream); appletThread = new Thread(this);
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch10.htm
if (started) { appletThread.start(); } } public void run() { while (true) { // Write byte values of 0-9 to the stream pipe over and over for (int i=0; i < 10; i++) { try { outStream.write(i); Thread.sleep(1000); } catch (Exception ignore) { } } } } public synchronized void start() { started = true; // // // // // // If the applet thread has already been created, start it. This is done to synchronize with the update method. We don't know if start or update will be called first. This method sets the started flag to true, but only starts the thread if it has been created. The update method creates the thread, but only starts it if the started flag is true. if (appletThread != null) { appletThread.start(); } } public void stop() { appletThread.stop(); appletThread = null; } }
The reader applet that corresponds to SenderApplet2 is not too different from the original reader applet. The main difference is that it now adds itself to the applet registry. It still uses a polling mechanism to wait for its input stream. Again, you could use the wait/notify mechanism to keep from polling. Listing 10.9 shows the ReaderApplet2 applet.
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch10.htm (17 of 19) [8/14/02 10:53:26 PM]
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch10.htm
Listing 10.9 Source Code for ReaderApplet2.java import java.applet.*; import java.awt.*; import java.io.*; // // // // This applet is the companion to the SenderApplet. It receives an input stream from the sender and begins reading one byte at a time, changing a label on the screen to the string representation of each byte read so you can see it in action.
public class ReaderApplet2 extends Applet implements Runnable { protected InputStream inStream; protected Thread appletThread; protected Label label; public void init() { label = new Label("X"); add(label); AppletRegistry.instance().addApplet("reader", this); } // This method will be called by the SenderApplet when it locates this // applet. public void setInputStream(InputStream inStream) { this.inStream = inStream; } public void run() { // Wait for the input stream while(inStream == null) { try { Thread.sleep(1000); } catch (Exception insomnia) { } } // Start reading bytes while (true) {
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch10.htm
try { int ch = inStream.read(); // If ch < 0, we hit EOF, indicating some type of shutdown if (ch < 0) return; // Update the label with the byte we just read label.setText(""+ch); } catch (Exception e) { return; } } } public void start() { appletThread = new Thread(this); appletThread.start(); } public void stop() { appletThread.stop(); appletThread = null; } }
CONTENTS
G G G
G G G
Creating 3-Tier Applications RMI Features Creating an RMI Server H Defining a Remote Interface H Creating the Server Implementation H Creating the Stub Class Creating an RMI Client Creating Peer-to-Peer RMI Applications Garbage Collection, Remote Objects, and Peer-to-Peer
There are many ways that objects can communicate with one another over a network. Traditionally, objects would communicate with one another using sockets and a custom protocol. Remote procedure calls have also been a popular communication mechanism for the past few years. Java provides two additional mechanisms for remote object-toobject communication. Java provides an interface into the CORBA-distributed object architecture, which is discussed in Chapter 17, "Creating CORBA Clients." Remote Method Invocation (RMI) provides a very simple method for one Java object to invoke a method in another Java object across a network with very little extra work. Unlike many remote communication systems that require you to describe the remote methods in a separate file, RMI works right off existing objects, providing seamless integration.
layer of business logic, and a database. Once you break out of the 2-tier mold, you often start adding multiple tiers. Figure 16.1 illustrates the difference between a 2-tier and 3-tier application design. Figure 16.1 : A 3-tier design adds an extra layer of abstraction to improve reuse. You can also divide your application into an application logic tier and a presentation tier (the user interface). In a 2tier model, the business logic is part of the application. In smaller applications, this is not a problem because there may be only one application implementing a particular business process. In larger systems, however, many applications use the same areas of business logic. In a 2-tier environment, this means that the business logic is replicated across every application. If you change the business logic, you must change every application. Durable software systems are designed from the ground up with change in mind. A good designer creates modular components with well-defined interfaces so that any single component can be changed without affecting the rest of the system. The 3-tier and multi-tier models are simply the results of modular design. Before you go off thinking that creating 3-tier designs is simple, think again. Identifying business processes in a large company and reducing them to a set of methods is not a task for the fainthearted. Many companies do not have their business logic documented as a series of processes. Instead, it is only implied in the code of the applications. The identification of business processes and business logic is a subject for another book, however. In practice, the line between 2-tier and 3-tier is often rather fuzzy. You may have what is essentially a 2-tier application whose user interface is broken out into a separate module. The application logic and the business logic are still intermixed, but a portion of the application is distributed. Just because you can't get a handle on the actual business logic doesn't mean you can't still work at making your software more modular and add the benefits of distributed computing. RMI is very useful for separating an application from its user interface. You define the methods that comprise the interactions between the user interface and the client, and then make these interactions through remote method invocation (RMI). This allows an applet to implement the user interface for an application running on a server somewhere, without developing a custom communications system.
RMI Features
RMI is like a remote procedure call (RPC) mechanism in other languages. One object makes a method call into an object on another machine and gets a result back. Like most RPC systems, RMI requires that the object whose method is being invoked (the server) must already be up and running. Remote methods are defined by remote interfaces. That is, a remote interface defines a set of methods that can be called remotely. Any object that wants some of its methods to be called remotely must use one or more remote interfaces.
An object that uses a remote interface is called a server. An object that calls a remote method is called a client. An object can be both a client and a server: These names indicate only who is calling in a particular instance and who is being called. Once you define a remote interface and create an object that uses the interface, you still need a way for the client to invoke methods on the server. Unfortunately, it is not quite as easy as instantiating a server object. You need to create a stub for the client. An object's stub is a remote view of that object in that it contains only the remote methods of the object. The stub runs on the client side and is the representative of the remote object in the client's data space. The client invokes methods on the stub and the stub then invokes the methods on the remote object. This allows any client to invoke remote methods through normal Java method invocation. A stub is also called a proxy. Figure 16.2 shows the relationship between a client, a server, and a stub. Figure 16.2 : A stub invokes remote methods on behalf of a client. RMI adds an extra feature that most RPC systems do not have. Remote objects can be passed as parameters in remote method calls. When you pass a remote object as a parameter, you actually pass a stub for the object. The real object always stays on the machine where it was originally started. The stub that is passed then invokes methods back to the original object. Stubs can also be passed as parameters and work the same way. Figure 16.3 illustrates how a client passes a stub to a server so that the server can invoke methods on the client. Figure 16.3 : A client can pass a stub to a server so the server can invoke methods on the client. In a distributed system, you need a way for clients to find the servers they need. RMI provides a simple name lookup object that allows a client to get a stub for a particular server based on the server's name. The naming service that comes with the RMI system is fairly simplistic but is useful for most cases. Figure 16.4 shows how a client uses the naming service to find a server. Figure 16.4 : The naming service allows a client to locate a server by name.
java.rmi.RemoteException. This exception is thrown by the underlying RMI system whenever there is an error in sending or receiving information. Listing 16.1 shows a sample remote interface for a simple banking application.
Listing 16.1 Source Code for Banking.java package banking; // This interface represents a set of remote methods for a // banking service. All money amounts are given in cents, so // one dollar is represented as 100. public interface Banking extends java.rmi.Remote { // getBalance returns the current balance in the account public int getBalance(Account account) throws java.rmi.RemoteException, BankingException; // withdraw subtracts an amount from an account public void withdraw(Account account, int amount) throws java.rmi.RemoteException, BankingException; // deposit adds an amount to the account public void deposit(Account account, int amount) throws java.rmi.RemoteException, BankingException; // transfer subtracts an amount from one account and // adds it to another. public void transfer(Account fromAccount, Account toAccount, int amount) throws java.rmi.RemoteException, BankingException; }
Notice that the account information is encapsulated in an Account object. This allows you to change the way you represent accounts without modifying the interface. Of course, you may have to change the client and server to understand the new account format. Tip
Try to encapsulate related parameters into a single object, especially if they are subject to change. If a position is given by an x,y coordinate, encapsulate it in a Position object so you can later change the position to be x,y,z or even polar coordinates. This allows you to keep the remote interface the same.
Listing 16.2 shows the Account object used in the Banking interface.
Listing 16.2 Source Code for Account.java package banking; // This class contains the information that defines // a banking account. public class Account extends { // Flags to indicate whether public static final int public static final int Object the account is savings or checking CHECKING = 1; SAVINGS = 2;
public String id; // Account id, or account number public String password; // password for ATM transactions public int which; // is this checking or savings public Account() { } public Account(String id, String password, int which) { this.id = id; this.password = password; this.which = which; } public String toString() { return "Account { "+id+","+password+","+which+" }"; } // Tests equality between accounts. public boolean equals(Object ob) { if (!(ob instanceof Account)) return false;
Account other = (Account) ob; return id.equals(other.id) && password.equals(other.password) && (which == other.which); } // Returns a hash code for this object public int hashCode() { return id.hashCode()+password.hashCode()+which; } }
Tip When encapsulating similar data into an object, always define the equals and hashCode methods. You may occasionally want to store the objects in hash tables and other structures, and without these methods, two objects containing identical data look like two separate objects.
Listing 16.3 shows the BankingException class for the Banking interface.
Listing 16.3 Source Code for BankingException.java package banking; // Defines a generic banking exception for the banking interface. public class BankingException extends Exception { public BankingException() { } public BankingException(String problem) { super(problem); } }
Tip Don't lump all your exceptions into one big exception, hiding the specific information in a string. Create exceptions specifically for each separate case. You don't want to parse the exception string to find out what kind of exception it was. Instead, you should be using instanceof.
For a simple interface like the Banking interface, there are only two specific exceptions defined: InvalidAccountException and InsufficientFundsException. Listings 16.4 and 16.5 show these exceptions.
Listing 16.4 Source Code for InvalidAccountException.java package banking; // Defines an exception for an invalid account and indicates // which account was invalid. Also allows an error string. public class InvalidAccountException extends BankingException { public Account account; // which account was invalid public InvalidAccountException() { } public InvalidAccountException(String str) { super(str); } public InvalidAccountException(Account account) { this.account = account; } public InvalidAccountException(Account account, String str) { super(str); this.account = account; }
Listing 16.5 Source Code for InsufficientFundsException.java package banking; // Defines a simple Insufficent Funds exception for the // Banking interface. public class InsufficientFundsException extends BankingException { public InsufficientFundsException() { } public InsufficientFundsException(String problem) { super(problem); } }
Listing 16.6 Source Code for BankingImpl.java package banking; import java.rmi.Naming; import java.rmi.server.UnicastRemoteServer; import java.rmi.server.StubSecurityManager; import java.util.*; // // // // // // // // This class implements a remote banking object. It sets up a set of dummy accounts and allows you to manipulate them through the Banking interface. Accounts are the password way to work, the password identified by the combination of the account id, and the account type. This is a quick and dirty and not the way a bank would normally do it, since is not part of the unique identifier of the account.
public class BankingImpl extends UnicastRemoteServer implements Banking { public Hashtable accountTable; // The constructor creates a table of dummy accounts. public BankingImpl() throws java.rmi.RemoteException { accountTable = new Hashtable(); accountTable.put( new Account("AA1234", "1017", Account.CHECKING), new Integer(50000)); // $500.00 balance accountTable.put( new Account("AA1234", "1017", Account.SAVINGS), new Integer(148756)); // $1487.56 balance accountTable.put( new Account("AB5678", "4456", Account.CHECKING), new Integer(7742)); // $77.32 balance accountTable.put( new Account("AB5678", "4456", Account.SAVINGS), new Integer(32201)); // $322.01 balance }
// getBalance returns the amount of money in the account (in cents). // If the account is invalid, it throws an InvalidAccountException public int getBalance(Account account) throws java.rmi.RemoteException, BankingException { // Fetch the account from the table Integer balance = (Integer) accountTable.get(account); // If the account wasn't there, throw an exception if (balance == null) { throw new InvalidAccountException(account); } // Return the account's balance return balance.intValue(); } // // // // withdraw subtracts an amount from the account's balance. If the account is invalid, it throws InvalidAccountException. If the withdrawal amount exceeds the account balance, it throws InsufficientFundsException. public synchronized void withdraw(Account account, int amount) throws java.rmi.RemoteException, BankingException { // Fetch the account Integer balance = (Integer) accountTable.get(account); // If the account wasn't there, throw an exception if (balance == null) { throw new InvalidAccountException(account); } // If we are trying to withdraw more than is in the account, // throw an exception if (balance.intValue() < amount) { throw new InsufficientFundsException(); } // Put the new balance in the account accountTable.put(account, new Integer(balance.intValue() amount)); } // Deposit adds an amount to an account. If the account is invalid
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch16.htm (10 of 22) [8/14/02 10:53:32 PM]
// it throws an InvalidAccountException public synchronized void deposit(Account account, int amount) throws java.rmi.RemoteException, BankingException { // Fetch the account Integer balance = (Integer) accountTable.get(account); // If the account wasn't there, throw an exception if (balance == null) { throw new InvalidAccountException(account); } // Update the account with the new balance accountTable.put(account, new Integer(balance.intValue() + amount)); } // // // // Transfer subtracts an amount from fromAccount and adds it to toAccount. If either account is invalid it throws InvalidAccountException. If there isn't enough money in fromAccount it throws InsufficientFundsException. public synchronized void transfer(Account fromAccount, Account toAccount, int amount) throws java.rmi.RemoteException, BankingException { // Fetch the from account Integer fromBalance = (Integer) accountTable.get(fromAccount); // If the from account doesn't exist, throw an exception if (fromBalance == null) { throw new InvalidAccountException(fromAccount); } // Fetch the to account Integer toBalance = (Integer) accountTable.get(toAccount); // If the to account doesn't exist, throw an exception if (toBalance == null) { throw new InvalidAccountException(toAccount); } // Make sure the from account contains enough money, otherwise throw // an InsufficientFundsException. if (fromBalance.intValue() < amount) { throw new InsufficientFundsException();
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch16.htm (11 of 22) [8/14/02 10:53:32 PM]
} // Subtract the amount from the fromAccount accountTable.put(fromAccount, new Integer(fromBalance.intValue() - amount)); // Add the amount to the toAccount accountTable.put(toAccount, new Integer(toBalance.intValue() + amount)); } public static void main(String args[]) { // Need a security manager to prevent malicious stubs System.setSecurityManager(new StubSecurityManager()); try { // Create the bank BankingImpl bank = new BankingImpl(); // Register the bank with the naming service. Naming.rebind("NetBank", bank); } catch (Exception e) { System.out.println("Got exception: "+e); e.printStackTrace(); } } }
Creating an RMI client is a simple task. When you need to access a remote object, you call the lookup method in the Naming service (also called the registry). The lookup method returns a stub for the remote object. The contains all the remote methods defined for that object. If the stub is not on the client system, the RMI system tries to download the stubs from the remote object's host or from wherever the remote object was loaded. Listing 16.7 shows a very simple application that remotely invokes methods in the BankingImpl object.
Listing 16.7 Source Code for BankingClient.java import java.rmi.server.StubSecurityManager; import java.rmi.Naming; import banking.*; // This program tries out some of the methods in the BankingImpl // remote object. public class BankingClient { public static void main(String args[]) { // Always set up a security manager when running RMI System.setSecurityManager(new StubSecurityManager()); // Create an Account object for the account we are going to access. Account myAccount = new Account( "AA1234", "1017", Account.CHECKING); try { // Get a stub for the BankingImpl object (the stub implements the // Banking interface). Banking bank = (Banking)Naming.lookup("NetBank"); // Check the initial balance System.out.println("My balance is: "+ bank.getBalance(myAccount)); // Deposit some money bank.deposit(myAccount, 50000);
// Check the balance again System.out.println("Deposited $500.00, balance is: "+ bank.getBalance(myAccount)); // Withdraw some money bank.withdraw(myAccount, 25000); // Check the balance again System.out.println("Withdrew $250.00, balance is: "+ bank.getBalance(myAccount)); } catch (Exception e) { System.out.println("Got exception: "+e); e.printStackTrace(); } } }
The RMI system allows an object to be both a client and a server, relieving you of many of these headaches. Typically, one object starts out as the server and one starts out as the client. At some point, the client invokes a method on the server and passes a stub back to the client, and the client also becomes a server. You might, for example, have a server that sends periodic updates of information. A client registers with the server telling it what information it wants and passes the client's stub to the server. Whenever the server has new information, it invokes a method in what was originally the client via the stub. Figure 16.5 shows the relationship between two objects in a peer-to-peer stock-quoting system. Figure 16.5 : The stock-quote server uses RMI to send quotes to its clients. Listing 16.8 shows a remote interface for a stock-quoting system that invokes a method in its clients to deliver stock quotes.
Listing 16.8 Source Code for StockQuoteServer.java package stocks; // Defines a remote interface for a stock quoting system. // Stock quotes are delivered to remote objects through the // StockQuoteClient interface. public interface StockQuoteServer extends java.rmi.Remote { // addWatch tells the server that the client wants quotes for // a certain stock. public void addWatch(StockQuoteClient client, String stock) throws java.rmi.RemoteException, StockQuoteException; // removeWatch tells the server that the client no longer wants // to watch a certain stock. public void removeWatch(StockQuoteClient client, String stock) throws java.rmi.RemoteException, StockQuoteException; // removeClient tells the server that the client no longer wants // to watch any stocks. public void removeClient(StockQuoteClient client) throws java.rmi.RemoteException, StockQuoteException; // getStockList returns an array of all the stocks that can be watched public String[] getStockList() throws java.rmi.RemoteException;
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch16.htm (15 of 22) [8/14/02 10:53:32 PM]
Listing 16.9 shows the StockQuoteClient interface that the StockQuoteServer uses to notify its clients of new quotes
Listing 16.9 Source Code for StockQuoteClient.java package stocks; // Defines a callback interface for the StockQuoteServer so // it can notify its clients of new stock quotes. public interface StockQuoteClient extends java.rmi.Remote { public void quote(StockQuote quote) throws java.rmi.RemoteException; }
Rather than putting the individual elements of a stock quote into the method definition, the stock quotes are passed around in a StockQuote object. If the system expands the information in the stock quote, it still works with the existing clients, as long as it doesn't remove or rename any fields. This lets you build an extensible system without having to change all your existing clients at once. If you change the quote method, however, all the clients have to change. Listing 16.10 shows the StockQuote object.
Listing 16.10 Source Code for StockQuote.java package stocks; // Defines the information contained in a stock quote for the // StockQuoteClient interface. public class StockQuote { public String stock; public double amount; public double change; public StockQuote() { }
public StockQuote(String stock, double amount, double change) { this.stock = stock; this.amount = amount; this.change = change; } }
The stock-quote system defines its own exceptions. You should always do this for your systems if you intend to throw any exceptions outside the standard ones in Java. StockQuoteException serves as the base class for all specific exceptions in the stock-quote system. There is only one specific exception defined: UnknownStockException. Again, if you can define a specific exception, do it. Don't heap everything into one generic exception. Listings 16.11 and 16.12 show StockQuoteException and UnknownStockException.
Listing 16.11 Source Code for StockQuoteException.java package stocks; // Defines a generic exception for the stock quoting system public class StockQuoteException extends Exception { public StockQuoteException() { } public StockQuoteException(String str) { super(str); } }
Listing 16.12 Source Code for UnknownStockException.java package stocks; // Defines an exception for an unknown stock. public class UnknownStockException extends StockQuoteException
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch16.htm (17 of 22) [8/14/02 10:53:32 PM]
Distributed systems have their own unique little problems. When you invoke a method on an object locally, you don't worry about whether or not the method will be invoked. If you get an exception, you know that there was an error within the method and not a problem invoking the method. There are, however, many things in a distributed system that can stand between a client and the remote method it is invoking. When you get a RemoteException, you don't know what the problem is. The network could have had a temporary failure, the server program could have died, or the machine the server was running on could have died. Listing 16.13 shows the addWatch method from the StockQuoteServerImpl class included on the CD for this book. This method is invoked by clients to subscribe to stock quotes. The first parameter to the addWatch method is a reference to the client (actually, a stub for communicating with the client). The server saves this reference for later use when it goes to publish new stock quotes. The StockQuoteServerImpl keeps a table of clients for each stock, because a stock can have multiple clients (a client of a stock receives quotes for that stock).
Listing 16.13 addWatch Method for StockQuoteServerImpl.java // addWatch adds a client to the list of clients watching a stock public void addWatch(StockQuoteClient client, String stock) throws java.rmi.RemoteException, StockQuoteException { // If we don't know about the stock, throw an exception if (stocks.get(stock) == null) { throw new UnknownStockException(stock); } // Get the container of clients watching this stock Vector clients = (Vector) stockClients.get(stock); // If no clients are watching, create the container if (clients == null) {
clients = new Vector(); clients.addElement(client); stockClients.put(stock, clients); // Only add the client if it isn't already there. We don't want to // double-update clients. } else if (!clients.contains(client)) { clients.addElement(client); } }
One of the most important things you must handle when performing callbacks is figuring out when a client has disconnected. The StockQuoteServerImpl class uses a very simple technique-when the server sends a stock quote to a client that results in an exception, the server disconnects the client. Listing 16.14 shows the publishQuote method from the StockQuoteServerImpl. Notice that when the publishQuote method catches an exception when publishing the quote, it does not immediately remove the client. Instead, it stores the reference to the client in a separate vector. This is necessary because an enumeration can become confused if you remove elements from a vector while you are enumerating through it.
Listing 16.14 publishQuote Method from StockQuoteServerImpl.java // publishQuote sends a stock quote to every client who is watching protected void publishQuote(StockQuote quote) { // Get the list of clients for the stock Vector v = (Vector) stockClients.get(quote.stock); // If there are no clients, we're done if (v == null) return; Enumeration e = v.elements(); // When we get an exception sending a notification to a client, we // remove the client. We don't do it until we've sent all the // notifications however. We store them in badClients until then. Vector badClients = null; while (e.hasMoreElements()) { StockQuoteClient client = (StockQuoteClient) e.nextElement();
// send the quote to the client try { client.quote(quote); // If we get an error, add the client to the list of bad clients } catch (java.rmi.RemoteException oops) { if (badClients == null) { badClients = new Vector(); } badClients.addElement(client); } } // If there were any bad clients, remove them if (badClients != null) { e = badClients.elements(); while (e.hasMoreElements()) { clearClient( (StockQuoteClient) e.nextElement()); } } }
To do peer-to-peer RMI from an applet, you have to create another object to be the server for method invocations to the applet. You can't remotely call methods in a subclass of Applet because you must inherit from RemoteServer. Since Java doesn't allow multiple inheritance, you must define another object to handle the incoming remote method invocations. If you really want to invoke methods in the applet, the special object you create can just turn around and invoke methods in the applet. Listing 16.15 shows a simple stock-quote client that receives all stock quotes from the stock-quote server.
Listing 16.15 Source Code for StockQuoter.java package stocks; import java.rmi.server.UnicastRemoteServer; import java.rmi.server.StubSecurityManager; // This class is a client of the StockQuoteServer. It acts as a // server too, since the StockQuoteServer invokes the update method // in this object.
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch16.htm (20 of 22) [8/14/02 10:53:32 PM]
public class StockQuoter extends UnicastRemoteServer implements StockQuoteClient { public StockQuoter() throws java.rmi.RemoteException { } // When we receive a stock quote, just print out the information public void quote(StockQuote stockQuote) throws java.rmi.RemoteException { System.out.println(stockQuote.stock+": "+stockQuote.amount+ "("+stockQuote.change+")"); } public static void main(String[] args) { // Always use a security manager for RMI. System.setSecurityManager(new StubSecurityManager()); try { // Get a stub to the stock quoting system StockQuoteServer server = (StockQuoteServer) java.rmi.Naming.lookup("StockQuotes"); // Create an instance of this object to receive the incoming stock quotes StockQuoter quoter = new StockQuoter(); // Get a list of all the stock we can watch String[] stocks = server.getStockList(); // Subscribe to each stock for (int i=0; i < stocks.length; i++) { server.addWatch(quoter, stocks[i]); } } catch (Exception e) { System.out.println("Got exception: "+e); e.printStackTrace(); } } }
CONTENTS
G G G G G G G G
Defining IDL Interfaces Compiling IDL Interfaces for Java Clients Writing a Client Applet Handling Exceptions CGI Programs, Java.net.*, and Java.io.* May Not Be the Best Choices Using the Dynamic Invocation Interface and the Interface Repository Using Filters Some Points About Distributed System Architecture
The combination of Java and CORBA is a very positive development for client/server systems. While CORBA is the leading heterogeneous distributed system standard, Java provides an extremely portable and powerful mechanism for the development of client-side functionality. And, due to the marriage of these two technologies, many client/server applications that were previously costprohibitive are now cost-effective. This situation results from the simple fact that Java and CORBA solve technical problems and simplify many aspects of client/server development and deployment. More specifically, Java and CORBA provide standards-based heterogeneous inter-process communication, client-side deployment, flexible decoupling of clients and servers, portable client-side functionality, and abstraction of some of the more time-consuming aspects of programming in general. This chapter highlights and illustrates some of the ways that CORBA enhances the client/server capabilities of Java-based client applications.
Are the clients and servers being developed independently? Will there be many different clients and/or many servers which must cooperate? Is the server intended to provide a wide variety of services?
If the answer to any two of the above questions is yes, then look into the possibility of defining your server's interface(s) in a more coarse and generic fashion. This can significantly enhance the extensibility and longevity of your distributed system. As new servers and clients are introduced to your distributed system there will be much less likelihood that existing clients and servers will need to change. An example of a coarse and generic interface is one that has a comparatively small number of IDL interfaces defined, uses some of the more generic IDL types as function parameters (for example, NameValuePairs, any), and has functions that are not specific to the services provided by the server. An example of a rather specific interface is the one following. This IDL interface describes the server functionality invoked by the client applet which forms the basis for the examples in this chapter.
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch17.htm (1 of 19) [8/14/02 10:53:39 PM]
Listing 17.1 CHAPT17LISTINGS.TXT-Notebook Server IDL Interface interface NotebookIF { typedef sequence<string> stringListType; //resizeable list of strings typedef exception AccessNotAuthorizedExc; // user access denied typedef exception NoSuchBookExc{string bookName;}; // no such notebook typedef exception NoSuchPageExc{string pageName;}; //no such page //each user must provide a user name and password short authorizeUser(in string userName, in string password) raises(AccessNotAuthorizedExc); //the list of existing notebooks is returned stringListType getBooks(in string userName, in string password) raises(AccessNotAuthorizedExc); //the list of existing notebook pages is returned stringListType getPages(in string bookName, in string userName, in string password) raises(AccessNotAuthorizedExc, NoSuchBookExc); //retrieves the contents of a notebook page string retrievePage(in string bookName, in string pageName, in string userName, in string password) raises(AccessNotAuthorizedExc, NoSuchPageExc, NoSuchBookExc); //saves the contents of a notebook page short savePage(in string bookName, in string pageName, in string pageContent, in string userName, in string password) raises(AccessNotAuthorizedExc, NoSuchBookExc); };
Note As a footnote, it has become a popular convention to arrive at the name of the class which implements an IDL interface by taking the name of the interface to be implemented and suffixing it with an abbreviation of the word "implementation." Using the Web-based notebook server as an example, this convention would result in an IDL interface named "Notebook" and an implementation class called "Notebook_impl". While this seems reasonable on the surface, the fact that this is an ORB-centric convention can present a problem as the server evolves through the development process. The problem arises when it becomes necessary to change the classes or services which are ORB-enabled (have IDL interfaces), or when it becomes necessary to alter the way in which classes implement the IDL interfaces. If a service that has not been previously ORB-enabled must become so, it will then be necessary to change the name of the class implementing that service to add the "impl" suffix. Worse yet, a change to the class's file name is also likely. A better convention is to suffix the name of the IDL interface with characters indicating that it is an
interface (such as IF or _if), and apply no suffix to the implementation class. In this example, the IDL interface is then "NotebookIF" while the implementation class name is simply "Notebook". This way, if you wish to support the implementation of the NotebookIF interface with another, pre-existing class, neither the Notebook class nor the additional implementation classes need have their names changed.
Tip When defining IDL functions use oneway where possible. CORBA provides the ability to classify IDL functions as oneway (as long as they do not have a return value or user defined exceptions). A oneway function results in a non-blocking call for the client process. The effect is that a client invoking a oneway call will continue processing immediately after the ORB call is made. There is not a wait for the called function to complete. The performance gains on the client side can be significant. There is also less likelihood for deadlock in the event that a server attempts to call back the client as part of its response to call from the client.
The client applet is a simplified Web-based Notebook allowing a user to create, store, retrieve, and display notebooks and notebook pages. The core of the applet is the free form drawing pallet on which the user types or draws whatever information is necessary. All persistent information about the authorized users, notebooks, and the contents of their pages is accessed and stored on the server's host. The services of the notebook server are invoked by the client applet, depicted in Figure 17.1, using ORB calls from the applet's host back to the server's host. The Java-enabled ORB product used for the examples in this chapter was OrbixWeb. It is important to recognize that OrbixWeb adheres to the security restrictions imposed on Java applets executing within Web browsers by only allowing ORB calls back to the host from which the applet was dynamically downloaded. As a consequence, the Notebook server must reside on the same machine as the Web server. This restriction has architectural ramifications which will be discussed later in the chapter. A side effect of the necessity to select a specific Java-enabled ORB product to create and compile the examples in this chapter is that some of the client-side syntax presented may be specific to the chosen ORB. However, most, if not all, of the points and concepts presented here will apply to all reasonably capable Java-enabled ORBs. Figure 17.1 : User Interface of the sample Notebook applet.
Figure 17.2 : Client-side proxy objects mediate between local client objects and the target server. When deploying the applet on your Web server, be sure to place all necessary ORB related files in their proper directory hierarchies. As the applet is loaded and the ORB-related classes are imported, the Java-enabled Web browser will look in the directories indicated by the standard Java "dot" notation. Listing 17.2 provides an example of this statement.
This statement tells the class loader to retrieve the file in IE/Iona/Orbix2/CORBA/SystemException.class relative to the CODEBASE of the applet. Loading these additional .class files from the Web server to support the necessary ORB functionality can consume a significant amount of time, many seconds in some cases. And because the Java loader is "lazy" (it does not load a class until its services are needed by the applet), the applet user may be surprised when there is a significant delay in response to a possibly minor input event. One reasonable solution is to force the loader to load all the necessary ORB-related class files when the applet is initialized. This can be accomplished by making a call from the applet's init() function to one or more functions in the ORB classes to be imported. It may also be effective to spawn a lower priority thread which performs some operations in the background resulting in the loading of the necessary ORB support classes.
NotebookIF.Ref notebookRef; public boolean bindObject () { String hostName = new String("xxx.xxx.xxx.xxx"); // host name of the target server String markerServer = new String(":notebookServer"); // name of the target server if(notebookRef!=null) // if the server object has not already been bound return true; else { // bind to the server object try {notebookRef = NotebookIF._bind(markerServer, hostName);} catch (SystemException sysExc) { showStatus("ORB Connect failed." + sysExc.toString ()); return false;} } showStatus("ORB Connect succeeded."); return true; }
The primary purpose of the bindObject() function here is to set the value of notebookRef. This is the reference to the object in our notebook server. Syntactically, this objectreference is used just as any other Java object reference is used. And, because your bindObject() function returns a boolean flag indicating whether notebookRef has been set, this function can be called prior to making any ORB call. In the event-handling function of the Notebook applet, the following function is called as part of the eventhandling process when you click the Save button.
Listing 17.4 CHAPT17LISTINGS.TXT-Client Applet Function to Save Note book Page public void savePage(String bookName, String pageName, String pageContent, String userName, String password) { if(bindObject()) //verify that the server object has been bound { try{notebookRef.savePage(bookName, pageName, pageContent, userName, password);} // make the ORB call catch(SystemException sysExc) { showStatus("ORB Call to savePage failed");return;} //...handle other exceptions ... showStatus("Notebook Page Saved"); // indicate that the page has been saved } else { showStatus("Error in ORB Connection."); } }
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch17.htm (5 of 19) [8/14/02 10:53:39 PM]
Similarly, in the event-handling function of the Notebook applet, the following function is called as part of the event-handling process when the Open Page button is pressed. The ability shown in Listing 17.5 to bind to a server-side ORB object and to use the resulting object reference to make a heterogeneous, interprocess ORB call is the fundamental functionality provided to a CORBA-enabled client. While most object request brokers provide a greate deal of functionality, you can use this basic "bind and call" functionality to perform most client-side operations
Listing 17.5 CHAPT17LISTINGS.TXT-Client Applet Function to Open a Notebook Page public void openPage(String pageName, String bookName, String userName, String password) { String content; if(bindObject()) //verify that the server object has been bound { try{content = notebookRef.retrievePage(bookName, pageName, userName, password);} // make the ORB call catch(SystemException exc) { showStatus("ORB Call to retrievePage failed");return;} //...handle other exceptions ... notepad.openPage(content); // open the Page on the Canvas } else { showStatus("Error in ORB Connection."); } }
Handling Exceptions
Not surprisingly, most Java IDL compilers generate exception classes which inherit from java.lang.Exception. It is this inheritance which enables any ORB exception to be handled in the same manner as any other Java exception. Recall that one of the functions in the NotebookIF IDL interface is the retrievePage() function that can generate three user-defined exceptions. The IDL shown in Listing 17.6 is a restatement of that function definition.
Listing 17.6 CHAPT17LISTINGS.TXT-IDL Definition of retrievePage Function with Exceptions string retrievePage(in string bookName, in string pageName, in string userName, in string password) raises(AccessNotAuthorizedExc, NoSuchPageExc,
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch17.htm (6 of 19) [8/14/02 10:53:39 PM]
NoSuchBookExc);
The IDL compiler generates the Java code shown in Listing 17.1 for the definition of this function, as shown in Listing 17.7.
Listing 17.7 CHAPT17LISTINGS.TXT-Java Code Generated from IDL Definition of savePage Function public String retrievePage(String bookName, String pageName, String userName, in string password) throws NotebookIF.AccessNotAuthorizedExc, NotebookIF.NoSuchPageExc, NotebookIF.NoSuchBookExc, IE.Iona.Orbix2.CORBA.SystemException;
Notice that the generated Java function definition has an additional exception defined, CORBA.SystemException. This is necessary due to the various CORBA-defined exceptions which may be thrown by an attempt to make a CORBA ORB call. For a complete list of these exceptions, refer to the documentation for your ORB and the CORBA specification. In order to pass compilation, your code must handle all potential exceptions. Therefore, the complete code for your client's openPage() function is shown in Listing 17.8.
Listing 17.8 CHAPT17LISTINGS.TXT-Client Applet openPage Function with all Exceptions Handled public void openPage(String pageName, String bookName, String userName, String password) { String content; if(bindObject()) //verify that the server object has been bound { try{content = notebookRef.retrievePage(bookName, pageName, userName, password);} // make the ORB call catch(NotebookIF.AccessNotAuthorizedExc noAccess) { showStatus("User access denied. Page Note Retrieved");return;} catch(NotebookIF.NoSuchPageExc noPage) { showStatus("No such notebook page: " + noPage.pageName");return;} catch(NotebookIF.NoSuchBookExc noBook) { showStatus("No such notebook: " + noBook.bookName");return;} catch(SystemException sysExc) { showStatus("ORB Call to retrievePage failed");return;} notepad.openPage(content); // open the Page on the Canvas
} }
If you have experience handling Java exceptions, the above exception-handling code will look very familiar. Each of the four possible exceptions is handled. For each exception, a status message is sent to the browser. Where the exception populates an exception attribute, that attribute is concatenated to the status message. Note As noted earlier in this chapter, it is advantageous to define IDL functions oneway (nonblocking). This is due to the performance gains resulting from the client processes' ability to continue processing immediately following an ORB call. An unfortunate side effect of defining one or more exceptions for an IDL function is that you then lose the option to make it oneway. An IDL function that may raise a user-defined exception cannot be nonblocking. Therefore, if client-side responsiveness is of particular importance to your application, it may be advantageous to define an exception callback IDL interface in your client, remove all exceptions from your server's IDL function definitions, and make them oneway. Then, code your server such that if the need arises to raise a user-defined exception, it calls the client's exception callback interface to asynchronously notify it of the problem.
CGI Programs, Java.net.*, and Java.io.* May Not Be the Best Choices
CGI programs have formed an invaluable function in bringing information and functionality to the Web. CGI programs, in concert with several of the Java.net classes (URL, URLConnection, DataInputStream, and DataOutputStream), are one of the primary mechanisms many Java developers use to communicate with a server. However, in many instances, an object-request broker provides a much more flexible and efficient solution to server-side connectivity than the combination of Java.net.*/Java.io.* and CGI. The advantages of CORBA over CGI and Java.net.* for server communication center around the simplicity of basic CORBAbased client/server interactions and the wide applicability of a CORBA-based server. This is in contrast to the cumbersome nature of CGI and Java.net.* client/server interactions and the limited applicability of a CGI-based server. More specifically, because the CGI protocol only supports input and output parameters by way of environment variables and standard input/output, all parameters must be packaged into and out of string form. Of course, CORBA has no such limitation. Parameters may be passed as any of the basic IDL types (short, float, string, sequence, and so on), or as any complex type defined in the server's interface definition. The CGI protocol does not inherently support the invocation of a specific function. As a result, the Web site designer must build and manage several CGI programs, each specifically designed either to perform a single function or write one or more multipurpose CGI programs. In the latter case, the invoked CGI program must parse the string-based input parameters passed to it in order to determine the desired function. CORBA allows a Web client to make a very specific function call to a very specific object in a server program using a very natural syntax. Additionally, most existing server applications were not written to support CGI access, and modifying a server application to support CGI access seems to be an unnecessarily narrow and cumbersome solution to the broader problem of supporting client interactions with a given server. On the other hand, many existing server applications already provide client access via a CORBA layer. But even where a server application is not CORBA-enabled, CORBA is a much more generic, extensible, and efficient solution to providing client/server access to data and functionality. The final benefit of CORBA over CGI and Java.net.* is that making a CORBA call in Java is simply less problematic than the corresponding Java.net.* calls. There is no need for the use of the URL, URLConnection, DataInputStream, and DataOutputStream classes.
Consider the example of passing a user name and password to a server program and getting back a list of notebook names to support your notebook applet. Listing 17.9 uses the Java.net.* and Java.io.* classes to establish a connection with a CGI program on the server, pass the user name and password to the CGI, and read the CGI output. Once the string containing the list of books is read from the input stream, the string must be unpacked (tokenized) to display each notebook name in a Java ListItem.
Listing 17.9 CHAPT17LISTINGS.TXT-Using Java.net.* and Java.io.* to Pass Data to and Read from a CGI String books; String userInfo = new String(userName + "|" + password); URLEncoder.encode(userInfo); try{ booksURL = new URL(this.getDocumentBase(),"CGIToGetListOfNotebooks"); conn = booksURL.openConnection(); conn.setUseCaches(false); outStream = new PrintStream(conn.getOutputStream()); outStream.println("string="+userInfo); outStream.close(); inData=new DataInputStream(conn.getInputStream()); books= new String(inData.readLine());inData.close(); } catch (MalformedURLException mExc) { System.err.println("MalformedURLException: " + mExc.toString()); } catch (IOException ioExc) { System.err.println("IOException: " + ioExc.toString()); if(books !=null) if(books.length()>0) { StringTokenizer tzr = new StringTokenizer(temps,"|"); while(tzr.hasMoreTokens()) bookList.addItem(tzr.nextToken(),-1); }
The same task accomplished in Listing 17.9 is accomplished in Listing 17.10 using an ORB call to a CORBA-based server. Differences of note are the comparative simplicity of establishing a server connection (just bind) and getting the result of the server call (the list of books is set as the return value of the ORB call).
Listing 17.10 CHAPT17LISTINGS.TXT-Code to Make an ORB Call and Get the Return Value StringListType books = null; //Define the ORB sequence of notebook names
try {notebookRef = NotebookIF._bind(markerServer, hostName);} catch (SystemException sysExc) { showStatus("ORB Connect failed. " + sysExc.toString ()); return true;} try{books = notebookRef.getNotebooks(userName,password);} //get the list of notebooks catch(SystemException sysExc) {
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch17.htm (9 of 19) [8/14/02 10:53:39 PM]
showStatus("ORB Call to getNotebooks failed"); return true;} //...handle other exceptions ... if(books != null) //verify that the ORB sequence has been set if(books.length>0) //verify that there is at least one notebook { for(int j = 0;j<books.length;j++) bookList.addItem(books[j],-1); //Add the notebook name to the ListItem }
Consider the example of reading a character string from the applet's server. The code segment shown in Listing 17.11 uses the Java.net.* and Java.io* classes to establish a connection with a preexisting file on the server and to read the file's content, a character string.
Listing 17.11 CHAPT17LISTINGS.TXT-Code Using Java.net* and Java.io* to Read from a File on the Server Host try{saveFile = new URL(this.getDocumentBase(),"docs/pagefile1");} catch (Exception exc) { showStatus("Error in URL creation."); return true;} try{conn = saveFile.openConnection();} catch (Exception exc) { showStatus("Error in URL connection.");return true;} conn.setUseCaches(false); try{inData=new DataInputStream(conn.getInputStream());} catch(Exception exc) { showStatus("Error getting input stream"); return true;} try{s= new String(inData.readLine());inData.close();} catch(Exception exc) { showStatus("Error reading data input stream"); return true;}
The code segment shown in Listing 17.12 uses an ORB call to a CORBA-based server to accomplish the same task.
Listing 17.12 CHAPT17LISTINGS.TXT-Code to Make an ORB Call to Read Data from the Server try {notebookRef = NotebookIF._bind(markerServer, hostName);} catch (SystemException sexc) { showStatus("ORB Connect failed. " + sexc.toString ()); return true;} try{s = notebookRef.retrievePage(encodedPageName,encodedBookName);} catch(SystemException exc) { showStatus("ORB Call to savePage failed"); return true;}
As you can see, even setting aside the code to define variables, significantly fewer lines of Java code are necessary to make the corresponding ORB call. And, more importantly, there are fewer points of failure. The above discussions and code segments highlight the advantages of writing Java applets as CORBA-based clients rather than clients based on Java.net.*, Java.io.*, and CGI. However, the intention here is not that a CORBA solution is always the best solution. It may be that the necessary server-side functionality is not sufficiently complex to warrant purchasing an ORB product and writing a CORBA server. Another meaningful consideration is a developer's exposure to CORBA technology. Development time is very valuable, and it may be that the time necessary to come up to speed on a given ORB is prohibitive given specific development goals and deadlines. A final consideration is firewall interoperability. It may be the case that an applet will be downloaded from a server to a client host residing behind the firewall set up by the client's organization. If this is the case, it is possible that the TCP/IP-based ORB connection attempts back to the originating host will raise a Java security exception. This results from the fact that the communication protocol your ORB uses may not support the ability to account for firewall proxies. On the other hand, Java.net.URLConnection does. Use of the Java.net.URLConnection to establish connections back to a host outside a firewall will have a greater likelihood of success.
Listing 17.13 CHAPT17LISTINGS.TXT-IDL Interfaces for Two Possible Workflow Simulation Servers // Simulation server called SimpleTrafficSimulator interface SimpleTrafficSimulator {
// Simulation server called SimpleHighwayCostCalculator interface SimpleHighwayCostCalculator { long determineConstructionCost(in short numberOfLanes, in short numberofHighwayMiles); long determineAnnualMaintenanceCost(in long constructionCost, in short numberofHighwayMiles, in short numberOfVehiclesPerMonth, in short averageWeatherCondition); };
To support the new workflow requirements of the Notebook applet, you will need to add two functions to the Notebook IDL interface: one to register a candidate simulation with the notebook server, and another to retrieve a list of these candidate simulations. This is the code for the IDL functions that support the new workflow requirement: oneway void registerSimulation(in string serverName); stringListType getAvailableSimulations(); Once the traffic simulator and highway cost simulations have been registered with the Notebook server through the registerSimulation() function call, the client applet may call the getAvailableSimulations() function to ask for all candidate simulations and display the simulation names to the user. Once the user has selected a simulation for inclusion in a workflow, the name of the selected simulation server is used as a parameter to the _bind() operation to obtain an object reference to the target server object. Providing the simulation name informs the server-side ORB daemon which server should be "bound," that is, which server process the client application wants to connect with (see Listing 17.14).
Listing 17.14 Binding to a Server Discovered at Runtime Object.Ref objectRef = null; String markerServer = new String(":" + userSelectedSimulationName); try {objectRef = Object._bind(markerServer, hostName);} catch (SystemException sysExc) { showStatus("ORB Connect failed. " + sysExc.toString ()); return true;}
Once the applet has a reference to the target object in the chosen simulation server, the interface repository can be queried to discover the list of operations supported by the interface, as well as the signature of each of the operations (see Listing 17.15).
// Get the complete interface definition try {interfaceRef = objRef._get_interface();} //... Handle any exceptions ... IE.Iona.Orbix2.InterfaceDef.FullInterfaceDescription entireInterface; try {entireInterface = interfaceRef.describe_interface();} // ...Handle any exceptions ...
The struct obtained from the call to Ref.describe_interface() includes all necessary information to construct and invoke an operation on the chosen interface. The struct is defined in Listing 17.16
Listing 17.16 Definition of CORBA's Interface Definition Struct struct FullInterfaceDescription { Identifier name; RepositoryId id; RepositoryId defined_in; OpDescriptionSeq operations; AttrDescriptionSeq attributes; };
As you can see, the interface description struct contains two sequences. The first is a sequence of structs, each of which describes an operation that you may invoke once you use the DII to build the call. This is actually a sequence of OperationDescription structs. For further details about the content of these structs, peruse the documentation for your ORB or the CORBA specification. But the content of the OperationDescription struct and its components tells you all you need to know in order to describe the interface of each invocable function to the workflow modeler. The modeler then selects the function to invoke and inputs the necessary function parameters. The DII is then used to dynamically build and invoke the function on the target server. Using these same mechanisms you can dynamically connect applications by obtaining the output parameters from a DII call and feeding them to another function in another server as defined by the user's workflow, again using the IR and DII. With your traffic simulator and highway cost calculator servers, it makes sense to model a workflow link between the SimpleTrafficSimulator::determineNumberOfLanes() function and the SimpleHighwayCostCalculator::determineConstructionCost() function. The results of a DII call to the first function will be captured and used as one of the DII input parameters to the second function. While the workflow example presents a compelling use for the IR and the DII, the Java code necessary to implement it is far too complex to present here. So the code segments presented later in the chapter use the IR and the DII to build and invoke one of the more simple operations in the NotebookIF IDL interface. Listing 17.17 is a restatement of the Notebook server's IDL interface.
Listing 17.17 Notebook Server IDL Interface interface NotebookIF { typedef sequence<string> stringListType; typedef exception AccessNotAuthorizedExc; typedef exception NoSuchBookExc{string bookName;}; typedef exception NoSuchPageExc{string pageName;};
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch17.htm (13 of 19) [8/14/02 10:53:39 PM]
short authorizeUser(in string userName, in string password) raises(AccessNotAuthorizedExc); stringListType getBooks(in string userName, in string password) raises(AccessNotAuthorizedExc); stringListType getPages(in string bookName, in string userName, in string password) raises(AccessNotAuthorizedExc, NoSuchBookExc); string retrievePage(in string bookName, in string pageName, in string userName, in string password) raises(AccessNotAuthorizedExc, NoSuchPageExc, NoSuchBookExc); short savePage(in string bookName, in string pageName, in string pageContent, in string userName, in string password) raises(AccessNotAuthorizedExc, NoSuchBookExc); };
As with the workflow example, create an object reference to the target server and use it to query the interface repository for the available operations and their corresponding arguments, as shown in Listing 17.18.
Listing 17.18 Binding to the Server and Querying Its Interface Repository NotebookIF.Ref notebookRef = null; String hostName = new String("xxx.xxx.xxx.xxx"); // host name of the target server String markerServer = new String(":notebookServer"); // name of the target server try {notebookRef = NotebookIF._bind(markerServer, hostName);} catch (SystemException sysExc) { showStatus("ORB Connect failed. " + sysExc.toString ()); return;} // Get the complete interface definition IE.Iona.Orbix2.InterfaceDef.Ref interfaceRef; try {interfaceRef = notebookRef._get_interface();} catch (SystemException sysExc) { showStatus("IR call to get interface failed. " + sysExc.toString ()); return;} IE.Iona.Orbix2.InterfaceDef.FullInterfaceDescription entireInterface; try {entireInterface = interfaceRef.describe_interface();} catch (SystemException sysExc) { showStatus("IR call to describe interface failed. " + sysExc.toString ()); return;}
In Listing 17.19, the operation and operation argument information is used to create and populate a CORBA "request" object. A request object houses the information necessary to communicate to the ORB what function should be invoked and the value of each of the function's input arguments.
Listing 17.19 Construct and Populate a CORBA Request Object Request req = null; String operationName = new String(entireInterface.attributes.buffer[0].name); try{req = notebookRef._request(operationName);} catch (SystemException sysExc) { showStatus("Add Request argument failed. " + sysExc.toString ()); return;} //Since we know here that the first operation is authorizeUser(string,string) //we can simply add the two arguments to the request without looking //at the content of the OperationDescription struct . try{(req.arguments().add(new Flags(_CORBA.ARG_IN)).value()).insertString(userName);} catch (SystemException sysExc) { showStatus("Add Request argument failed. " + sysExc.toString ()); return;} catch (IE.Iona.Orbix2.CORBA.CORBAException cExc) { showStatus("Add Request argument failed. " + cExc.toString ()); return;} try{(req.arguments().add(new Flags(_CORBA.ARG_IN)).value()).insertString(password);} catch (SystemException sysExc) { showStatus("Add Request argument failed. " + sysExc.toString ()); return;} catch (IE.Iona.Orbix2.CORBA.CORBAException cExc) { showStatus("Add Request argument failed. " + cExc.toString ()); return;} Listing 17.19 includes this line of code: try{(req.arguments().add(new Flags(_CORBA.ARG_IN)).value()).insertString(password);}
This deserves a bit of explanation. Clearly, it could be broken down into many more lines of function calls and attribute definitions, but as a single line of code, it very succinctly adds an argument value to the DII request object. Broken down, this line of code gets the list of arguments from the request object, adds a new in argument to the list of arguments, gets the value object for the new argument, and finally, sets the argument value with a string containing the password entered by the user. Once the parameters are inserted, the request can be invoked using the DII's invoke function, as shown in Listing 17.20.
Listing 17.20 Invoke the request Object try{req.invoke();} catch (SystemException sysExc) { showStatus("Attempt to invoke Request failed. " + sysExc.toString ()); return;} catch(IE.Iona.Orbix2.CORBA.CORBAException cExc) { showStatus("Attempt to invoke Request failed. " + cExc.toString ()); return;}
And very simply, the return value is retrieved by calling it, as shown in Listing 17.21.
Listing 17.21 Extract the Returned NamedValue Object from the request Object
NamedValue returnValue = null; try{returnValue = req.result();} catch (SystemException sysExc) { showStatus("Attempt to extract Request result failed. " + sysExc.toString ()); return;}
However, the return value is of type NamedValue. This is a CORBA type which contains an optional name and a value. But to further complicate the matter, the value is of type Any. Any is a CORBA type which is comprised of a type indicator and the value itself. In Listing 17.22, you know that the return value of the function authorizeUser(...) is a short.
Listing 17.22 Extract the return Value from the Returned NamedValue Object short authorizationResult = 0; try{authorizationResult = (returnValue.value()).extractShort();} catch (SystemException sysExc) { showStatus("Attempt to extract Request return value failed. " + sysExc.toString ()); return;}
Clearly, the process of creating, invoking, and getting the result(s) of a DII call is significantly more involved than making the same call using the SII (Static Invocation Interface). Given this fact, you would probably want to use the DII only when absolutely necessary. However, as the workflow example suggests, it can come in handy when runtime program discovery and interaction are functional requirements.
Using Filters
For security purposes, each function in your NotebookIF IDL interface requires that a user name and password be included as two of the function parameters. But suppose that the need for this information was just an afterthought, and that your original interface definition did not require these parameters for every call. Depending on the number and complexity of the clients dependent on your server, it could be problematic to recode each client applet and the server functions to provide and accept the user name and password with every call. The functionality provided by filters can simplify this problem (not all available ORBs provide filter-type functionality). The idea behind filters is that they intercept outgoing and incoming ORB calls at various points in the ORB's request marshalling and unmarshalling process. At each of these interception points, data can be added to or removed from the ORB request. There are various imaginable uses for the utility of filters (encryption, bookkeeping, and so on), but, for your notebook applet and server, the new necessity to verify access on each ORB call can be addressed using a client-side and server-side filter without requiring a change to any preexisting client or server code. What your client-side filter will need to do is piggyback each outgoing ORB call with a user name and password. A server-side filter will then be written to extract them and assess authorization, raising a system exception if authorization does not succeed. Your Java-enabled ORB supports filtering functionality by enabling the implementation of a user-defined filter class. This class must inherit from the ORB's built-in filter class. The point in the marshalling process where your authorization data is added to the ORB request is dictated by the filter function which you choose to override in the filter class (see Figure 17.3). In the filter class defined in Listing 17.23, the user name and passwords are added to any outgoing ORB request prior to the marshalling and creation of the outgoing request packet. Because the outgoing request object is passed to the filtering functions, the functionality of the DII::Request class, as described earlier in this chapter, can be used to add the user name and passwords to the outgoing ORB
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch17.htm (16 of 19) [8/14/02 10:53:39 PM]
call. Figure 17.3 : User defined filters enable examination and modification of function parameter values during the marshalling and unmarshalling of ORB function calls.
Listing 17.23 Implementation of PiggybackFilter Class import IE.Iona.Orbix2.CORBA.SystemException; import IE.Iona.Orbix2.CORBA.Request; import java.io.*; public class PiggybackFilter extends IE.Iona.Orbix2.CORBA.Filter { public boolean outRequestPreMarshal(Request request) { try{request.insertString(userName);} catch(IE.Iona.Orbix2.CORBA.SystemException ex) {System.out.println("Outgoing filter failure"); return false;} try{request.insertString(password);} catch(IE.Iona.Orbix2.CORBA.SystemException ex) {System.out.println("Outgoing filter failure"); return false;} return true; } };
To register the filter object with the client ORB, the filter's constructor should be called prior to the first ORB call.
Figure 17.5 is an example of an application architecture which works with, but around, the inability to access only a single host from a Java/ORB-based applet. This architecture is applicable when there is a need for client applets to request the services of multiple ORBbased servers residing on multiple hosts. The primary difference with this architecture is the existence of an application proxy server. This ORB-enabled server is called by all client applets for any server request. The parameters sent with each client request are examined by the application proxy to determine which host and server it should forward the call to. It then forwards the call to the target host and server, returning any output parameters to the originating client. Figure 17.5 : Distributed architecture with an applications proxy server to indirectly support multi-host applet connectivity while adhering to the Java applet client/server connectivity restrictions. There are a few important ramifications of this architecture. As discussed earlier in this chapter, IDL interfaces can be very finegrained and specific or more coarse and general purpose. The notebook server example has a very specific IDL interface. Because the notebook server is rather simplistic and because the server's client applet is likely to be the only client application, having a less generic interface is not likely to present a problem. In contrast, however, the application proxy server in Figure 17.5 must be able to forward function calls to multiple-target applications and potentially support many different clients. So it is preferable that its IDL interface be very generic and inherently extensible. It would not be good if each introduction of a new target application resulted in the need to significantly change the IDL interface and the implementation code of the application proxy. Given this, the application proxy server, in its simplest form, could have a single function capable of handling a call targeted for any function in any of the target applications. The IDL definition of this function could take the form shown in Listing 17.24.
Listing 17.24 IDL Definition of a Generic Application proxy Function NVPairListType performOperation(in in in in string targetApp, string targetInterface, string targetFunction, NVPairListType inParameters);
Using the proxy function, the client provides the names of the target application (a CORBA-enabled server), IDL interface, and function to indicate where the application proxy should forward the call. Any input and output parameters are provided and returned using a list of name/value pairs. There are several variations on the specific signature of this function, but the intent is always the same: To provide a single generic interface to one or more specific services in support of client/server extensibility. As you have probably ascertained, an additional ramification of this architecture is the requirement that client applets be able to deal with the generic nature of the application proxy interface. More specifically, each client ORB call requires creation and population of a name/value pair list, and examination of the returned name/value pair list. While this process can be simplified using the various DII and IR facilities of the ORB, it is an unfortunate reality of generic IDL interfaces. The advantages of loose coupling of clients and servers do not come without a price. It is worth pointing out that your application proxy server is just one example of the need to decouple client and servers by defining generic IDL interfaces. This architectural technique is not specific to the marriage of Java applets and CORBA servers. Many existing distributed systems were built on this very paradigm. There are certainly other architectural possibilities which may provide a more appropriate solution to a given problem. For example, a downfall of both previously described architectures is the lack of scalability in the face of high client demand on the servers. In both cases, there is a single host supporting the throughput demands of all clients, a classic shortcoming of 2-tier client/server architectures. As illustrated in Figure 17.6, one architectural solution is to create a 3-tier architecture by pushing the server's persistent data store to a commonly accessible host and establishing two or more server hosts, each having resident Web-server and ORB-server applications. Figure 17.6 : A distributed architecture with three tiers and multiple ORB server processes can support greater scalability. The complexity to manage with this solution is concurrent data access attempts fromthe now multiple ORB servers. However, most of
the more capable object and relational database products provide mechanisms to support multiple concurrent clients. The notebook server referred to in this chapter, for example, uses an object-oriented database for its persistent storage mechanism. It is certainly feasible to implement the notebook server such that it utilizes the distribution, transaction, and locking mechanisms provided by the OODBMS to support multiple notebook servers accessing the single persistent store of notebook information.
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f17-1.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f17-2.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f17-3.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f17-4.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f17-5.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f17-6.gif
CONTENTS
G G
G G
What Is CORBA? Sun's IDL to Java Mapping H IDL Modules H IDL Constants H IDL Data Types H Enumerated Types Structures H Unions H Sequences and Arrays H Exceptions H Interfaces H Attributes Using CORBA in Applets H Choosing Between CORBA and RMI Creating CORBA Clients with JavaIDL Creating CORBA Clients with VisiBroker
What Is CORBA?
In Chapter 17, "Creating CORBA Clients," you learned how to create a CORBA client using the OrbixWeb client. If this is your first exposure to CORBA, you may not realize what CORBA actually is. The Common Object Request Broker Architecture (CORBA) is a tremendous vision of distributed objects interacting without regard to their location or operating environment. CORBA is still in its infancy, with some standards still in the definition stage, but the bulk of the CORBA infrastructure is defined. Many software vendors are still working on some of the features that have been defined. CORBA consists of several layers. The lowest layer is the Object Request Broker, or ORB. The ORB is essentially a remote method invocation facility. The ORB is language-neutral, meaning you can create objects in any language and use the ORB to invoke methods in those objects. You can also use any language to create clients that invoke remote methods through the ORB. There is a catch to the "any language" idea. You have to define a language mapping between the implementation language and CORBA's Interface Definition Language (IDL). When you go from IDL to your implementation language, you generate a stub and a skeleton in the implementation language. The stub is the interface between the client and the ORB; the skeleton is the interface between the ORB and the object (or server). Figure
18.1 shows the relationship between the ORB, an object, and a client wishing to invoke a method on the object. Figure 18.1 : COBRA clients use the ORB to invoke methods on a COBRA server. While the ORB is drawn conceptually as a separate part of the architecture, it is often just part of the application. A basic ORB implementation might include a naming service (see the following discussion) and a set of libraries that facilitate communications between clients and servers. Once a client locates a server, it communicates directly with that server, not going through any intermediate program. This permits efficient CORBA implementations. The ORB is both the most visible portion of CORBA and the least exciting. CORBA's big benefit comes in all the services that it defines. Among the services defined in CORBA are
G G G G G G G
These services are a subset of the full range of services defined by CORBA. The Lifecycle and the Naming services crystallize Sun's visionary phrase, "The network is the computer." These services allow you to instantiate (create) new objects without knowing where the objects reside. You might be creating an object in your own program space, or you might be creating an object halfway around the world, and your program will never know it. The Lifecycle service enables you to create, delete, copy, and move objects on a specific system. As an application programmer, you would prefer not to know where an object resides. As a systems programmer, you need the Lifecycle service to implement this location transparency for the application programmer. One of the hassles you frequently run into in remote procedure call systems is that the server you are calling must already be up and running before you can make the call. The Lifecycle service removes that hassle; you can create an object, if you need to, before invoking a method on it. The Naming service enables you to locate an object on the network by name. You want the total flexibility of being able to move objects around the network without having to change any code. The Naming service gives you that ability by associating an object with a name instead of a network address. The Persistence service lets you save objects somewhere and retrieve them later. This might be in a file, or it might be on an object database. The CORBA standard doesn't specify which. That is left up to the individual software vendors. The Event service is a messaging system that allows more complex interaction than a simple message call. You could use the Event service to implement a network-based Observer-Observable model, for instance. There are event suppliers that send events, and event consumers that receive them. A server or a client is either push or pull. A push server sends events out when it wants to (it pushes them out), while a push client has a push method and automatically receives events through this method. A pull server doesn't send out events until it is asked; you have to pull them out of the server. A pull client does not receive events until it asks for them. It might help to use the term poll in place of pull. A pull server doesn't deliver events on its own; it gives them out when it is polled. A pull client goes out and polls for events. The Transaction service is one of the most complex services in the CORBA architecture. It enables you to define operations across multiple objects as a single transaction. This kind of transaction is similar to a database transaction. It handles concurrency, locking, and even rollbacks in case of a failure. A transaction must comply with a core set of requirements that are abbreviated ACID:
G
Atomicity A transaction is a single event. Everything in the transaction is either done as a whole or undone. You don't perform a transaction partially. Consistency When you perform a transaction, you do not leave the system in an inconsistent state. For example, if you have
an airline flight with one seat left, you don't end up assigning that seat to two different people if their transactions occur at the same time. Isolation No other objects see the results of a transaction until that transaction is committed. Even if transactions are executing simultaneously, they have a sequential order with respect to the data. Durability If you commit a transaction, you can be sure that the change has been made and stored somewhere. It doesn't get lost.
The transaction service usually relies on an external Transaction Processing (TP) system. The Object Querying service lets you locate objects based on something other than name. For instance, you could locate all ships registered in Liberia, or all Krispy Kreme donut locations in Georgia. This feature is usually used when your objects are stored in an object database. The Properties service lets objects store information on other objects. A property is like a sticky-note. An object writes some information down on a sticky-note and slaps it on another object. This has tremendous potential because it lets information be associated with an object without the object having to know about it. The beauty of the whole CORBA system is that all of these services are available through the ORB interface, so once your program can talk to the ORB, you have these services available. Of course, your ORB vendor may not have implemented all of these services yet.
IDL Modules
A module is the IDL equivalent of the Java package. It groups sets of interfaces together in their own namespace. Like Java packages, IDL modules can be nested. Here is an example IDL module definition (shown without any definitions, which are discussed later in this chapter): module MyModule { // insert your IDL definitions here, you must have at least // one definition for a valid IDL module }; This module would be generated in Java as a package called MyModule: package MyModule; When you nest modules, the Java packages you generate are also nested. For example, consider the following nested module definition: module foo { module bar {
Tip Don't forget to put a semicolon (;) after the closing brace (}) of a module definition. Unlike Java, C, and C++, you are required to put a semicolon after the brace in IDL.
The Java package definition for interfaces within the baz module is package foo.bar.baz;
IDL Constants
As in Java, you can define constant values in IDL. The format of an IDL constant definition is const type variable = value; The type of a constant is limited to boolean, char, short, unsigned short, long, unsigned long, float, double, and string. Constants are mapped into Java in an unusual way. Each constant is defined as a class with a single, static, final, public variable, called value, that holds the value of the constant. This is done because IDL lets you define constants within a module, but Java requires that constants belong to a class. Here is an example IDL constant definition: module ConstExample { const long myConstant = 123; }; This IDL definition produces the following Java definition: package ConstExample; public final class myConstant { public static final int value = (int) (123L); }
The IDL equivalent of the Java byte data type is the octet. IDL supports the String type, but it is called string. Characters in IDL can only have values between 0 and 255. The JavaIDL system checks your characters to make sure they fall within this range, including characters stored in strings. IDL supports 16, 32, and 64-bit integers, but the names for the 32 and 64-bit types are slightly different. In IDL, the 32-bit
value is called a long, while in Java it is called an int. The IDL equivalent of the Java long is the long long. IDL supports unsigned short, int, and long values. In Java, these values are stored in signed variables. You must be very careful when dealing with large unsigned values, since they may end up negative when represented in Java.
Enumerated Types
Unlike Java, IDL lets you create enumerated types that represent integer values. The JavaIDL system turns the enumerated type into a class with public, static, final values. Here is an example IDL enumerated type: module EnumModule { enum Medals { gold, silver, bronze }; }; This definition produces the Java class shown in Listing 18.1:
Listing 18.1 Java Definition of Enumerated Types package EnumModule; public class Medals { public static final int gold = 0, silver = 1, bronze = 2; public static final int narrow(int i) throws sunw.corba.EnumerationRangeException { if (gold <= i && i <= bronze) { return i; } throw new sunw.corba.EnumerationRangeException(); } }
Since you can also declare variables of an enumerated type, JavaIDL creates a holder class that is used in place of the data type. The holder class contains a single instance variable called value that holds the enumerated value. The holder for the Medals enumeration looks like the definition in Listing 18.2:
Listing 18.2 Java Definition of Holder Class for Enumerated Types package EnumModule; public class MedalsHolder { // instance variable public int value; // constructors public MedalsHolder() { this(0); }
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch18.htm (5 of 15) [8/14/02 10:53:51 PM]
You can create a MedalsHolder by passing an enumerated value to the constructor: MedalsHolder medal = new MedalsHolder(Medals.silver); The narrow method performs range checking on values and throws an exception if the argument is outside the bounds of the enumeration. It returns the value passed to it, so you can use it to perform passive bounds checking. For example: int x = Medals.narrow(y); assigns y to x only if y is in the range of enumerated values for Medals, otherwise it throws an exception.
Structures
An IDL struct is like a Java class without methods. In fact, JavaIDL converts an IDL struct into a Java class whose only methods are a null constructor and a constructor that takes all the structure's attributes. Here is an example IDL struct definition: module StructModule { struct Person { string name; long age; }; }; This definition produces the Java class declaration shown in Listing 18.3 (with some JavaIDL-specific methods omitted):
Listing 18.3 Java Definition of IDL Struct package StructModule; public final class Person { // instance variables public String name; public int age; // constructors public Person() { } public Person(String __name, int __age) { name = __name; age = __age; } }
Like the enumerated type, a struct also produces a holder class that represents the structure. The holder class contains a single instance variable called value. Listing 18.4 shows the holder for the Person structure:
Listing 18.4 Java Definition of Holder Class for IDL Struct package StructModule; public final class PersonHolder { // instance variable public StructModule.Person value; // constructors public PersonHolder() { this(null); } public PersonHolder(StructModule.Person __arg) { value = __arg; } }
Unions
The union is another C construct that didn't survive the transition to Java. The IDL union actually works more like the variant record in Pascal, since it requires a discriminator value. An IDL union is essentially a group of attributes, only one of which can be active at a time. The discriminator indicates which attribute is in use at the current time. A short example should make this a little clearer. Listing 18.5 shows an IDL union declaration:
Listing 18.5 An IDL Union Declaration module UnionModule { union MyUnion switch (char) { case 'a': string aValue; case 'b': long bValue; case 'c': boolean cValue; default: string defValue; }; };
The character value in the switch, known as the discriminator, indicates which of the three variables in the union is active. If the discriminator is 'a', the aValue variable is active. Because Java doesn't have unions, a union is turned into a class with accessor methods for the different variables and a variable for the discriminator. The class is fairly complex. Listing 18.6 shows a subset of the definition for the MyUnion union:
Listing 18.6 Subset of Java Definition for an IDL Union package UnionModule; public class MyUnion { // constructor public MyUnion() { // only has a null constructor } // discriminator accessor public char discriminator() throws sunw.corba.UnionDiscriminantException { // returns the value of the discriminator } // branch constructors and get and set accessors public static MyUnion createaValue(String value) { // creates a MyUnion with a discriminator of 'a' } public String getaValue() throws sunw.corba.UnionDiscriminantException { // returns the value of aValue (only if the discriminator // is 'a' right now) } public void setaValue(String value) { // sets the value of aValue and set the // discriminator to 'a' } public void setdefValue(char discriminator, String value) throws sunw.corba.UnionDiscriminantException { // // // // } } Sets the value of defValue and sets the discriminator. Although every variable has a method in this form, it is only useful when you have a default value in the union.
The holder structure should be a familiar theme to you by now. JavaIDL generates a holder structure for a union. The holder structure for MyUnion would be called MyUnionHolder and would contain a single instance variable called value.
sequence <boolean> unboundedBools; sequence <char, 15> boundedChars; }; }; The arrays are defined in Java as: public int[] longArray; public boolean[] unboundedBools;
public char[] boundedChars;
Exceptions
CORBA has the notion of exceptions. Unlike Java, however, exceptions are not just a type of object, they are separate entities. IDL exceptions cannot inherit from other exceptions. Otherwise, they work like Java exceptions and may contain instance variables. Here is an example IDL exception definition: module ExceptionModule { exception YikesError { string info; }; }; This definition creates the Java file shown in Listing 18.7 (with some JavaIDL-specific methods removed):
Listing 18.7 Java Definition of IDL Exception package ExceptionModule; public class YikesError extends sunw.corba.UserException { // instance variables public String info; // constructors public YikesError() { super("IDL:ExceptionModule/YikesError:1.0"); } public YikesError(String __info) { super("IDL:ExceptionModule/YikesError:1.0"); info = __info; } }
Interfaces
Interfaces are the most important part of IDL. An IDL interface contains a set of method definitions, just like a Java interface. Like Java interfaces, an IDL interface can inherit from other interfaces. Here is a sample IDL interface definition:
module InterfaceModule { interface MyInterface { void myMethod(in long param1); }; }; IDL classifies method parameters as being either in, out, or inout. An in parameter is identical to a Java parameter; it is a parameter passed by value. Even though the method can change the value of the variable, the changes are discarded when the method returns. An out variable is an output-only variable. The method is expected to set the value of this variable, which is preserved when the method returns, but no value is passed in for the variable (it is uninitialized). An inout variable is a combination of the two; you pass in a value to the method. If the method changes the value, the change is preserved when the method returns. The fact that Java parameters are in-only poses a small challenge when mapping IDL to Java. Sun has come up with a reasonable approach, however. For any out or inout parameters, you pass in a holder class for that variable. The CORBA method can then set the value instance variable with the value that is supposed to be returned.
Attributes
IDL lets you define variables within an interface. These translate into get and set methods for the attribute. An attribute can be specified as readonly, which prevents the generation of a set method for the attribute. For example, if you defined an IDL attribute as attribute long myAttribute; your Java interface would then contain the following methods: int getmyAttribute() throws omg.corba.SystemException; void setmyAttribute() throws omg.corba.SystemException;
Listing 18.8 Source Code for Banking.idl module banking { enum AccountType { CHECKING, SAVINGS }; struct AccountInfo {
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch18.htm (11 of 15) [8/14/02 10:53:51 PM]
string id; string password; AccountType which; }; exception InvalidAccountException { AccountInfo account; }; exception InsufficientFundsException { }; interface Banking { long getBalance(in AccountInfo account) raises (InvalidAccountException); void withdraw(in AccountInfo account, in long amount) raises (InvalidAccountException, InsufficientFundsException); void deposit(in AccountInfo account, in long amount) raises (InvalidAccountException); void transfer(in AccountInfo fromAccount, in AccountInfo toAccount, in long amount) raises (InvalidAccountException, InsufficientFundsException); }; };
You create a reference to a stub for the banking interface with the following line: BankingRef bank = BankingStub.createRef(); Next, you must create a connection between the stub and a CORBA server by resolving it. Since JavaIDL is meant to be the standard Java interface for all ORBs, it requires an ORB-independent naming scheme. Sun decided on a URL-type naming scheme of the format idl:orb_name://orb_parameters The early versions of JavaIDL shipped with an ORB called the Door ORB, which is a very lightweight ORB containing little more than a naming scheme. To access a CORBA object using the Door ORB, you must specify the host name and port number used by the CORBA server you are connecting to and the name of the object you are accessing. The format of this information is hostname:port/object_name If you want to access an object, named Bank, via the Door ORB, running on a server at port 5150 on the local host, you resolve your stub this way: sunw.corba.Orb.resolve( "idl:sunw.door://localhost:5150/Bank",
bank); Remember that the bank parameter is the BankingRef returned by the BankingStub.createRef method. Once the stub is resolved, you can invoke remote methods in the server using the stub. Listing 18.9 shows the full JavaIDL client for the banking interface. As you can see, once you have connected the stub to the server, you can invoke methods on the stub just like it was a local object.
Listing 18.9 Source Code for BankingClient.java import banking.*; // This program tries out some of the methods in the BankingImpl // remote object. public class BankingClient { public static void main(String args[]) { // Create an Account object for the account we are going to access. Account myAccount = new Account( "AA1234", "1017", AccountType.CHECKING); AccountInfo myAccountInfo = myAccount.toAccountInfo(); try { // Get a stub for the BankingImpl object BankingRef bank = BankingStub.createRef(); sunw.corba.Orb.resolve( "idl:sunw.door://localhost:5150/Bank", bank); // Check the initial balance System.out.println("My balance is: "+ bank.getBalance(myAccountInfo)); // Deposit some money bank.deposit(myAccountInfo, 50000); // Check the balance again System.out.println("Deposited $500.00, balance is: "+ bank.getBalance(myAccountInfo)); // Withdraw some money bank.withdraw(myAccountInfo, 25000); // Check the balance again System.out.println("Withdrew $250.00, balance is: "+ bank.getBalance(myAccountInfo));
The only difference between the two ORBs on the client side is in the way you connect a stub to a server. Under VisiBroker, you must first initialize the ORB with the following line: CORBA.ORB orb = CORBA.ORB.init(); You only need to initialize the ORB once, no matter how many stubs you create. Next, you connect the stub to the server using the bind method. For example, to connect your stub to an object named Bank, you would use the following call: Banking bank = Banking_var.bind("Bank");
Tip You'll often see the terms bind and resolve used in network programming. Bind refers to attaching two things together, like you bind a port number to a socket, meaning you assign that port number to the socket. In the VisiBroker context, you bind a stub to a server object, meaning you connect them together. Resolve is another word for lookup. When you resolve a name, you find the object the name refers to. When you resolve a stub, you find the named object the stub should be connected to.
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f18-1.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f18-2.gif
CONTENTS
G
G G
Creating a Basic CORBA Server H Using Classes Defined by IDL Structs H VisiBroker Skeletons H Using the VisiBroker TIE Interface H JavaIDL Skeletons Creating Callbacks in CORBA Wrapping CORBA Around an Existing Object H Mapping to and from CORBA-Defined Types H Creating Remote Method Wrappers H Implementing Wrapped Callbacks
ACORBA server has objects whose methods are invoked remotely by its clients. A single server can have any number of objects, and can activate and deactivate objects at any time. An active object is visible to the clients, whereas an inactive object cannot be accessed by clients.
Listing 19.1 Source Code for Banking.idl module banking { enum AccountType { CHECKING, SAVINGS }; struct AccountInfo { string id; string password; AccountType which; }; exception InvalidAccountException { AccountInfo account; }; exception InsufficientFundsException { }; interface Banking { long getBalance(in AccountInfo account) raises (InvalidAccountException); void withdraw(in AccountInfo account, in long amount) raises (InvalidAccountException, InsufficientFundsException); void deposit(in AccountInfo account, in long amount) raises (InvalidAccountException); void transfer(in AccountInfo fromAccount, in AccountInfo toAccount, in long amount) raises (InvalidAccountException, InsufficientFundsException); }; };
If you want to add custom methods to these structs, you have to create a separate class and define methods to convert from one class to the other. In Chapter 16, "Creating 3-Tier Distributed Applications with RMI," the RMI-based banking application defines an Account class that has its own hashCode and equals methods. This allows it to be stored in a hash table. The following code shows the two methods that need to be added to the Account class to convert to and from AccountInfo objects. // Allow this object to be created from an AccountInfo instance public Account(AccountInfo acct) { this.id = acct.id; this.password = acct.password; this.which = acct.which; } // Convert this object to an AccountInfo instance public AccountInfo toAccountInfo() { return new AccountInfo(id, password, which); }
VisiBroker Skeletons
VisiBroker implements skeletons in a traditional way. When you generate your skeletons and stubs for an IDL module, it generates an abstract skeleton class. This class has all the code to communicate with the ORB and to invoke the remote methods. It leaves the remote methods themselves as abstract methods. When you use the remote methods, you create a subclass of the skeleton class and use those abstract methods. The name of the skeleton class generated for an IDL interface, at least for VisiBroker, is the name of the interface, prefaced by _sk_. Every object that uses a CORBA interface in VisiBroker must identify itself by a name. The constructor for the skeleton class sends this name to the ORB itself. When you create a subclass of the skeleton, you must create a constructor that passes a name up to the superclass, for example: public class BankingImpl extends _sk_Banking { public BankingImpl(String bankingObjectName) {
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch19.htm (3 of 16) [8/14/02 10:53:57 PM]
super(bankingObjectName); // other initialization code here } The implementation of the skeleton methods is straightforward. In fact, you can use the implementation of the BankingImpl class from Chapter 16, changing only the java.rmi.RemoteException exceptions to CORBA.SystemException. The following code fragment is an example of one of the methods from the BankingImpl for VisiBroker: // getBalance returns the amount of money in the account (in cents). // If the account is invalid, it throws an InvalidAccountException public int getBalance(AccountInfo accountInfo) throws CORBA.SystemException, InvalidAccountException { // Fetch the account from the table Integer balance = (Integer) accountTable.get( new Account(accountInfo)); // If the account wasn't there, throw an exception if (balance == null) { throw new InvalidAccountException(accountInfo); } // Return the account's balance return balance.intValue(); } Once you create an object that uses a remote interface, you need a program that creates instances of these objects so the clients can use them. To do this, you must first initialize the VisiBroker ORB with the following line: CORBA.ORB orb = CORBA.ORB.init(); Next, you must create an instance of the Basic Object Adapter, or BOA. The BOA is a standard CORBA object used to communicate with the ORB. It was intended for systems where the ORB and the server are separate programs or even on separate machines. The BOA has methods for activating and deactivating objects, and for activating and deactivating the entire server. You create an instance of a BOA with the following line: CORBA.BOA boa = orb.BOA_init(); Next, create an instance of your implementation object. In this case, the implementation object is the BankingImpl object.
While you are instantiating a BankingImpl object, you want to refer to it as an instance of the Banking object, as far as the BOA is concerned, so you should store the new object in a variable of type Banking. Also, when you create the implementation, you must give it the name that the other clients will use to access it. In this case, the name is Bank. You create the instance with the following line: Banking banking = new BankingImpl("Bank"); Even though you have created an implementation object, the ORB still does not know about it. You must identify the object to the BOA by calling the obj_is_ready method: boa.obj_is_ready(banking); If you have more than one implementation object, you should identify them all to the BOA at this point. Once all the objects have been identified, call the BOA's impl_is_ready to tell the ORB that everything is ready to go: boa.impl_is_ready(); When you have called impl_is_ready, your server is ready to go, and clients can begin connecting to it. Listing 19.2 shows a startup object that initializes the ORB, creates an instance of the BankingImpl class, and activates it using the BOA.
Listing 19.2 Source Code for BankingServer.java package banking; // This class creates a BankingImpl object and activates // it through the BOA (Basic Object Adaptor). public class BankingServer { public static void main(String[] args) { try { // Initialize the ORB CORBA.ORB orb = CORBA.ORB.init(); // Create a BOA CORBA.BOA boa = orb.BOA_init(); // Create the banking service implementation. Clients will // request access to the object by the name "Bank".
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch19.htm (5 of 16) [8/14/02 10:53:57 PM]
Banking banking = new BankingImpl("Bank"); // Tell the BOA about the banking object boa.obj_is_ready(banking); // Activate all the objects in the BOA boa.impl_is_ready(); } catch (Exception e) { System.out.println("Got exception: "+e); e.printStackTrace(); } } }
words, the TIE skeleton class works like a pass-thru. Figure 19.2 illustrates the relationship between a TIE skeleton and an object using the BankingOperations interface. Figure 19.2 : The TIE skeleton invokes implementation methods in another object. When you create an implementation object for a TIE interface, it does not have to be a subclass of the skeleton. In fact, it shouldn't be. Instead, it must use an Operations interface (BankingOperations, for example). The only link that the implementation has to CORBA is that each method in the Operations interface can throw the CORBA.SystemException exception. Unlike the regular skeleton implementation, the constructor for the TIE implementation object does not have to accept the object's name. You can use the empty constructor if you like. The only difference in start-up between the regular skeleton implementation and the TIE implementation is that for the TIE implementation, you first create the TIE implementation object, and then you create an instance of the TIE skeleton, passing it the implementation object. The following code fragment creates a TIE implementation of the Banking interface, followed by the TIE skeleton for that interface: BankingTieImpl impl = new BankingTieImpl(); Banking banking = new _tie_Banking(impl, "Bank"); As you can see, the object's name is passed to the constructor for the TIE skeleton, not to the TIE implementation.
JavaIDL Skeletons
JavaIDL doesn't even bother with an abstract skeleton class. Instead, it always generates a TIE interface for every class, although the JavaIDL specification never uses the term TIE. Like VisiBroker, JavaIDL creates an Operations interface that has Java versions of the methods defined in the IDL interface. The only difference between the interface created by JavaIDL and the interface created by VisiBroker is the name of the CORBA system exception. JavaIDL calls it sunw.corba.SystemException. The implementation object you create does not use the Operations interface directly. Instead, it uses the Servant interface, which extends the Operations interface. For example, your implementation for the Banking interface might be declared as public class BankingImpl implements BankingServant Although JavaIDL is intended to be Sun's recommendation for mapping IDL into Java, it was released with a lightweight ORB called the Door ORB. This ORB provides just enough functionality to get clients and servers
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch19.htm (7 of 16) [8/14/02 10:53:57 PM]
talking to each other, but not much more. Depending on the ORB, the initialization varies, as does the activation of the objects. For the Door ORB distributed with JavaIDL, you initialize the ORB with the following line: sunw.door.Orb.initialize(servicePort); The servicePort parameter you pass to the ORB is the port number it should use when listening for incoming clients. It must be an integer value. Your clients must use this port number when connecting to your server. After you initialize the orb, you can instantiate your implementation object. For example: BankingImpl impl = new BankingImpl(); Next, you create the skeleton, passing it the implementation object: BankingRef server = BankingSkeleton.createRef(impl); Finally, you activate the server by publishing the name of the object: sunw.door.Orb.publish("Bank", server); Listing 19.3 shows the complete JavaIDL startup program for the banking server.
Listing 19.3 Source Code for BankingServer.java package banking; public class BankingServer { // Define the port that clients will use to connect to this server public static final int servicePort = 5150; public static void main(String[] args) { // Initialize the orb sunw.door.Orb.initialize(servicePort); try { BankingImpl impl = new BankingImpl(); // Create the server BankingRef server = BankingSkeleton.createRef(impl);
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch19.htm (8 of 16) [8/14/02 10:53:57 PM]
// Register the object with the naming service as "Bank" sunw.door.Orb.publish("Bank", server); } catch (Exception e) { System.out.println("Got exception: "+e); e.printStackTrace(); } } }
When you create CORBA implementation objects, you are tying that object to a CORBA implementation. Even if you use the TIE interface in your ORB, your methods still can throw the CORBA SystemException exception. This is not the ideal situation. Even though JavaIDL and VisiBroker are similar, you still have to make minor changes when going from one to the other. If the conversion is a one-time thing, that may not be a big deal. If, on the other hand, you have to maintain multiple versions of a complex object, you don't want to keep multiple copies of the actual implementation code. You can solve this problem, but it takes a little extra work up front. First, concentrate on using the object you want, without using CORBA, RMI, or any other remote interface mechanism. This is the one copy you use across all your implementations. This object, or set of objects, can define its own types, exceptions, and interfaces. Next, to make this object available remotely, define an IDL interface that is as close to the object's interface as you can get. There may be cases where they won't match exactly, but you can take care of that. Once you generate the Java classes from the IDL definition, create an implementation that simply invokes methods on the real implementation object. This is essentially the same as a TIE interface, with one major exception: the implementation class has no knowledge of CORBA. You can even use this technique to provide multiple ways to access a remote object. Figure 19.3 shows a diagram of the various ways you might provide access to your implementation object. Figure 19.3 : A single object can be accessed by many types of remote object systems. Although this may sound simple, it has some additional complexities that you must address. If your implementation object defines its own exceptions, you must map those exceptions to CORBA exceptions. You must also map between Java objects and CORBA-defined objects. Once again, the banking interface is a good starting point for illustrating the problems and solutions in separating the application from CORBA. The original banking interface is defined with a hierarchy of exceptions, a generic BankingException, with InsufficientFundsException and InvalidAccountException as subclasses. This poses a problem in CORBA, since exceptions aren't inherited. You must define a BankingException exception in your IDL file, this way: exception BankingException {}; In addition, since you probably want the banking application itself to be in the banking package, change the IDL module name to remotebanking.
The implementation for the Banking interface in the remotebanking module must do two kinds of mapping. First, it must convert instances of the Account object to instances of the AccountInfo object. This may seem like a pain and, frankly, it is, but it's a necessary pain. If you start to intermingle the classes defined by CORBA with the real implementation of the application, you end up having to carry the CORBA portions along with the application, even if you don't use CORBA.
// call the withdraw function in the real implementation, catching // any exceptions and throwing the equivalent CORBA exception public synchronized void withdraw(AccountInfo accountInfo, int amount) throws sunw.corba.SystemException, InvalidAccountException, InsufficientFundsException, BankingException { try { // Call the real withdraw method, converting the accountInfo object // to a banking.Account object first impl.withdraw( makeAccount(accountInfo), amount); } catch (banking.InvalidAccountException excep) { // The banking.InvalidAccountException contains an Account object. // Convert it to an AccountInfo object when throwing the CORBA exception throw new InvalidAccountException( makeAccountInfo(excep.account)); } catch (banking.InsufficientFundsException nsf) { throw new InsufficientFundsException(); } catch (banking.BankingException e) { throw new BankingException(); } } Although it would be nice if you could get the IDL-to-Java converter to generate this automatically, it has no way of knowing exactly how the real implementation looks.
Listing 19.4 Source Code for StockQuoteClient.java package stocks; // Defines a callback interface for the StockQuoteServer so // it can notify its clients of new stock quotes. public interface StockQuoteClient { public void quote(StockQuote quote) throws StockQuoteClientException; }
The StockQuoteClientException is a simple subclass of Exception with no extra parameters. The stockquote server likes to know when it can no longer send quotes to a client. If an error occurs, it removes the client from its tables. The trick to wrapping CORBA around a callback system is that you have to create a wrapper for the callback as well as for the server. The wrapper uses the methods that the non-CORBA server defines for its callback and then invokes the corresponding method in the CORBA client. Listing 19.5 shows the implementation of the RemoteStockCallback object, which does the distributed callback.
Listing 19.5 Source Code for RemoteStockCallback.java package remotestocks; public class RemoteStockCallback implements stocks.StockQuoteClient { // Client is the CORBA client whose methods we want to invoke protected StockQuoteClientRef client; // Server is the CORBA-wrapper for the stock quote server. If there // is an error sending a quote to the client, we tell the wrapper // about it so it can remove this client from its tables. protected RemoteStockQuoteImpl server;
// makeRemoteStockQuote converts a stocks.StockQuote object (the Non-CORBA // object) into a remotestocks.StockQuote object (the CORBA version of // the stock quote). protected static StockQuote makeRemoteStockQuote( stocks.StockQuote quote) { return new StockQuote(quote.stock, quote.amount, quote.change); } public RemoteStockCallback(StockQuoteClientRef client, RemoteStockQuoteImpl server) { this.client = client; this.server = server; } // quote is invoked by the real stock quote server implementation, not // the CORBA server. public void quote(stocks.StockQuote quote) throws stocks.StockQuoteClientException { // Try to invoke the remote method, converting the stocks.StockQuote // to the CORBA version, remotestocks.StockQuote try { client.quote(makeRemoteStockQuote(quote)); } catch (Exception e) { // If there was an error, remove this client from the server, then // throw an exception to the real implementation server. try { server.removeCallback(client); } catch (Exception ignore) { } throw new stocks.StockQuoteClientException(); } } }
Now in the normal implementation of the stock quote system, the client calls the addWatch method in the server, passing it the reference to the client and the name of the stock to watch. The server adds this client to its tables and invokes the quote method in the client every time the stock's value changes. With a CORBA front-end on the server, things change slightly. When the request comes into the CORBA wrapper, it
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch19.htm (14 of 16) [8/14/02 10:53:57 PM]
creates an instance of RemoteStockCallback, passing to its constructor the client stub that was passed in the addWatch call. The wrapper then calls the addWatch method in the real server implementation, passing it the RemoteStockCallback. Here is the implementation of the addWatch method in the CORBA wrapper: // addWatch adds a client to the list of clients watching a stock public void addWatch(StockQuoteClientRef client, String stock) throws sunw.corba.SystemException, UnknownStock { // See if we have already created a callback object for this client. RemoteStockCallback callback = (RemoteStockCallback) stockClients.get(client); // If we didn't already have a callback, create one and add it // to the table. if (callback == null) { callback = new RemoteStockCallback(client, this); stockClients.put(client, callback); } // Now call the addWatch method in the real implementation try { impl.addWatch(callback, stock); } catch (stocks.UnknownStockException e) { throw new UnknownStock(stock); } } Now when the real stock quote server publishes a stock quote, it ends up calling the quote method in the RemoteStockCallback object. This callback object, in turn, calls the quote method in the distributed client. Figure 19.5 illustrates the sequence of events for publishing a stock quote. Figure 19.5 : RemoteStockCallback acts as a proxy for a distributed client.
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f19-1.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f19-2.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f19-3.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f19-4.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f19-5.gif
CONTENTS
G
G G
Double-Buffering to Speed Up Drawing H Detecting the Best Drawing Method at Runtime H Creating an Autodetecting update Method Performing Selective Updates Redrawing Changed Areas
If you are just doing simple animation sequences in Java,the performance you get from most Java environments is probably fine. If you are trying to write a video game or some other graphics-intensive program, you may find that you have to squeeze out every bit of performance from Java. Sometimes you are able to make some basic assumptions about the graphics environment you are running on, but that is not the case with Java. You don't know if your program will be running on a laptop with a simple VGA card, or on a Silicon Graphics system with extremely fast graphics. On the PC platform alone, you have a wide variety of graphics capabilities. A simple system may have an old VGA card with an ISA bus interface, or an accelerated graphics card running on a PCI bus Pentium system. The approach used by many graphics vendors is to detect the system type either at installation time or at runtime. Once a program knows how well the graphics perform,it can adjust itself to accommodate a slower system. You can do this in Java, as well.In fact, it is even more important in Java since Java runs on so many different platforms. Aside from adjusting to the capabilities of the local system, you can also reduce the amount of drawing your program has to do. If you redraw only the parts of a screen that need to be redrawn, you have more time to perform other tasks, or to create more animation frames.
then use the getGraphics method to get a graphics context for the image, which you pass to your paint method. When the paint method does its drawing, it is really drawing on the image you created and not the actual screen. Once the paint method is finished, the update method copies the off-screen image to the screen.
To autodetect the graphics speed, do a simple series of drawings on an off-screen image and record the number of milliseconds it takes to complete the drawing. Then, do the same series of drawings to the screen and compare the results. If you want the test to be invisible, do all the drawings in the applet's background color. Instead of doing the drawings invisibly, you can make a neat design that is just a normal part of the applet's startup. Listing 20.1 shows an autodetect method that tries doing double-buffering and direct drawing. It tries to draw a series of images as many times as it can for approximately 500 milliseconds. Normally, you would just use whichever method is able to draw more frames in 500 milliseconds. If they happen to come up with the same number of frames, this autodetect method compares the total time used by each test. It is possible that one of the tests was allowed more drawing time than the other. If that is the case, and the tests each drew the same number of frames, the test that took less time to run is the faster method.
Listing 20.1 doAutoDetect Method from AutoDetect.java // // // // doAutoDetect performs tries drawing to the screen and to a buffer. Whichever one takes the least time (actually, whichever one it can do the most times within a set time constraint) is the one that is best. protected void doAutoDetect(Graphics g) { // Create the off-screen drawing area
offscreenImage = createImage(size().width, size().height); offscreenGraphics = offscreenImage.getGraphics(); long start; long end; // Tally the number of times we were able to draw direct and buffered int directCount = 0; int bufferedCount = 0; // Draw in the applet's background color, makes the autodetection invisible. g.setColor(getBackground()); // Mark what time we started start = System.currentTimeMillis(); end = start; // Paint patterns directly to the screen, but only for 500 milliseconds while ((end-start) < 500) { paintDetectDesign(g); end = System.currentTimeMillis(); directCount++; } g.setColor(getForeground()); // record the total time spent drawing directly long directTime = end - start; start = System.currentTimeMillis(); end = start; // Paint patterns to the offscreen graphics, but only for 500 milliseconds while ((end-start) < 500) { paintDetectDesign(offscreenGraphics); end = System.currentTimeMillis(); bufferedCount++; } long bufferedTime = end - start; // If we were able to draw more times using the buffered graphics, // or if the drawing counts are the same, but the total time for // the buffering was less, buffering is faster. if ((bufferedCount > directCount) || ((bufferedCount == directCount) && (bufferedTime < directTime))) { drawDirect = false; } else {
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch20.htm (3 of 13) [8/14/02 10:54:05 PM]
// If we want to draw direct, free the space taken up by the // offscreen image and graphics context. offscreenImage.flush(); offscreenImage = null; offscreenGraphics = null; drawDirect = true; } detected = true; }
The doAutoDetect method does not do any drawing itself. Instead, it calls another method called paintDetectDesign. This allows you to change the pattern you draw to perform the test. One of the things you might do when performing your test is to simulate the kind of drawing you plan to do. If you plan to draw a lot of images, your drawing test should draw some images. Listing 20.2 shows a sample paintDetectDesign that performs some basic graphics operations.
Listing 20.2 paintDetectDesign Method from AutoDetect.java // paintDetectDesign performs some graphical operations to gauge the time // it takes to paint either directly or to an offscreen area. It just draws // some lines, boxes and ovals a number of times and then returns. protected void paintDetectDesign(Graphics g) { for (int i=0; i < 10; i++) { g.drawLine(0, 0, 100, 100); g.fillRect(0, 0, 100, 100); g.fillOval(0, 0, 100, 100); } }
If it is faster to draw directly to the screen, the update method simply calls super.update, which will clear the screen and call the paint method. Listing 20.3 shows an update method that performs all of these functions and works in conjunction with the doAutoDetect method.
Listing 20.3 update Method from AutoDetect.java public void update(Graphics g) { // If we haven't run auto-detection yet, do it now if (!detected) { doAutoDetect(g); } // // // // If we draw direct, go ahead and call the parent update. This will clear the drawing area and then call paint. If you don't want the drawing area cleared, just change the super.update(g); to paint(g); if (drawDirect) { super.update(g); } else { // // // // If we're doing buffered drawing, simulate the effects of the default update method by clearing the offscreen drawing area. If you don't want the drawing area cleared, remove the calls to setColor and fillRect.
// Clear the offscreen drawing area and set the drawing // color back to foreground. offscreenGraphics.setColor(getBackground()); offscreenGraphics.fillRect(0, 0, size().width, size().height); offscreenGraphics.setColor(getForeground()); // Paint to the offscreen image paint(offscreenGraphics); // Copy the offscreen image to the screen g.drawImage(offscreenImage, 0, 0, this); } }
The doAutoDetect and update methods from the AutoDetect class require some other variables to be present. You will find the complete source to the AutoDetect class on the CD that comes with this book.
Listing 20.4 Source Code for UpdateRects.java import java.awt.*; import java.applet.*;
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch20.htm (6 of 13) [8/14/02 10:54:05 PM]
// This applet demonstrates the use of selective updates, calling // repaint specifically for the areas that change. public class UpdateRects extends Applet implements TimerCallback { // colors contains the colors we cycle through for each shape we draw Color colors[] = { Color.red, Color.green, Color.blue, Color.yellow }; // rects contains the rectangles for each area we want to draw Rectangle rects[] = { new Rectangle(0, 0, 50, 50), new Rectangle(100, 0, 50, 50), new Rectangle(0, 100, 50, 50), new Rectangle(100, 100, 50, 50) }; // We cycle each rectangle through a set of colors. Start them off // with different colors. int rectColor[] = { 0, 1, 2, 3 }; Timer timer; // // // // // paint assumes that it is only painting a portion of the screen. It examines the area it is supposed to repaint by calling getClipRect, then it uses the intersects method in the Rectangle class to see which rectangles intersect with the repainted area. If a rectangle doesn't intersect, it doesn't need to be redrawn. public void paint(Graphics g) { // Get the area we are painting Rectangle clipRect = g.getClipRect(); for (int i=0; i < rects.length; i++) { // If this rectangle doesn't intersect with the clipping area, // we don't need to repaint it, so just go on to the next rectangle if (!clipRect.intersects(rects[i])) continue; // For each rectangle we just call fillOval and use the dimensions of // the rectangle. g.setColor(colors[rectColor[i]]); g.fillOval(rects[i].x, rects[i].y, rects[i].width, rects[i].height);
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch20.htm (7 of 13) [8/14/02 10:54:05 PM]
} } // For every timer tick we change the colors of each rectangle and // call repaint for each area we change, rather than calling one // big repaint. public void tick() { for (int i=0; i < rects.length; i++) { // Change the rectangle's color rectColor[i] = (rectColor[i] + 1) % colors.length; // Call repaint just for this rectangle repaint(rects[i].x, rects[i].y, rects[i].width, rects[i].height); } } public void start() { // Timer tick every 250 milliseconds (4 times a second) timer = new Timer(this, 250); timer.start(); } public void stop() { timer.stop(); timer = null; } }
Alternatively, you can create a rectangle that represents the changed area and enlarge the rectangle to encompass newly changed areas. The Rectangle class contains an add method that returns the smallest rectangle that encloses two other rectangles. When you determine the rectangle that encloses a changed area, you add that rectangle to the current changed area, producing a new changed-area rectangle. You have to be careful with this approach. If you start adding all your rectangles together, you may end up with one big rectangle that is as large as the drawing area. This method is useful when you are moving an object around in fairly small increments. If the rectangular area holding the object's old area intersects with the new area, you might be better off adding the rectangles together. The closer the areas are to each other, the better it is to add the rectangles. If they are far apart, the sum of the rectangles holds a lot more unaffected space.
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch20.htm (8 of 13) [8/14/02 10:54:06 PM]
This might cause you to spend a lot of time repainting areas that haven't changed. Adding rectangles is a trade-off. You have to balance the redrawing of areas that may not need redrawing against the reduced number of repaints you actually do.
Listing 20.5 Partial Listing of BlockDrop.java // paintBlock colors in a single grid block on a graphics object public void paintBlock(Graphics g, int x, int y)
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch20.htm (9 of 13) [8/14/02 10:54:06 PM]
{ g.setColor(colors[blockGrid[y][x]]); g.fillRect(x * blockSize, y * blockSize, blockSize, blockSize); } // drawNewBlock paints a new block on the off-screen image, then calls // repaint for just that block's area public void drawNewBlock(int x, int y) { paintBlock(offscreenGraphics, x, y); repaint(x * blockSize, y * blockSize, blockSize, blockSize); } // drawBlockPair paints a block and the block below, then calls repaint // for the 2-block area. public void drawBlockPair(int x, int y) { paintBlock(offscreenGraphics, x, y); paintBlock(offscreenGraphics, x, y+1); repaint(x * blockSize, y * blockSize, blockSize, blockSize*2); } // drawAllBlocks draws all the blocks in the grid to the off-screen area, // then calls repaint for the entire screen. public void drawAllBlocks() { for (int y=0; y < gridHeight; y++) { for (int x=0; x < gridWidth; x++) { paintBlock(offscreenGraphics, x, y); } } repaint(); } public void paint(Graphics g) { g.drawImage(offscreenImage, 0, 0, this); } public void update(Graphics g) { paint(g); }
Note Notice that the drawBlock and drawBlockPair methods in BlockDrop.java call repaint with a specific region. Even though the paint method assumes it is redrawing the entire screen, it really updates just a tiny portion of the screen. This technique does make a difference, even when paint still tries to draw the whole area. The reason it makes a difference is that you aren't using the low-level graphics routines to update every pixel on the screen, which does take some time.
The whole reason for this exercise of drawing to an off-screen buffer is that you are no longer constrained to doing all your drawing in the paint method. As soon as you decide that something on the screen needs to change, you can change it. Of course, you have to repaint the screen before the change is visible. The BlockDrop applet drops blocks from the top of the screen by using a timer. It is able to redraw blocks from within the tick method (called by the timer) because it is drawing to an off-screen buffer. If it were drawing directly to the screen, it would have to make a note of which items had changed and then repaint the area for those items. The paint method would have to look at what had changed and repaint only those areas of the screen. Listing 20.6 shows the tick method from the BlockDrop applet. Notice that once it decides to add a new block or change a block, it immediately calls the methods to redraw the blocks.
Listing 20.6 tick Method from BlockDrop.java // Every time tick is called, either move the current block down, or // start a new block public void tick() { // If there isn't a block falling, create a new one if (!blockFalling) { blockX = (int)(gridWidth * Math.random()); blockY = 0; // Put the block into the grid with a random color (adjust the random color // to start at 1 and not 0). blockGrid[blockY][blockX] = 1+(int)((colors.length-1) * Math.random()); blockFalling = true; drawNewBlock(blockX, blockY); } else { // See if we can still move the block down. If the block's Y is still above
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch20.htm (11 of 13) [8/14/02 10:54:06 PM]
// the bottom, and the color of the grid element below it is 0, the block // is allowed to move. if ((blockY < gridHeight-1) && (blockGrid[blockY+1][blockX]) == 0) { // Copy the block's color to the grid element below blockGrid[blockY+1][blockX] = blockGrid[blockY][blockX]; // Clear out the current grid element blockGrid[blockY][blockX] = 0; blockY++; // Draw both the newly empty element and the block's new location drawBlockPair(blockX, blockY-1); } else { // If we can't move the block, need to check the next time blockFalling = false; } // See if the bottom is full checkGridFloor(); } }
Figure 20.1 shows the BlockDrop applet in action. The complete source code to BlockDrop.java is on the CD that comes with this book. Figure 20.1 : The BlockDrop applet calls drawing routines from outside the paint method. You can convert the BlockDrop applet to do direct screen writes very easily. Simply comment out the calls to paintBlock in drawNewBlock, drawBlockPair, and drawAllBlocks, and insert the following update method: public void update(Graphics g) { Rectangle clipRect = g.getClipRect(); // Compute the starting X and ending X of the area to be repainted int blockStartX = clipRect.x / blockSize; int blockEndX = (clipRect.x + clipRect.width) / blockSize; if (blockEndX >= gridWidth) blockEndX = gridWidth - 1; // Compute the starting Y and ending Y of the area to be repainted int blockStartY = clipRect.y / blockSize; int blockEndY = (clipRect.y + clipRect.height) / blockSize; if (blockEndY >= gridHeight) blockEndY = gridHeight - 1;
// Repaint only the blocks that need to be repainted for (int y=blockStartY; y <= blockEndY; y++) { for (int x=blockStartX; x <= blockEndX; x++) { paintBlock(g, x, y); } } } Some of these issues may be less important as the Java graphics system is improved. One of the features to be added is sprite animation, which allows you to define objects that can move around the screen. The graphics system would then take care of updating the changed areas. You would no longer have to keep track of them by hand.
CONTENTS
G G G G G G G
Animation An Animation Driver Animating Image Sequences Animating Portions of an Image Animating with a Filter Cycling the Color Palette Animating Graphics H Redrawing the Entire Screen H Doing Animation with XOR Eliminating Flicker H Double-Buffering
Animation
Animation involves changing a picture over and over to simulate movement of some sort. There are several different types of animation you can perform in a Java applet. You can display a sequence of images, or you can display a single image while changing a portion of it. You can change an image by running it through a filter or by changing the colors in the image. You can also perform animation with the basic graphics classes. Animation is a powerful technique for applets. Having a portion of your display that moves can make your application or your Web page much livelier. There are far more uses for animation that just sprucing up a Web page, however. Animation is frequently used in video games. In fact, it's probably one of the most common factors in computer games. Modern adventure games often use sequences of real-life images to give the game a modern feel. Arcade games frequently employ graphical animation, although some have begun to integrate images into the games as well. Beyond gaming, animation is an excellent tool for computer-assisted learning. Rather than describing a technique with plain old text, you can demonstrate it through animation. This type of animation is often done with images but not necessarily. You may be demonstrating a technique that requires a graphical representation that is computed while the program is running.
An Animation Driver
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch5.htm (1 of 27) [8/14/02 10:54:12 PM]
To perform animation, you have to create a thread that repeatedly changes the animation sequence and repaints the picture. Because the different animation techniques all use this same method, you can use the same mechanism to trigger the next animation sequence. The idea here is that you create a timer class that calls a method at specific intervals. Ideally, you would like the timer class to be able to invoke different methods in your applet, allowing you to run multiple timers in a single applet. You can do this in Java, but it takes a little extra work. The first thing you need to do in setting up this timer class is to define the method the timer will call each time. Listing 5.1 shows the TimerCallback interface that defines this method.
Listing 5.1 Source Code for TimerCallback.java // This interface defines a callback for the Timer class public interface TimerCallback { public void tick(); }
Once you have this interface defined, you can create a Timer class that repeatedly calls the tick method at some fixed interval. You should keep track of the amount of time that elapses during the call to the tick method and only sleep for the amount of time remaining before the next tick. This makes your animation much smoother and helps minimize the effects of garbage collection. Obviously, if the tick method takes longer than the interval, your animation will be slower than you desire. Listing 5.2 shows a reusable timer class that repeatedly calls the tick method in a TimerCallback interface at a certain interval. You can set the interval when you create the Timer object, and you may change it at any time.
Listing 5.2 Source Code for Timer.java /** * This class implements an interval timer. It calls * the tick method in the callback interface after * a fixed number of milliseconds (indicated by the * interval variable). It measures the amount of time spent * in the tick method and adjusts for it. * To start up a timer with this class, create it with * a callback and the number of milliseconds in the interval * and then call the start method: * <PRE> * * Timer timer = new Timer(this, 2000); // 2 second interval * timer.start(); *
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch5.htm (2 of 27) [8/14/02 10:54:12 PM]
* </PRE> * * @author Mark Wutka */ public class Timer extends Object implements Runnable { protected Thread timerThread; /** The number of milliseconds in the interval*/ protected long interval; /** The callback interface containing the tick method */ protected TimerCallback callback; public Timer() { } public Timer(TimerCallback callback) { this.callback = callback; } public Timer(long interval) { this.interval = interval; } public Timer(TimerCallback callback, long interval) { this.callback = callback; this.interval = interval; } /** returns the number of milliseconds in the interval */ public long getInterval() { return interval; } /** sets the number of milliseconds in the interval * @param newInterval the new number of milliseconds */ public void setInterval(long newInterval) { interval = newInterval; } /** returns the callback interface */ public TimerCallback getCallback() {
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch5.htm (3 of 27) [8/14/02 10:54:12 PM]
return callback; } /** changes the callback interface * @param callback the new callback */ public void setCallback(TimerCallback callback) { this.callback = callback; } /** starts the timer */ public void start() { timerThread = new Thread(this); timerThread.start(); } /** stops the timer */ public void stop() { timerThread.stop(); timerThread = null; } public void run() { while (true) { // Check the current time long startTime = System.currentTimeMillis(); // If there is a callback, call it if (callback != null) { callback.tick(); } // Check the time again long endTime = System.currentTimeMillis(); // The amount of time to sleep is the interval minus the time spent // in the tick routine long sleepTime = interval - (endTime - startTime); // If you've passed the next interval, hurry up and call the next tick if (sleepTime <= 0) continue; try { Thread.sleep(sleepTime); } catch (Exception insomnia) { // might as well ignore this exception }
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch5.htm (4 of 27) [8/14/02 10:54:12 PM]
} } }
Now, you can only have one tick method, so how can you support multiple timers in a single application? The answer lies in a design pattern called the Command pattern. An object that implements the tick method is considered a command object. If you want to have multiple timers that call different methods in your class, you create a number of intermediate objects. Suppose you have the following object: public class DualTimers extends Object { public DualTimers() { } public void timer1Tick() { System.out.println("Tick!"); } public void timer2Tick() { System.out.println("Tock!"); } } To set up timers to call each of these methods, create small intermediate classes to invoke each of these methods: public class Timer1Callback extends Object implements TimerCallback { protected DualTimers whichTimer; public Timer1Callback(DualTimers timer) { whichTimer = timer; } public void tick() { whichTimer.timer1Tick(); } } Notice that this Timer1Callback class can be passed to a Timer object, and when its tick method is called, it will call the timer1Tick method in the DualTimer class. Similarly, you can create a Timer2Callback object: public class Timer2Callback extends Object implements TimerCallback { protected DualTimers whichTimer;
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch5.htm (5 of 27) [8/14/02 10:54:13 PM]
public Timer2Callback(DualTimers timer) { whichTimer = timer; } public void tick() { whichTimer.timer2Tick(); } } Figure 5.1 shows the relationship between a Timer object and a TimerCallback object. Figure 5.1 : In the simple configuration, a Timer object invokes the tick method in a TimerCallback object. Figure 5.2 shows how the relationship between these two objects changes when you use intermediate objects. Figure 5.2 : Intermediate timer collbacks allow a single object to have multiple timer callbacks. Now that you have these command objects, you can create two timers that call methods in the DualTimer object: public class TestDualTimers extends Object { public static void main(String[] args) { DualTimers dual = new DualTimers(); Timer1Callback tc1 = new Timer1Callback(dual); Timer2Callback tc2 = new Timer2Callback(dual); Timer timer1 = new Timer(tc1, 1000); Timer timer2 = new Timer(tc2, 2000); timer1.start(); timer2.start(); } } In the previous example, you create only a single instance of the DualTimers object but two different timers that eventually trigger methods in DualTimers. If you only need to invoke a single method in an object, you don't need to create this extra layer of objects. Figure 5.3 shows this test program in operation. Figure 5.3 : Timer 1, triggered every second, prints tick, while timer 2, triggered every two seconds, prints tock.
When you are first loading the images for an animation sequence, you should print some text in your applet or even some of the images from the animation-just to keep your viewers from getting impatient. You can use the ImageObserver interface to track which images have been loaded. Listing 5.3 shows an applet that cycles a set of six images using the Timer class to trigger the next change. It displays a text message when it first comes up, and when the first image has been loaded, it displays it. Then, when the rest of the images have been loaded, it starts the animation.
Listing 5.3 Source Code for CycleAnimation.java import java.applet.*; import java.awt.*; import java.awt.image.*; // // // // // // This applet demonstrates several useful techniques in dealing with animating images. 1. It uses the Timer class to trigger the next animation frame 2. It displays a "teaser" while the images are being loaded 3. It does not use the MediaTracker to wait for images, but knows when all the images are ready.
public class CycleAnimation extends Applet implements TimerCallback, ImageObserver { protected Image[] images; int whichImage; // This applet cycles from 0-5 and then cycles back down from 5-0 // You won't need imageDirection if you only cycle one way. int imageDirection; // have we started the animation or not? boolean animationStarted; Timer timer; public CycleAnimation() { timer = null; animationStarted = false; } public void init() { images = new Image[6]; for (int i=0; i < 6; i++) { // Get images named mark1.gif - mark6.gif images[i] = getImage(getDocumentBase(), "mark"+(i+1)+".gif");
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch5.htm (7 of 27) [8/14/02 10:54:13 PM]
// Start downloading the images, but don't wait for them prepareImage(images[i], this); } // Animation will start at image 0, and go up to 5 whichImage = 0; imageDirection = 1; } // This update method prevents a minor flickering that occurs because // the default update method clears the screen before drawing public void update(Graphics g) { paint(g); } public void paint(Graphics g) { // If we haven't started the animation, display a "teaser" if (!animationStarted) { // If image 0 has been loaded go ahead and display it int flags = checkImage(images[0], this); if ((flags & ImageObserver.ALLBITS) != 0) { g.drawImage(images[0], 10, 10, this); } else { // If we haven't even gotten image 0, just display text g.drawString("Watch this space", 10, 30); g.drawString("For neat animation", 10, 50); } } else { // If we're in the animation, draw the current image in the sequence g.drawImage(images[whichImage], 10, 10, this); } } // imageUpdate is called when there is an update to any of the images // that are being loaded. public boolean imageUpdate(Image img, int flags, int x, int y, int width, int height) { // If this update isn't telling us that an image has been loaded // completely, we don't want to hear about it if ((flags & ImageObserver.ALLBITS) == 0) { return true; } // If we've gotten the first image, go ahead and repaint
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch5.htm (8 of 27) [8/14/02 10:54:13 PM]
if (img == images[0]) { repaint(); } // Check to see if all the images have been loaded (ALLBITS set in // all of them) for (int i=0; i < images.length; i++) { int iflags = checkImage(images[i], this); // Uh oh, we found one that isn't finished if ((iflags & ImageObserver.ALLBITS) == 0) { return true; } } // all right, we're ready to roll! startAnimation(); return true; } public void tick() { // Change the image - if we are counting from 0 to 5, which direction // will be 1, if we are going from 5 to 0, whichDirection will be -1 whichImage += imageDirection; // If we've gone past the first image, change direction if (whichImage < 0) { whichImage = 0; imageDirection = 1; // If we've gone past the last image, change direction } else if (whichImage >= images.length) { whichImage = images.length-1; imageDirection = -1; } repaint(); } protected void startAnimation() { animationStarted = true; timer = new Timer(this, 500); timer.start(); } }
Figure 5.4 : You can create an animation sequence by cycling through different images. Figure 5.5 : Animation sequences often have only minor changes between frames.
Listing 5.4 Source Code for AnimatePortion.java import java.applet.*; import java.awt.*; import java.awt.image.*; // This applet performs animation by loading in a whole image // and then successive versions of a much smaller potion of the // image. public class AnimatePortion extends Applet implements TimerCallback, ImageObserver { protected Image baseImage; protected Image[] images; int whichImage; // This applet cycles from 0-5 and then cycles back down from 5-0 // You won't need imageDirection if you only cycle one way. int imageDirection; // Have we started the animation or not? boolean animationStarted; Timer timer; public AnimatePortion()
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch5.htm (10 of 27) [8/14/02 10:54:13 PM]
{ timer = null; animationStarted = false; } public void init() { images = new Image[6]; // Load the base (whole) image baseImage = getImage(getDocumentBase(), "mark1.gif"); for (int i=0; i < 6; i++) { // Get partial images named animprt1.gif - animprt6.gif images[i] = getImage(getDocumentBase(), "animprt"+(i+1)+".gif"); // Start downloading the images, but don't wait for them prepareImage(images[i], this); } // Animation will start at image 0, and go up to 5 whichImage = 0; imageDirection = 1; } // This update method prevents a minor flickering that occurs because // the default update method clears the screen before drawing public void update(Graphics g) { paint(g); } public void paint(Graphics g) { // If we haven't started the animation, display a "teaser" if (!animationStarted) { // If base image has been loaded go ahead and display it int flags = checkImage(baseImage, this); if ((flags & ImageObserver.ALLBITS) != 0) { g.drawImage(baseImage, 10, 10, this); } else { // If we haven't even gotten the base image, just display text g.drawString("Watch this space", 10, 30); g.drawString("For neat animation", 10, 50); } } else { // If we're in the animation, draw the base image and the current // smaller animated portion. g.drawImage(baseImage, 10, 10, this);
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch5.htm (11 of 27) [8/14/02 10:54:13 PM]
g.drawImage(images[whichImage], 70, 70, this); } } // imageUpdate is called when there is an update to any of the images // that are being loaded. public boolean imageUpdate(Image img, int flags, int x, int y, int width, int height) { // If this update isn't telling us that an image has been loaded // completely, we don't want to hear about it if ((flags & ImageObserver.ALLBITS) == 0) { return true; } // If we've gotten the base image, go ahead and repaint if (img == baseImage) { repaint(); return true; } // Check to see if all the images have been loaded (ALLBITS set in // all of them) for (int i=0; i < images.length; i++) { int iflags = checkImage(images[i], this); // Uh oh, we found one that isn't finished if ((iflags & ImageObserver.ALLBITS) == 0) { return true; } } // all right, we're ready to roll! startAnimation(); return true; } public void tick() { // Change the image - if we are counting from 0 to 5, which direction // will be 1, if we are going from 5 to 0, whichDirection will be -1 whichImage += imageDirection; // If we've gone past the first image, change direction if (whichImage < 0) { whichImage = 0; imageDirection = 1; // If we've gone past the last image, change direction
} else if (whichImage >= images.length) { whichImage = images.length-1; imageDirection = -1; } repaint(); } protected void startAnimation() { animationStarted = true; timer = new Timer(this, 500); timer.start(); } }
Figures 5.6 and 5.7 show the portions of the overall image that actually change. The overall animation is identical to the one in Figures 5.4 and 5.5. Figure 5.6 : It is often better to animate images by animating only the section that changes. Figure 5.7 : Only the changed sections are redrawn when painting the next frame.
Another thing to consider when filtering images is that even though the original image may be completely loaded, you also need to make sure that the filtered image has been generated completely before drawing it. This is not as big a deal when displaying an image once, but when you get into a loop and start flushing out pixels over and over, you can really slow things down trying to display a filtered image before it is ready. This is one of the places in which the MediaTracker class comes in handy. After you flush out an image, you should set up a MediaTracker class to wait for the image to be rebuilt. Here is an example that rebuilds an image after the call to flush: image.flush(); MediaTracker tracker = new MediaTracker(this); tracker.addImage(image, 0); try { if (!tracker.waitForID(0)) { // If there's an error, don't repaint return; } } catch (Exception waitingError) { return; // Again, if there's an error, don't repaint } repaint(); // Draw the newly-loaded image Listing 5.5 shows a simple filter that changes the transparency value of an image, causing it to fade in and out. It is quite suitable for animation.
Listing 5.5 Source Code for TransFilter.java import java.awt.image.*; // // // // // // // // // // This filter fades an image by moving the RGB values towards the background color. The transValue variable controls the amount of transparency for the image. A level of 255 means fully opaque, while 0 means fully transparent. The formula for a color component is (background * (255 - transValue) + component * transValue) / 255 Notice that when transValue is 0, the component isn't part of the final color, instead the background is the whole color when transValue is 255, the background isn't used at all, and the full color component is the new color.
public class TransFilter extends RGBImageFilter { private int transValue; private int bgRed; private int bgGreen; private int bgBlue; public TransFilter(int tV, int backgroundRGB) { canFilterIndexColorModel = true; transValue = tV; bgRed = (backgroundRGB >> 16) & 255;
bgGreen = (backgroundRGB >> 8) & 255; bgBlue = backgroundRGB & 255; } // Changes the transparency value public void setTransValue(int newValue) { transValue = newValue; } // Retrieves the transparency value public int getTransValue() { return transValue; } // Adjusts the transparency value of an RGB value, essentially // multiplying the transparency by transValue / 255 public int filterRGB(int x, int y, int rgb) { // Compute the new red, green and blue components int red = (bgRed * (255 - transValue) + ((rgb >> 16) &0xff) * transValue) / 255; int green = (bgGreen * (255 - transValue) + ((rgb >> 8) & 0xff) * transValue) / 255; int blue = (bgBlue * (255 - transValue) + (rgb & 0xff) * transValue) / 255; // Combine the components back into a single RGB value, preserving the // original transparency value from the RGB component. return (rgb & 0xff000000) + (red << 16) + (green << 8) + blue; } }
Figure 5.8 shows an image in the process of fading. The applet that produces this image uses the TransFilter image filter to produce the fading effect. This applet is included on the CD-ROM that comes with this book. Figure 5.8 : You can produce neat effects by filtering the colors in an image.
sections of the color palette. This technique is often used to show flowing water or motion between two points. If you want to perform palette cycling on an existing image, you may have to do a bit of work with the image to set up the palette order. If you generate the image from a memory image source, you can create your own index color model that is easy to cycle. Unlike other color filters, you don't change the colors in the getRGB method in a color palette cycler. Instead, you override the filterIndexColorModel method, which creates a new index color model based on the existing one. Listing 5.6 shows an implementation of a color palette cycler. Its constructor takes three parameters-the location of the first palette entry to be cycled, the number of consecutive entries to cycle, and the direction of the cycle. If the direction is positive, the colors are cycled from left to right. If the direction is negative, they are cycled from right to left. The direction also indicates the number of positions a color moves during each cycle. Normally, direction should be 1 or -1, to move each color over by 1 position. You may want to move sets of colors, however. For instance, if you need to shift colors in pairs, set the direction to 2 or -2. The cycleComponent method cycles the palette every time it is called.
Listing 5.6 Source Code for CycleFilter.java import java.awt.*; import java.awt.image.*; // // // // // // // // // //
This class cycles the colors in an index color model. When you create a CycleFilter, you give the offset in the index color model and also the number of positions you want to cycle. Then every time you call cycleColors, it increments the cycle position. You then need to re-create your image and its colors will be cycled. This filter will only work on images that have an indexed color model.
public class CycleFilter extends RGBImageFilter { // The offset in the index to begin cycling protected int cycleStart; // How many colors to cycle protected int cycleLen; // The current position in the cycle protected int cyclePos; // The cycle direction and length protected int direction; // A temporary copy of the color components being cycled protected byte[] tempComp; public CycleFilter(int cycleStart, int cycleLen, int direction) { this.cycleStart = cycleStart; this.cycleLen = cycleLen;
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch5.htm (16 of 27) [8/14/02 10:54:13 PM]
this.direction = direction; tempComp = new byte[cycleLen]; cyclePos = 0; // Must set this to true to allow the shortcut of filtering // only the index and not each individual pixel canFilterIndexColorModel = true; } // // // // cycleColorComponent takes an array of bytes that represent either the red, green, blue, or alpha components from the index color model, and cycles them based on the cyclePos. It leaves the components that aren't part of the cycle intact. public void cycleColorComponent(byte component[]) { // If there aren't enough components to cycle, leave this alone if (component.length < cycleStart + cycleLen) return; // Make a temporary copy of the section to be cycled System.arraycopy(component, cycleStart, tempComp, 0, cycleLen); // Now for each position being cycled, shift the component over // by cyclePos positions. for (int i=0; i < cycleLen; i++) { component[cycleStart+(cyclePos+i)%cycleLen] = tempComp[i]; } } // // // // cycleColors moves the cyclePos by <direction> locations. If direction is positive, the colors move from left to right. If direction is negative, they move from right to left. public void cycleColors() { cyclePos = (cyclePos + direction) % cycleLen; if (cyclePos < 0) cyclePos += cycleLen; } // Can't really filter direct color model RGB this way, because you have // no idea what rgb values get cycled, so just return the original // rgb values. public int filterRGB(int x, int y, int rgb) { return rgb; }
// // // // //
filterIndexColorModel is called by the image filtering mechanism whenever the image uses an indexed color model and the canFilterIndexColorModel flag is set to true. This allows you to filter colors without filtering each and every pixel in the image. public IndexColorModel filterIndexColorModel(IndexColorModel icm) {
// Get the size of the index color model int mapSize = icm.getMapSize(); // Create space for the red, green, and blue components byte reds[] = new byte[mapSize]; byte greens[] = new byte[mapSize]; byte blues[] = new byte[mapSize]; // Copy in the red components and cycle them icm.getReds(reds); cycleColorComponent(reds); // Copy in the green components and cycle them icm.getGreens(greens); cycleColorComponent(greens); // Copy in the blue components and cycle them icm.getBlues(blues); cycleColorComponent(blues); // See if there is a transparent pixel. If not, copy in the alpha // values, just in case the image should be partially transparent. if (icm.getTransparentPixel() == -1) { // Copy in the alpha components and cycle them byte alphas[] = new byte[mapSize]; icm.getAlphas(alphas); cycleColorComponent(alphas); return new IndexColorModel(icm.getPixelSize(), mapSize, reds, greens, blues, alphas); } else { // If there was a transparent pixel, ignore the alpha values and // set the transparent pixel in the new filter return new IndexColorModel(icm.getPixelSize(), mapSize, reds, greens, blues, icm.getTransparentPixel()); } } }
Listing 5.7 shows an applet that displays a .GIF image with a customized color palette. The image contains several figures colored with consecutive color palette entries. As the palette cycles, the colors of the figures change.
Listing 5.7 Source Code for Cycler.java import java.awt.*; import java.awt.image.*; import java.applet.*; // This applet creates a series of moving // lines by creating a memory image and cycling // its color palette. public class Cycler extends Applet implements TimerCallback { protected Image origImage; // the image before color cycling protected Image cycledImage; // image after cycling protected CycleFilter colorFilter; // performs the cycling public void init() { // Create the uncycled image origImage = getImage(getDocumentBase(), "cycleme.gif"); MediaTracker mt = new MediaTracker(this); mt.addImage(origImage, 0); try { mt.waitForID(0); } catch (Exception hell) { } // Create the filter for cycling the colors colorFilter = new CycleFilter(1, 5, 1); // Create the first cycled image cycledImage = createImage(new FilteredImageSource( origImage.getSource(), colorFilter)); Timer t = new Timer(this, 1000); t.start(); } // Paint simply draws the cycled image public synchronized void paint(Graphics g) { g.drawImage(cycledImage, 0, 0, this); }
// Flicker-free update public void update(Graphics g) { paint(g); } // Cycles the colors and creates a new cycled image. Uses media // tracker to ensure that the new image has been created before // trying to display. Otherwise, we can get bad flicker. public synchronized void tick() { // Cycle the colors colorFilter.cycleColors(); // Flush clears out a loaded image without having to create a // whole new one. When we use waitForID on this image now, it // will be regenerated. cycledImage.flush(); MediaTracker myTracker = new MediaTracker(this); myTracker.addImage(cycledImage, 0); try { // Cause the cycledImage to be regenerated myTracker.waitForID(0); } catch (Exception ignore) { } // Now that we have reloaded the cycled image, ask that it // be redrawn. repaint(); } }
Figure 5.9 shows an image whose colors are being cycled. Figure 5.9 : Cycling colors in the color palette creates neat animation effects.
Animating Graphics
In addition to animating images, you can perform animation with the graphics drawing functions. There are two ways to animate graphical figures. You can create each frame anew, clearing the screen and drawing the new figure, or you can use the XOR drawing function to move a figure without redrawing the rest of the screen.
area has already been cleared. All you need to do is draw the new frame. Listing 5.8 shows an applet that moves a ball back and forth on the screen using this simple technique. As usual, it uses the Timer class to trigger the next frame.
Listing 5.8 Source Code for BallAnim1.java import java.awt.*; import java.applet.*; // This applet moves three balls around the screen by repainting // the entire scene every time. public class BallAnim1 extends Applet implements TimerCallback { int int int int ballX[] = { 0, 200, 0 }; // Current X coord of each ball ballY[] = { 0, 0, 100 }; // Current Y coord ballXSpeed[] = { 1, 0, 1 }; // Current X speed ballYSpeed[]= { 1, 1, 0 }; // Current Y Speed
Timer timer; Color ballColor[] = { Color.red, Color.yellow, Color.blue }; int numBalls = ballX.length; public void init() { } // Repaint the entire scene (i.e. draw each ball) public void paint(Graphics g) { for (int i=0; i < numBalls; i++) { g.setColor(ballColor[i]); g.fillOval(ballX[i], ballY[i], 30, 30); } } // For each timer tick, move the balls. If they go off the edge anywhere, // change their direction. public void tick() { for (int i=0; i < numBalls; i++) { // Move the ball ballX[i] += ballXSpeed[i]; ballY[i] += ballYSpeed[i]; // See if it goes off the edge anywhere
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch5.htm (21 of 27) [8/14/02 10:54:13 PM]
public void start() { Timer timer = new Timer(this, 100); timer.start(); } public void stop() { timer.stop(); timer = null; } }
Using this method, the figures you draw have a notion of depth. The first figure drawn is on the bottom, while the last figure drawn is on the top. Being on top means that if a figure shares any screen area with another figure, the figure on top covers the other figure. This makes sense-if you draw one figure and then draw another figure on top of it, the second figure would obscure the first one. Figure 5.10 shows the BallAnim1 applet in action. Figure 5.10: By redrawing all figures in a particular sequence, you create a sense of depth.
{ paint(g); } Listing 5.9 shows an example of XOR animation, with three shapes moving in different directions. Notice the interesting color combinations when the figures collide.
Listing 5.9 Source Code for BallAnim2.java import java.awt.*; import java.applet.*; // This applet moves 3 balls around the screen using XOR animation. public class BallAnim2 extends Applet implements TimerCallback { int ballX[] = { 0, 200, 0 }; // X coords of each ball int ballY[] = { 0, 0, 100 }; // Y coords int ballXSpeed[] = { 1, 0, 1 }; // Speed in X direction int ballYSpeed[]= { 1, 1, 0 }; // Speed in Y direction boolean drewFirst = false; Timer timer; Color ballColor[] = { Color.red, Color.yellow, Color.blue }; int numBalls = ballX.length; public void init() { } // special version of update that doesn't erase the screen public void update(Graphics g) { paint(g); } public void paint(Graphics g) { // Go into XOR mode with white as the XOR color g.setXORMode(Color.white); // redraw the old balls, causing them to be erased. Don't try to erase anything // if we haven't drawn the first time, otherwise there will be garbage // left on the screen. if (drewFirst) {
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch5.htm (23 of 27) [8/14/02 10:54:13 PM]
for (int i=0; i < numBalls; i++) { g.setColor(ballColor[i]); g.fillOval(ballX[i], ballY[i], 30, 30); } } // Now move the balls. In our repaint, we erase the old, move, and then // draw the new. moveBalls(); // Draw the balls in their new position for (int i=0; i < numBalls; i++) { g.setColor(ballColor[i]); g.fillOval(ballX[i], ballY[i], 30, 30); } drewFirst = true; } public void moveBalls() { for (int i=0; i < numBalls; i++) { // Move the ball ballX[i] += ballXSpeed[i]; ballY[i] += ballYSpeed[i]; // See if it has gone off the edge if ((ballX[i] < 0) || (ballX[i] >= size().width)) { ballXSpeed[i] = -ballXSpeed[i]; } if ((ballY[i] < 0) || (ballY[i] >= size().height)) { ballYSpeed[i] = -ballYSpeed[i]; } } } // Rather than moving the balls in the tick method, we move them in the // paint method. All tick needs to do is trigger a repaint. public void tick() { repaint(); } public void start() { Timer timer = new Timer(this, 100); timer.start(); } public void stop() { timer.stop();
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch5.htm (24 of 27) [8/14/02 10:54:13 PM]
timer = null; } }
Figure 5.11 shows the BallAnim2 applet in action. Figure 5.11: XOR animation produces strange results when objects collide.
Eliminating Flicker
You may have noticed a lot of flicker in some of your animation applets. Flicker is actually just a very quick change on the drawing area. If you use the standard update method, the screen is cleared before your paint method is called. Your eye picks up that momentary clearing, making it look like the screen flickers. Fortunately, there are simple ways to eliminate flicker. First, you can override the update method that doesn't clear the screen before calling paint: public void update(Graphics g) { paint(g); } If your paint method relies on the screen being cleared, this may give you trouble. You can counter this, however, by using the second technique, which, when used in conjunction with the above update method, should eliminate flicker from your applet completely. This method is called "double-buffering."
Double-Buffering
The technique of double-buffering involves drawing to an off-screen image and then drawing the off-screen in a single method call. To perform double-buffering, you need to declare an instance variable to hold the off-screen image: Image offscreenImage; Next, in your init method, you need to create the image: offscreenImage = createImage(size().width, size().height); Finally, you can create an update method that draws to this off-screen image. By clearing the off-screen image before calling paint, you can add double-buffering to any applet without changing the applet's paint method. Listing 5.10 shows one of the animation examples with a flicker-free update method. The changes made to accommodate the flicker-free update are the addition of the offscreenImage variable, its initialization in the init method, and a new update method.
Listing 5.10 Source Code for BallAnim3.java import java.awt.*; import java.applet.*; // This applet moves three balls around the screen by repainting // the entire scene every time. // It has a flicker-free update method. public class BallAnim3 extends Applet implements TimerCallback { int int int int ballX[] = { 0, 200, 0 }; // Current X coord of each ball ballY[] = { 0, 0, 100 }; // Current Y coord ballXSpeed[] = { 1, 0, 1 }; // Current X speed ballYSpeed[]= { 1, 1, 0 }; // Current Y Speed
Timer timer; Color ballColor[] = { Color.red, Color.yellow, Color.blue }; int numBalls = ballX.length; Image offscreenImage; public void init() { offscreenImage = createImage(size().width, size().height); } public void update(Graphics g) { // Get a graphics context for the offscreen area Graphics offscreenG = offscreenImage.getGraphics(); // Clear the offscreen area to the background color offscreenG.setColor(getBackground()); offscreenG.fillRect(0, 0, size().width, size().height); // Paint on the offscreen image paint(offscreenG); // Copy the offscreen image to the screen g.drawImage(offscreenImage, 0, 0, this); } // Repaint the entire scene (i.e. draw each ball) public void paint(Graphics g) { for (int i=0; i < numBalls; i++) { g.setColor(ballColor[i]); g.fillOval(ballX[i], ballY[i], 30, 30); }
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch5.htm (26 of 27) [8/14/02 10:54:13 PM]
} // For each timer tick, move the balls. If they go off the edge anywhere, // change their direction. public void tick() { for (int i=0; i < numBalls; i++) { // Move the ball ballX[i] += ballXSpeed[i]; ballY[i] += ballYSpeed[i]; // See if it goes off the edge anywhere if ((ballX[i] < 0) || (ballX[i] >= size().width)) { ballXSpeed[i] = -ballXSpeed[i]; } if ((ballY[i] < 0) || (ballY[i] >= size().height)) { ballYSpeed[i] = -ballYSpeed[i]; } } repaint(); } public void start() { Timer timer = new Timer(this, 100); timer.start(); } public void stop() { timer.stop(); timer = null; } }
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f5-1.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f5-2.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f5-3.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f5-4.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f5-5.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f5-6.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f5-7.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f5-8.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f5-9.gif
CONTENTS
G G
G G
Images in Java Displaying Simple Images H Shrinking and Stretching Images Creating Your Own Images Displaying Other Image Formats H The Microsoft Windows Bitmap (BMP) File Format Manipulating Images H Performing Image-Processing Algorithms Filtering Image Colors H Filtering Based on Pixel Position Downloading Images
Images in Java
Java applets frequently need to display images. Sometimes, these images are GIF and JPEG files downloaded from a Web server. Other times, they are images that are created internally by the applet. You can also create classes that load Java images in formats other than GIF and JPEG. Once you create a Java image, you can either display it using the AWT Graphics class, or you can apply different filters to change the appearance of the image. When you display an image, you can either draw it as is, or resize it. Java's image filtering mechanism is very powerful. It allows you to create classes that change the appearance of an image. Because the filters are implemented as classes, once you create a filter that performs a specific visual effect, you can use the filter in any number of applets and applications. Since images tend to take a while to download, Java allows your applet to run while the images are still downloading. It provides ways to track images so you can tell when they finish downloading, or when there is an error in downloading. Although it isn't always a good idea, you can even wait for all the images to finish downloading before starting your applet.
instances of the Image class. The getImage method in the Applet class fetches a GIF or JPEG image from a URL and creates an instance of Image. Listing 4.1 shows an applet that loads an image and displays it.
Listing 4.1 Source Code for DrawImage.java import java.applet.Applet; import java.awt.Graphics; import java.awt.Image; // This is a simple example applet that loads an image and // displays it. public class DrawImage extends Applet { Image image; public void init() { image = getImage(getDocumentBase(), "samantha.gif"); } public void paint(Graphics g) { g.drawImage(image, 10, 10, this); } }
Figure 4.1 shows the image displayed by this program. Figure 4.1 : Java can display any GIF or JPEG file.
Listing 4.2 Source Code for ShrinkStretch.java import import import import java.applet.Applet; java.awt.Graphics; java.awt.Image; java.awt.MediaTracker;
// This applet takes an image and displays it stretched and shrunk. public class ShrinkStretch extends Applet { Image image; public void init() { // Get the image image = getImage(getDocumentBase(), "samantha.gif"); // Create a media tracker to wait for the image MediaTracker tracker = new MediaTracker(this); // Tell the media tracker to watch the image tracker.addImage(image, 0); // Wait for the image to be loaded try { tracker.waitForAll(); } catch (Exception ignore) { } } public void paint(Graphics g) { // Get the width of the image int width = image.getWidth(this); // Get the height of the image int height = image.getHeight(this); // Draw the image in its normal size g.drawImage(image, 10, 10, width, height, this); // Draw the image at half-size. g.drawImage(image, width+20, 10, width / 2, height / 2, this); // // // // // Draw the image at twice its size. Notice that the x coordinate for this image is width * 3 / 2 + 30. The 30 represents a 10-pixel padding between each image, for 3 images. The 3/2 represents the total image size of the previous two images. One full image, plus one half the original size. g.drawImage(image, width * 3 / 2 + 30, 10, width * 2, height * 2, this); } }
Figure 4.2 shows the output from this applet. The image on the left is the untouched image. The middle image is half the size of the original, and the image on the right is twice the size of the original. Figure 4.2 : Java automatically shrinks and stretches images. Generally, images stretch better than they shrink. When you shrink an image, you are losing some part of the picture because there are fewer pixels. When you stretch an image, on the other hand, you don't lose any pixels. In addition, the stretching works best when the new size is a multiple of the original size. In other words, it is better to double or triple the size of an image rather than increasing it by only 50 percent. Figure 4.3 shows an image that has been stretched by 50 percent next to an image whose size has been doubled. Notice how the image on the left shows "stretch marks" where some areas are stretched a little more than others. Figure 4.3 : Images scale better in whole multiples.
Listing 4.3 Source Code for MemoryImage.java import java.applet.*; import java.awt.*; import java.awt.image.*; // This applet creates an image from an array of color values // and displays it. public class MemoryImage extends Applet { // Create some shortcut constants for yellow, black, and white protected static final int y = Color.yellow.getRGB(); protected static final int b = Color.black.getRGB(); protected static final int w = Color.white.getRGB(); // Define an array of pixel values. The pixels will be converted // into a 1616 image. protected static w, w, w, w, w, w, w, y, w, w, y, y, w, y, y, y, final y, y, y, y, y, y, b, b, int imageData[] = y, y, y, y, y, y, y, y, y, y, y, y, y, y, y, y, y, y, y, y, y, y, b, b, { w, y, y, y,
w, w, y, y,
w, w, w, y,
w, w, w, w,
y, y, y, y, y, y, y, y, w, w, w, w, };
y, y, y, y, y, y, y, y, y, w, w, w,
y, y, y, y, y, y, y, y, y, y, w, w,
y, y, y, y, y, b, y, y, y, y, y, w,
b, y, y, y, y, y, b, y, y, y, y, y,
b, y, y, y, y, y, y, b, y, y, y, y,
y, y, y, y, y, y, y, b, y, y, y, y,
y, y, y, y, y, y, y, y, b, y, y, y,
y, y, y, y, y, y, y, y, b, y, y, y,
y, y, y, y, y, y, y, b, y, y, y, y,
b, y, y, y, y, y, y, b, y, y, y, y,
b, y, y, y, y, y, b, y, y, y, y, y,
y, y, y, y, y, b, y, y, y, y, y, w,
y, y, y, y, y, y, y, y, y, y, w, w,
y, y, y, y, y, y, y, y, y, w, w, w,
y, y, y, y, y, y, y, y, w, w, w, w
Image smiley; public void init() { // Create an image from the array of pixels smiley = createImage( new MemoryImageSource(16, 16, imageData, 0, 16)); } public void paint(Graphics g) { // Display the image, stretched considerably from its original 1616 // to a size of 128128. g.drawImage(smiley, 10, 10, 128, 128, this); } }
Figure 4.4 shows the output from this applet. Figure 4.4 : You can define your own image from an array of pixels.
Tip Whenever you create a class that understands a particular image format, make sure that it can use an InputStream object. This allows you to read in images from either a file or an URL.
Table 4.1 shows the contents of the bitmap file header. Table 4.1 Format of the Windows Bitmap File Header # of Bytes 2 4 2 2 4 Type Character 32-bit Int 16-bit Int 16-bit Int 32-bit Int Description File type (should be the characters 'B' and 'M') Total size of the file in bytes Reserved Reserved Byte-offset in file where the actual bitmap bits begin
The file type in the bitmap file header allows a program to make sure that this is a bitmap file before proceeding. If it doesn't start with "BM," it isn't a bitmap file. The byte offset for the bitmap bits is important, because there may be some padding between the headers and the actual bits. You need to know how much padding to skip over. Note
The technique of putting a special value at the beginning of a file (like 'BM') is very common. This value is often referred to as a magic number. Unix uses this same technique to identify the type of a file.
Table 4.2 shows the contents of the bitmap info header. Table 4.2 Format of the Windows Bitmap Info Header # of Bytes 4 4 4 2 2 4 4 4 4 4 4 Type 32-bit Int 32-bit Int 32-bit Int 16-bit Int 16-bit Int 32-bit Int 32-bit Int 32-bit Int 32-bit Int 32-bit Int 32-bit Int Description Size (in bytes) of the info header Width of bitmap (in pixels) Height of bitmap (in pixels) Number of bitplanes (should be 1) Number of bits per pixel (should be 1, 4, 8, or 24) Type of compression used Actual number of bytes in bitmap (only necessary if compression is used) Number of horizontal pixels per meter (used for scaling) Number of vertical pixels per meter (used for scaling) Number of colors actually used Number of colors that are really important (helps when reducing the number of colors)
After the bitmap info header is a table of colors. The colors are stored in a format called RGBQUAD, which consists of 4 bytes. An RGBQUAD value contains an 8-bit blue intensity value, an 8-bit green intensity, an 8-bit red intensity, and 8-bits of 0. This may look backwards to you when you are used to thinking of colors in the order red-green-blue, but remember that this is actually just a 32-bit number stored in little-endian format. If you were to read in an RGBQUAD and perform the necessary byte-order adjustments, you end up with a normal RGB color value. For example, suppose an RGBQUAD contained the bytes 0x56, 0x34, 0x12, and 0. If you read this value in and converted it from a little-endian number to a Java big-endian number, you would have an RGB color value of 0x123456. The number of colors in the color table is given in the bitmap info header as the number of colors actually used. Note If a bitmap contains pixels using 24-bit color, there is no color table because the actual color values are stored in the pixel bits.
If the compression type in the bitmap info header is 0, then no compression is used. If the compression type is 1, the bitmap uses RLE8 compression. A compression type of 2 indicates that the bitmap uses RLE4 compression. RLE4 and RLE8 compression are both simple run-length encoding schemes. The only difference is that RLE4 is used when you have 4-bit pixels, and RLE8 is used when you have 8-bit pixels. Basically, these two encoding schemes consist of a number of 2-byte codes and pixel values. A 2-byte code can contain a repeat count and a pixel value. The count indicates
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch4.htm (7 of 28) [8/14/02 10:54:29 PM]
how many times in a row the pixel value is repeated. If the first byte of the code is 0, the second byte can indicate a number of things. It might indicate that you should skip to the next line, or you should skip to a certain x,y position, or that there are a certain number of unencoded bytes following this 2-byte code. You could completely ignore the run-length encoding and still support most of the Windows bitmaps you find. Very few of them actually use the run-length encoding. The most peculiar thing about windows bitmaps is that they are stored upside-down. The last line in the bitmap is stored first, and the first line is stored last. Listing 4.4 shows the BMPReader class that reads a Windows Bitmap from an input stream and creates an image.
Listing 4.4 Source Code for BMPReader.java import java.awt.*; import java.awt.image.*; import java.io.*; // // // // // // This class provides a public static method that takes an InputStream to a Windows .BMP file and converts it into an ImageProducer via a MemoryImageSource. You can fetch a .BMP through a URL with the following code: URL url = new URL( <wherever your URL is> ) Image img = createImage(BMPReader.getBMPImage(url.openStream()));
public class BMPReader extends Object { // Constants indicating how the data is stored public static final int BI_RGB = 0; public static final int BI_RLE8 = 1; public static final int BI_RLE4 = 2; public static ImageProducer getBMPImage(InputStream stream) throws IOException { // The DataInputStream allows you to read in 16 and 32 bit numbers DataInputStream in = new DataInputStream(stream); // Verify that the header starts with 'BM' if (in.read() throw new } if (in.read() throw new } != 'B') { IOException("Not a .BMP file"); != 'M') { IOException("Not a .BMP file");
// Get the total file size int fileSize = intelInt(in.readInt()); // Skip the 2 16-bit reserved words in.readUnsignedShort();
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch4.htm (8 of 28) [8/14/02 10:54:29 PM]
in.readUnsignedShort(); int bitmapOffset = intelInt(in.readInt()); int bitmapInfoSize = intelInt(in.readInt()); int width = intelInt(in.readInt()); int height = intelInt(in.readInt()); // Skip the 16-bit bitplane size in.readUnsignedShort(); int bitCount = intelShort(in.readUnsignedShort()); int compressionType = intelInt(in.readInt()); int imageSize = intelInt(in.readInt()); // Skip pixels per meter in.readInt(); in.readInt(); int colorsUsed = intelInt(in.readInt()); int colorsImportant = intelInt(in.readInt()); if (colorsUsed == 0) colorsUsed = 1 << bitCount; int colorTable[] = new int[colorsUsed]; // Read the bitmap's color table for (int i=0; i < colorsUsed; i++) { colorTable[i] = (intelInt(in.readInt()) & 0xffffff) + 0xff000000; } // Create space for the pixels int pixels[] = new int[width * height]; // Read the pixels from the stream based on the compression type if (compressionType == BI_RGB) { if (bitCount == 24) { readRGB24(width, height, pixels, in); } else { readRGB(width, height, colorTable, bitCount, pixels, in); } } else if (compressionType == BI_RLE8) { readRLE(width, height, colorTable, bitCount, pixels, in, imageSize, 8); } else if (compressionType == BI_RLE4) { readRLE(width, height, colorTable, bitCount, pixels, in, imageSize, 4); } // Create a memory image source from the pixels return new MemoryImageSource(width, height, pixels, 0,
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch4.htm (9 of 28) [8/14/02 10:54:29 PM]
width); } // Reads in pixels in 24-bit format. There is no color table, and the // pixels are stored in 3-byte pairs. Oddly, all windows bitmaps are // stored upside-down - the bottom line is stored first. protected static void readRGB24(int width, int height, int pixels[], DataInputStream in) throws IOException { // Start storing at the bottom of the array for (int h = height-1; h >= 0; h--) { int pos = h * width; for (int w = 0; w < width; w++) { // Read in the red, green, and blue components int red = in.read(); int green = in.read(); int blue = in.read(); // Turn the red, green, and blue values into an RGB color with // an alpha value of 255 (fully opaque) pixels[pos++] = 0xff000000 + (red << 16) + (green << 8) + blue; } } }
The readRGB method is a good example of how to extract bits that have been packed into a byte. It computes the number of pixels stored in a byte by dividing 8 by the number of bits per pixel. When you extract bits from a byte, you have to shift the byte to the right and mask out everything but the bits you are interested in. For example, if you want the leftmost 2 bits in a byte, you shift the byte 6 bits to the right, then AND the byte with 3 (3 is the bit mask for 2 bits). The general formula for an n-bit bit mask is (1 << n) - 1. For instance, for a 2-bit mask, it's (1 << 2) - 1, which is 4-1, or 3. The readRGB method computes an array of shift values indicating how many bits to shift for each pixel stored in the byte. For instance, if you are storing 4 pixels per byte (i.e., 2-bit pixels), you will have 4 shift values which are 6, 4, 2, and 0. That is, for the first pixel value, you shift the byte 6 bits to the right. For the second pixel, you shift 4 bits to the right. Note that these shifts are not cumulative. You are always starting with the original byte.
Listing 4.4 Source Code for BMPReader.java (continued) // readRGB reads in pixels values that are stored uncompressed. // The bits represent indices into the color table. protected static void readRGB(int width, int height, int colorTable[], int bitCount, int pixels[], DataInputStream in) throws IOException
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch4.htm (10 of 28) [8/14/02 10:54:29 PM]
{ // How many pixels can be stored in a byte? int pixelsPerByte = 8 / bitCount; // A bit mask containing the number of bits in a pixel int bitMask = (1 << bitCount) - 1; // The shift values that will move each pixel to the far right int bitShifts[] = new int[pixelsPerByte]; for (int i=0; i < pixelsPerByte; i++) { bitShifts[i] = 8 - ((i+1) * bitCount); } int whichBit = 0; // Read in the first byte int currByte = in.read(); // Start at the bottom of the pixel array and work up for (int h=height-1; h >= 0; h--) { int pos = h * width; for (int w=0; w < width; w++) { // Get the next pixel from the current byte pixels[pos] = colorTable[ (currByte >> bitShifts[whichBit]) & bitMask]; pos++; whichBit++; // If the current bit position is past the number of pixels in // a byte, we advance to the next byte if (whichBit >= pixelsPerByte) { whichBit = 0; currByte = in.read(); } } } } // readRLE reads run-length encoded data in either RLE4 or RLE8 format. protected static void readRLE(int width, int height, int colorTable[], int bitCount, int pixels[], DataInputStream in, int imageSize, int pixelSize) throws IOException { int x = 0; int y = height-1;
// You already know how many bytes are in the image, so only go // through that many. for (int i=0; i < imageSize; i++) { // RLE encoding is defined by two bytes int byte1 = in.read(); int byte2 = in.read(); i += 2; // If byte 0 == 0, this is an escape code if (byte1 == 0) { // If escaped, byte 2 == 0 means you are at end of line if (byte2 == 0) { x = 0; y--; // If escaped, byte 2 == 1 means end of bitmap } else if (byte2 == 1) { return; // if escaped, byte 2 == 2 adjusts the current x and y by // an offset stored in the next two words } else if (byte2 == 2) { int xoff = (char) intelShort( in.readUnsignedShort()); i+= 2; int yoff = (char) intelShort( in.readUnsignedShort()); i+= 2; x += xoff; y -= yoff; // If escaped, any other value for byte 2 is the number of bytes // that you should read as pixel values (these pixels are not // run-length encoded) } else { int whichBit = 0; // Read in the next byte int currByte = in.read(); i++; for (int j=0; j < byte2; j++) { if (pixelSize == 4) { // The pixels are 4-bits, so half the time you shift the current byte // to the right as the pixel value if (whichBit == 0) { pixels[y*width+x] = colorTable[(currByte >> 4) & 0xf]; } else {
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch4.htm (12 of 28) [8/14/02 10:54:29 PM]
// The rest of the time, you mask out the upper 4 bits, save the pixel // value, then read in the next byte pixels[y*width+x] = colorTable[currByte & 0xf]; currByte = in.read(); i++; } } else { pixels[y*width+x] = colorTable[currByte]; currByte = in.read(); i++; } x++; if (x >= width) { x = 0; y--; } } // The pixels must be word-aligned, so if you read an uneven number of // bytes, read and ignore a byte to get aligned again. if ((byte2 & 1) == 1) { in.read(); i++; } } // If the first byte was not 0, it is the number of pixels that // are encoded by byte 2 } else { for (int j=0; j < byte1; j++) { if (pixelSize == 4) { // If j is odd, use the upper 4 bits if ((j & 1) == 0) { pixels[y*width+x] = colorTable[(byte2 >> 4) & 0xf]; } else { pixels[y*width+x+1] = colorTable[byte2 & 0xf]; } } else { pixels[y*width+x+1] = colorTable[byte2]; } x++; if (x >= width) { x = 0; y--; } } } } }
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch4.htm (13 of 28) [8/14/02 10:54:29 PM]
// intelShort converts a 16-bit number stored in intel byte order into // the local host format protected static int intelShort(int i) { return ((i >> 8) & 0xff) + ((i << 8) & 0xff00); } // intelInt converts a 32-bit number stored in intel byte order into // the local host format protected static int intelInt(int i) { return ((i & 0xff) << 24) + ((i & 0xff00) << 8) + ((i & 0xff0000) >> 8) + ((i >> 24) & 0xff); } }
The intelShort and intelInt methods in Listing 4.4 are extremely handy methods that really belong in their own class. They convert numbers from little-endian to big-endian byte order. You can actually use these methods to convert both ways. If you use intelInt on a big-endian number, it returns a little-endian number. The same holds true for intelShort.
Manipulating Images
Java's producer-consumer model makes it simple to create filters that provide many interesting image effects. Just to refresh your memory, an image producer provides the data for an image. An image consumer takes the image data and displays it. When you create an image from an URL, the data read from that URL serves as the image producer. When you create an image from an in-memory array, the MemoryImageSource is the image producer. To display an image, you connect an image producer to an image consumer and the image consumer displays the image. An image filter works like both a producer and a consumer. It acts like a consumer when it receives pixel data from the producer; then it acts like a producer when it sends the pixel data on to the consumer. Depending on the image effect you are creating, you may have to create a complete in-memory copy of the image before passing it on to the consumer. Other times, you may be able to take the array of pixels passed to you, manipulate it, and pass it on to the consumer. For example, if you want to rotate an image 90 degrees, you do not have to create an in-memory image. You only need to recompute the position of the pixels you receive. Listing 4.5 shows a filter that performs a 90-degree rotation of an image.
Listing 4.5 Source Code for Rotate Filter.java import java.awt.image.*; // This filter rotates an image 90 degrees by reversing the horizontal // coordinates and then exchanging the x and y coordinates of each // pixel. public class RotateFilter extends ImageFilter {
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch4.htm (14 of 28) [8/14/02 10:54:29 PM]
public RotateFilter() { } // // // // Since you flip the image, if the image is delivered in either complete scan lines or top-down, left-right order, you won't be passing the data to the consumer that way, so filter out those flags from the hints. public void setHints(int hints) { consumer.setHints(hints & ~(ImageConsumer.COMPLETESCANLINES + ImageConsumer.TOPDOWNLEFTRIGHT)); } // Because you exchange x and y coordinates, width is now height and // height is now width. public void setDimensions(int width, int height) { consumer.setDimensions(height, width); } // To rotate the pixels, create a new array and copy over the // pixels, reversing the horizontal pixels and then swapping // x and y. public void setPixels(int x, int y, int width, int height, ColorModel model, byte[] pixels, int offset, int scansize) { // Create a new array for the pixels byte[] rotatePixels = new byte[pixels.length]; for (int ry=0; ry < height; ry++) { for (int rx=0; rx < width; rx++) { // copy in the pixels with reversed x and y rotatePixels[rx*height + ry] = pixels[(ry+1)*scansize-rx-1+offset]; } } consumer.setPixels(y, x, height, width, model, rotatePixels, 0, height); } // To rotate the pixels, create a new array and copy over the // pixels, reversing the horizontal pixels and then swapping // x and y. public void setPixels(int x, int y, int width, int height, ColorModel model, int[] pixels, int offset, int scansize) { // Create a new array for the pixels
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch4.htm (15 of 28) [8/14/02 10:54:29 PM]
int[] rotatePixels = new int[pixels.length]; for (int ry=0; ry < height; ry++) { for (int rx=0; rx < width; rx++) { // copy in the pixels with reversed x and y rotatePixels[rx*height + ry] = pixels[(ry+1)*scansize-rx-1+offset]; } } consumer.setPixels(y, x, height, width, model, rotatePixels, 0, height); } }
Listing 4.6 shows an applet that uses this filter to display an image and a rotated version of the image.
Listing 4.6 Source Code for RotateApplet.java import import import import import java.applet.Applet; java.awt.Graphics; java.awt.Image; java.awt.MediaTracker; java.awt.image.*;
// This applet displays an image rotated 90 degrees using the // RotateFilter image filter. public class RotateApplet extends Applet { Image image; Image origImage; public void init() { String imageName = getParameter("image"); if (imageName == null) imageName = "samantha.gif"; // Get the original image origImage = getImage(getDocumentBase(), imageName); // // // // Need to wait on the image for this one. The image filters get upset if you try to filter an image that hasn't been loaded yet. You should really display alternate information rather than just waiting for the image, though.
MediaTracker mt = new MediaTracker(this); mt.addImage(origImage, 0); try { mt.waitForAll(); } catch (Exception e) { } // Now filter the image image = createImage(new FilteredImageSource( origImage.getSource(), new RotateFilter())); } public void paint(Graphics g) { g.drawImage(origImage, 10, 10, this); g.drawImage(image, 240, 10, this); } }
Figure 4.5 shows a plain image and an image rotated 90 degrees by this filter. Figure 4.5 : Image filters enable you to perform effects such as rotation.
Listing 4.7 Source Code for EffectFilter.java import java.awt.image.*; /** * Abstract class for implementing image effects on the * whole image. This class loads in an image and then calls * the performEffect method to perform the image effect, then
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch4.htm (17 of 28) [8/14/02 10:54:29 PM]
* it delivers the pixels to the consumer. * @author Mark Wutka */ // This class is an example of a filter that requires all the // pixels to be present before it can operate. public abstract class EffectFilter extends ImageFilter { // Storage area for image info int width; int height; int pixels[]; public EffectFilter() { } // Filter the COMPLETESCANLINES hint out of the hints. You know you won't be // presenting complete scan lines. public void setHints(int hints) { consumer.setHints(hints & ~ImageConsumer.COMPLETESCANLINES); } // When you find out the dimensions of the image, you can create the holding // area for the pixels. public void setDimensions(int width, int height) { this.width = width; this.height = height; this.pixels = new int[width*height]; consumer.setDimensions(width, height); } // // // // // An image filter has two different versions of setPixels. This one takes an array of bytes as the pixel values. This implies that the color model is an indexed color model. Because this filter needs pixels in RGB format, you just get the RGB value from the color model and put it into our array of pixels. public void setPixels(int x, int y, int width, int height, ColorModel model, byte[] pixels, int offset, int scansize) { // Process every row in the source array for (int i=0; i < height; i++) { // Shortcuts to save some computation time int destLineOffset = (y+i)*width;
int srcLineOffset = i*scansize+offset; // Process every pixel in the row for (int j=0; j < width; j++) { // Get the pixel value, make sure it is unsigned (the &0xff does this) int pixel = pixels[srcLineOffset+j]&0xff; // Get the RGB value this.pixels[destLineOffset+x+j] = model.getRGB(pixel); } } } // You don't actually know if the color model here is the RGB color // model or not, so just treat it like it might be an indexed model. public void setPixels(int x, int y, int width, int height, ColorModel model, int[] pixels, int offset, int scansize) { // Process every row in the source array for (int i=0; i < height; i++) { // Shortcuts to save some computation time int destLineOffset = (y+i)*width; int srcLineOffset = i*scansize+offset; // Process every pixel in the row for (int j=0; j < width; j++) { // Get the pixel value, make sure it is unsigned (the &0xff does this) int pixel = pixels[srcLineOffset+j]; // Get the RGB value this.pixels[destLineOffset+x+j] = model.getRGB(pixel); } } } // // // // // When the image producer is finished sending us pixels it calls imageComplete. You take this opportunity to perform the effect and then send all the pixels to our consumer before passing on the imageComplete call to the consumer. Up to this point the consumer doesn't know anything about our pixels. It's about to learn!
public void imageComplete(int status) { // Do the effect performEffect(); // Send the pixels to the consumer
deliverPixels(); // You're done now! super.imageComplete(status); } public abstract void performEffect(); // deliverPixels sends the whole array of pixels to the consumer in one shot protected void deliverPixels() { consumer.setPixels(0, 0, this.width, this.height, ColorModel.getRGBdefault(), this.pixels, 0, this.width); } }
Listing 4.8 shows an image effect that performs an imaging algorithm called an emboss. The emboss filter looks rather confusing, but it is actually fairly simple. It assumes that you are shining a light from the upper-left corner of the image, and that a light pixel is typically higher (closer to the front) than a dark pixel. Given these two conditions, it looks at each pixel and examines the 8 surrounding pixels. The emboss algorithm applies a weighting matrix to the surrounding pixels. The upper left pixel is given a weight of -2, the top and left pixels are given a weight of -1. The lower right pixel is given a weight of 2, and the bottom and right pixels are given a weight of 1. You multiply the color values of these pixels by their weights, add the values together, and divide by 8. This creates a weighted average. What this really does is compute a slope. If you were walking from the upper left pixel to the lower right pixel, it would decide whether you were going uphill or downhill. If the weighted average is negative, you would be walking downhill, because the upper and left pixels would be lighter (they have higher pixel values) than the lower- right pixels. This slope is then used to either lighten or darken the current pixel. If it is an uphill slope, it would catch more light, so the pixel is lightened. If it is a downhill slope, it would catch less light, so it is darkened. Rather than lightening and darkening the existing pixel, the algorithm starts with a uniform gray value for each pixel. This essentially transfers the slopes of the original image onto a plain gray image without transferring the colors themselves. The end result is an interesting emboss effect that gives the image a 3-D look.
Listing 4.8 Source Code for EmbossFilter.java public class EmbossFilter extends EffectFilter { public EmbossFilter() { } // // // // This is where the actual emboss effect is performed. It uses an edge-detection matrix and maps the edge value onto a field of all gray. When it does this, it is essentially "bending" the gray by adding shadows where all the edges are. This creates a neat
// embossing effect. public void performEffect() { // newPixels holds the new embossed image int newPixels[] = new int[width*height]; // For each pixel, compute the embossing values. You start one pixel down // and in because the edge-detection needs to look one pixel in every // direction and this keeps us from running off the edge. for (int y=1; y < height-1; y++) { int lineOffset = y * width; for (int x = 1; x < width-1; x++) { int pointOffset = lineOffset+x; int redSum = 0; int greenSum = 0; int blueSum = 0; // // // // // // // Perform the edge detection - the matrix used is: -2 -1 0 -1 0 1 0 1 2 These values are applied individually to the red, green and blue components, then the values are normalized and added to the plain gray image to "bend" or "crinkle" it. redSum -= 2*((pixels[pointOffset-width-1] >> 16)&0xff); greenSum -= 2*((pixels[pointOffset-width-1] >> 8)&0xff); blueSum -= 2*(pixels[pointOffset-width-1] &0xff); redSum -= ((pixels[pointOffset-width] >> 16)&0xff); greenSum -= ((pixels[pointOffset-width] >> 8)&0xff); blueSum -= (pixels[pointOffset-width] &0xff); redSum -= ((pixels[pointOffset-1] >> 16)&0xff); greenSum -= ((pixels[pointOffset-1] >> 8)&0xff); blueSum -= (pixels[pointOffset-1] &0xff); redSum += 2*((pixels[pointOffset+width+1] >> 16)&0xff); greenSum += 2*((pixels[pointOffset+width+1] >> 8)&0xff); blueSum += 2*(pixels[pointOffset+width+1]
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch4.htm (21 of 28) [8/14/02 10:54:29 PM]
&0xff); redSum += ((pixels[pointOffset+width] >> 16)&0xff); greenSum += ((pixels[pointOffset+width] >> 8)&0xff); blueSum += (pixels[pointOffset+width] &0xff); redSum += ((pixels[pointOffset+1] >> 16)&0xff); greenSum += ((pixels[pointOffset+1] >> 8)&0xff); blueSum += (pixels[pointOffset+1] &0xff); // Normalize the values redSum >>= 3; greenSum >>= 3; blueSum >>= 3; // Add these sums to medium-gray redSum += 0x7f; greenSum += 0x7f; blueSum += 0x7f; // Make sure the values are within the 0-255 range if if if if if if (redSum < 0) redSum = 0; (redSum > 255) redSum = 255; (greenSum < 0) greenSum = 0; (greenSum > 255) greenSum = 255; (blueSum < 0) blueSum = 0; (blueSum > 255) blueSum = 255;
// Compute the final gray value as the maximum of red, green, and blue int gray = Math.max(greenSum, Math.max(redSum, blueSum)); // Store the new value in the array (since you want the gray value for // red, green and blue, multiplying by 0x010101 will fill all 3 components // with the gray value. newPixels[pointOffset] = 0xff000000 + 0x010101 * gray; } } this.pixels = newPixels; } }
Figure 4.6 : Image embossing is one of the many imaging algorithms you can perform.
Listing 4.9 Source Code for NegativeFilter.java import java.awt.image.*; // This class is a simple RGB filter that inverts // colors by XORing the color components with // 0xff, which makes black become white and vice versa. public class NegativeFilter extends RGBImageFilter { public NegativeFilter() { canFilterIndexColorModel = true; } public int filterRGB(int x, int y, int rgb) { return (rgb & 0xff000000) + // preserve transparency (rgb & 0xffffff) ^ 0xffffff; // xor the components } }
Figure 4.7 shows an image along with its negative counterpart. Figure 4.7 : The RGBImageFilter modifies the colors of an image.
Listing 4.10 Source Code for LightingFilter.java import java.awt.image.*; /** * Simulates the presence of a white light source shining * down on an image. When you create the filter, you supply * the X,Y coordinates for the center of the light, the radius * of the light, the intensity of the light, and a fading factor * for the light. An intensity of 1.0 gives you at most the same * brightness as the original image, higher than 1.0 causes white * washout near the light (really bright light), and less than 1 means * it's pretty dark. The fade factor indicates how much the light fades * as you get farther from it. A fade of 0.0 means it doesn't fade at all. * The formula for the intensity of light is the distance from the circle * of light * the fade. If the intensity is 1 and the fade is 0.01, any point * that is 100 pixels or more away from the light will be black (since by * the intensity formula, the intensity is 1.0 - (100 * 0.01), or 0). * * @author Mark Wutka */ public class LightingFilter extends RGBImageFilter { /** the center x coordinate of the circle of light */ public int centerX; /** the center y coordinate of the circle of light */ public int centerY; /** the radius of the circle of light */ public int radius; /** the intensity of the light */ public double intensity; /** How quickly the intensity fades as you go away from the light */ public double fade; /** * Creates an instance of a lighting filter which shines a circle * of light on an image. * * @param centerX the X coordinate of the center of the light circle * @param centerY the Y coordinate of the center of the light circle * @param radius the radius of the light circle * @param intensity the intensity of the light, > 1.0 whitens the colors * within the circle (it's brighter). * @param fade how quickly the light fades as you leave the circle. If fade
* */
is >= 1.0, it is pitch black outside the circle. public LightingFilter(int centerX, int centerY, int radius, double intensity, double fade) { this.centerX = centerX; this.centerY = centerY; this.radius = radius; this.intensity = intensity;
// Can't have an intensity less than 0, an intensity of 0 is total darkness // How much blacker could it be? The answer is "none more black". - N. Tufnel if (intensity < 0.0) intensity = 0.0; this.fade = fade; // Because the lighting is position dependent, this filter // cannot filter an index color model; canFilterIndexColorModel = false; } public int filterRGB(int x, int y, int rgb) { // Save the pixel's transparency value int trans = rgb & 0xff000000; // Compute the distance from the edge of the circle (distance from center // - radius). double dist = Math.sqrt((x-centerX)*(x-centerX) + (y-centerY)*(y-centerY)) - radius; if (dist < 0.0) dist = 0.0; // Compute the intensity based on distance and fade double intense = intensity - dist * fade; // Again, none more black than 0.0 if (intense < 0.0) intense = 0.0; // Adjust the colors based on the new intensity int red = (int)(((rgb >> 16) & 0xff) * intense); // Max color value for each component is 255 if (red > 255) red = 255; int green = (int)(((rgb >> 8) & 0xff) * intense); if (green > 255) green = 255; int blue = (int)((rgb & 0xff) * intense); if (blue > 255) blue = 255; // Return the new color return trans + (red << 16) + (green << 8) + blue; } }
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch4.htm (25 of 28) [8/14/02 10:54:29 PM]
Figure 4.8 shows the output from the lighting filter. Figure 4.8 : Filters based on pixel position can create neat effects.
Downloading Images
When your applet starts up and begins downloading its images, you shouldn't wait for all the images to be loaded before your applet really starts. The MediaTracker class is nice for this, but you must be careful when using it. The media tracker allows you to wait for your images to be downloaded. Many times, this is acceptable to you. If you are creating an applet for a commercial Web page, however, you should avoid any possible delay. One option you have with the media tracker is to spawn a thread that waits for images and calls repaint when all the images have been loaded. Of course, that requires you to set up a special thread, which you also may not want to do. If your applet is not already runnable, you can create a run method and use the media tracker in the run method. If your applet already has a run method, you must set up another class that is responsible for running the media tracker. By the time you create another runnable class that uses the media tracker, you might as well just use the ImageObserver interface. The Component class, of which Applet is a subclass, contains everything you need to start downloading images and check on their progress. Your applet can implement the ImageObserver interface so it can be notified as your images are downloaded successfully. When you use the ImageObserver interface, you implement the imageUpdate method, which is called whenever there is more information available about the image. Your imageUpdate method should check the flags parameter to see when the ImageObserver.ALLBITS flag is set. When this flag is set, the image has finished loading. You can then repaint the screen using the full image. Listing 4.11 shows an applet that implements the ImageObserver interface to see when all its images have finished downloading.
Listing 4.11 Source Code for DownloadApplet.java import java.awt.*; import java.awt.image.*; import java.applet.*; // // // // // This applet acts as an image observer to watch for images to be ready. While applets are being loaded, it displays a message where the applet will be drawn. Once the image is loaded, it displays the image. If there is an error loading an image, it prints an error message in place of the image.
public class DownloadApplet extends Applet implements ImageObserver { // The three images we are loading protected Image moe; protected Image larry; protected Image curly;
public DownloadApplet() { } public void init() { // get the images, this doesn't necessarily start download them, however moe = getImage(getDocumentBase(), "moe2.gif"); larry = getImage(getDocumentBase(), "larry2.gif"); curly = getImage(getDocumentBase(), "curly2.gif"); // start downloading the images prepareImage(moe, this); prepareImage(larry, this); prepareImage(curly, this); } // // // // Show image checks the flags associated with an image. If the image is still loading, it displays the loadingMessage string. If the image had an error loading, it displays the errorMessageString. Otherwise, it displays the fully-loaded image.
protected void showImage(Graphics g, int x, int y, Image image, String loadingMessage, String errorMessage) { // Get the status of the image int flags = checkImage(image, this); // If the image aborted or had an error, print the error message if ((flags & (ImageObserver.ABORT+ImageObserver.ERROR)) != 0) { g.drawString(errorMessage, x, y+30); return; // If the image has been loaded fully, display it } else if ((flags & ImageObserver.ALLBITS) != 0) { g.drawImage(image, x, y, this); // If the image is still loading, display the loading message } else { g.drawString(loadingMessage, x, y+30); return; } } public void paint(Graphics g) { showImage(g, 10, 10, moe, "Moe's coming!", "Moe can't make it."); showImage(g, 200, 10, larry, "Larry's coming!", "Larry can't make it."); showImage(g, 390, 10, curly, "Curly's coming!",
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch4.htm (27 of 28) [8/14/02 10:54:29 PM]
"Curly can't make it."); } public boolean imageUpdate(Image img, int flags, int x, int y, int width, int height) { // Whenever an image's status changes, imageUpdate gets called. If the // image aborts, has an error, or is complete, we call repaint to redraw // the image with the updated information. if ((flags & ImageObserver.ALLBITS+ImageObserver.ABORT+ ImageObserver.ERROR) != 0) { repaint(); } // Otherwise, if we just got more pixels or something else, there's // no need to repaint, we don't need to change the current message. return true; } }
Figure 4.9 shows the output of the applet while it is waiting for the image to be downloaded. Figure 4.9 : You should display alternate information while waiting for an image to download.
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f4-1.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f4-2.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f4-3.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f4-4.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f4-5.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f4-6.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f4-7.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f4-8.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f4-9.gif
CONTENTS
G G G G G
Applet Security File Access Restrictions Network Restrictions Other Security Restrictions Getting Around Security Restrictions H Using Digital Signatures for Increased Access H Creating a Customized Security Manager
Applet Security
Applet security is generally regarded as a necessary pain by most Java programmers. The ability to download code on-the-fly is a major advantage, but it is also a wonderful tool for the same kind of people who like to write viruses that infect your PC. Fortunately, the designers of Java took that into account and developed a security model that protects your system from malicious attacks. You may consider some of the applet security restrictions draconian, but it is much better to have too much security than too littleespecially when Java is still striving for acceptance. Theserestrictions do not apply to applications, because they are meant to access local files and the local network. The security restrictions are there to protect you from unknowingly loading a malicious program that can be hidden on a Web page. You have to manually run an application on your local system, however, so you are responsible if the application is malicious. Security restrictions vary from browser to browser. Netscape, for instance, has a very tight security model, although HotJava allows you to switch off some of the security restrictions. The Microsoft Internet Explorer version 3 (IE3) supports several security models from completely relaxed (no restrictions) to completely secure (won't download and run applets at all). In addition, IE3 allows digitally signed classes to have fewer restrictions.
Tip Many browsers, including HotJava, IE3, and Netscape, relax the security policy for applets that are loaded from files on the local system-that is, files that are loaded with a type of "file:". If you load a file with "http:", even if the file is stored on your local drive, you will be under the full scrutiny of the security manager.
Network Restrictions
The network restrictions in Java may seem a little overboard, but they are there for good reason. The general philosophy of network security is that applets can only make network connections back to the Web server they were loaded from. An applet may not listen for incoming socket connections, nor can it listen for datagrams (connectionless network data) from anywhere but its home server. It also can only send datagrams back to its home server. These security restrictions are intended to protect organizations that have Internet firewalls set up. In case you are unfamiliar with the intricacies of Internet security, many companies have large internal IP networks (the main networking protocol of the Internet). These networks are connected to the rest of the world through machines called "firewalls." A firewall's job in life is to protect the internal IP network from prying eyes in the outside world while allowing people on the inside to access data out on the Internet. These firewalls usually render the internal network invisible to the rest of the world. Given the clever ways people have found to attack systems, it is best to not give out any information about host names or addresses on the internal network. The problem with Java is that applets run inside the firewall on your local machine. This means that without any network restrictions, your entire network is exposed to any malicious applets. You might be thinking that it would be nice if you could just tell your browser the names of hosts that you trust. It would not be difficult for the security system in Java to handle that, but it would keep your poor network administrator on
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch3.htm (2 of 9) [8/14/02 10:54:47 PM]
a steady supply of indigestion medication, wondering when someone will trust an untrustworthy host. If you're an administrator at a site using HotJava, go ahead and get yourself a good spoonful of Maalox-you can completely turn off the networking restrictions in HotJava! Keep in mind, also, that Internet Explorer also lets you turn off all security restrictions. Netscape does not support such an option, however. If your applet is loaded from the local filesystem, you can get around these security restrictions. You may have to set the appletviewer.security.mode system property to unrestricted to completely get around these restrictions. Because one of the other restrictions on applets is that they cannot change the system properties, you'll have to come up with unique ways of getting around this.
When a non-local applet opens a top-level frame (a window separate from the browser), the frame contains a warning message indicating that the applet is not trusted.
To digitally sign your code for Internet Explorer you must have a Software Publishers Certificate, signed by a trusted certificate authority. You can find information on obtaining this certificate from Microsoft's Web server at http://www.microsoft.com/intdev/signcode.
Once you have created a cabinet file, use the SignCode program to digitally sign your code. If you simply type signcode on the command line, you will be presented with a handy step-by-step windowed interface for signing code. You can also use the command-line version of signcode. If your Software Publishers Certificate is in the file MyCert.spc and you want to use a private key called MyKey to sign MyCab.cab, you would use the following command: signcode -name MyCab.cab -spc MyCert.spc -pvk MyKey Once your cabinet is signed, any class loaded from that cabinet is trusted by Internet Explorer and is allowed free access to the local system. Java 1.1 includes support for digitally signed Java classes. Under Sun's security policy, you are able to restrict access based on the signature. If a class is signed by Sun, you might permit it full access to your system. If the class is signed by a vendor that you do not completely trust, however, you might give it only limited abilities. Because digital signatures are a part of Java 1.1, eventually all Java-enabled browsers will contain code to support digitally signed applets. These applets will be given much more freedom to access the local system. For now, however, if you really need to create applets that have little or no security restrictions, you have to create your own custom security manager.
// Uncomment one of these to create a security manager // for the browser of your choice // package Netscape.applet; // package sun.applet; import Java.io.FileDescriptor; import Java.net.URL; public class AppletSecurity extends SecurityManager { public void checkAccept(String host, int port) { } public void checkAccess(Thread g) { } public void checkAccess(ThreadGroup g) { } public void checkConnect(String host, int port) { } public void checkConnect(String host, int port, Object context) { } public void checkCreateClassLoader() { } public void checkDelete(String file) { } public void checkExec(String cmd) { } public void checkExit(int status) { }
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch3.htm (6 of 9) [8/14/02 10:54:47 PM]
public void checkLink(String lib) { } public void checkListen(int port) { } public void checkPackageAccess(String pkg) { } public void checkPackageDefinition(String pkg) { } public void checkPropertiesAccess() { } public void checkPropertyAccess(String key) { } public void checkRead(FileDescriptor fd) { } public void checkRead(String file) { } public void checkRead(String file, Object context) { } public boolean checkTopLevelWindow(Object window) { return true; } public void checkURLConnect(URL url) { } public void checkWrite(FileDescriptor fd)
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch3.htm (7 of 9) [8/14/02 10:54:47 PM]
Once you have created your own custom security manager, you can install it over the existing security manager in the browser you are using. Netscape stores its classes either in moz2_x.zip (moz2_0.zip, moz2_01.zip, and so on) or moz3_x.zip, depending on whether it is Netscape 2.x or Netscape 3.x. HotJava stores its classes in classes.zip. Internet Explorer also stores its files in classes.zip, which is usually found in the C:\WINDOWS\JAVA\CLASSES directory. You'll need a zip program to replace the old security manager. See the section "Creating Your Own Archive File with Info-ZIP" in Chapter 14, "Creating Your Own Class Archive Files," for more information on zip programs and creating Java class .zip files. To install this security manager in a Netscape mozxxx.zip (moz2_0.zip, moz2_1.zip, and so on) file, perform the following steps: 1. Go to the directory in which the mozxxx.zip file is stored. Under Win 95/NT, this is probably Program Files\Netscape\Navigator\Program\Java\classes. 2. Create a subdirectory called Netscape, and then create a subdirectory under that called applet. 3. Copy the AppletSecurity.class file you compiled into the Netscape/applet (or Netscape\applet) subdirectory. 4. Make a backup copy of the mozxxx.zip file; you'll need it if you want to go back to the old security manager. 5. zip -0 -u mozxxx.zip Netscape/applet/AppletSecurity.class The procedure for HotJava is almost identical: 1. Go to the directory in which the classes.zip file is stored. Under Win 95/NT, this is probably \hotJava\lib. 2. Create a subdirectory called sun, and then create a subdirectory under that called applet. 3. Copy the AppletSecurity.class file you compiled into the sun/applet (or sun\applet) subdirectory. 4. Make a backup copy of the classes.zip file; you'll need it if you want to go back to the old security manager. 5. zip -0 -u classes.zip sun/applet/AppletSecurity.class
The procedure for installing your own security manager for Internet Explorer is also very similar: 1. Go to the directory in which the classes.zip file is stored. It should be C:\WINDOWS\JAVA\CLASSES, but may be slightly different if your Windows directory is in a different place. For instance, it might be C:\WINNT\JAVA\CLASSES. 2. Create a subdirectory called com, and then create a subdirectory under that called ms. Under the com\ms directory, create another subdirectory called applet. 3. Copy the AppletSecurity.class file you compiled into the com\ms\applet subdirectory. 4. Make a backup copy of the classes.zip file; you'll need it if you want to go back to the old security manager. 5. zip -0 -u classes.zip com/ms/applet/AppletSecurity.class The next time you start your browser, your applets should be completely unrestricted. Caution Warning! Turning off applet security like this is extremely dangerous. Don't do this unless you know what you are doing. Remember, there is a better solution coming in the form of digital signatures, so only do this if you need unrestricted applets immediately.
CONTENTS
G G G G G G
Class Archive Files Creating Your Own Archive File with Info-ZIP Viewing the Contents of a Zip Archive Adding Classes Directly to the Browser's Library Creating Class Archives with Other Zip Archivers Creating Cabinet Files for Internet Explorer
To archive the entire mylib package into a file called mylib.zip, go to the parent directory of mylib and type: zip -r -0 mylib.zip mylib The -r argument tells zip to include subdirectories in the archive. Without this argument, you would have to include the full path name of every file in the mylib directory. The -0 option is extremely important because it stores the files with no compression. If you don't use the 0 option, you still create a zip file but Java can't use it, because zip compresses files by default. Tip
When you create a zip archive, remember to add the full path name of the zip file to your CLASSPATH variable. Although class files are picked up when their directory name is in the CLASSPATH, .zip files must be mentioned explicitly. For example, if you created a file called myclasses.zip, your CLASSPATH under Windows might look like: CLASSPATH=C:\mystuff\myclasses.zip;C:\JAVA\LIB\CLASSES.ZIP.
Tip Notice the relationship between package names and subdirectories. For each "." in the class name, there is a "/" (under UNIX) or a "\" (under Windows and OS/2) in the .class file path.
If you are really daring, you can use javap -c to disassemble the code in the .class file:
javap -c sun.net.www.protocol.http.HttpURLConnection
Caution Before disassembling any code that comes with a license agreement, look over the license agreement closely. Many vendors explicitly forbid the disassembling of their code. Although they are probably more worried about you stealing their trade secrets than just seeing what their code does, you are still breaking the law if you disassemble their code without permission.
Adding classes to a zip file is no different from creating a new zip file. To add the mylib package to a class archive named classes.zip, use the following command: zip -0 classes.zip mylib You can also replace classes in a class archive. For instance, you may want to insert a dummy security manager that removes any restrictions on applets and applications. The security manager for each browser is in a different package, so you need to adjust your dummy security manager and the zip command for each browser. To insert a dummy security manager into Netscape, for instance, you need to replace netscape.applet.AppletSecurity.
The following command does that for Netscape version 2: zip -0 moz2_0.zip netscape/applet/AppletSecurity.class You need to have copied AppletSecurity.class into the netscape/applet (netscape\applet under Windows) subdirectory. Again, this is a dangerous thing to play with. You could open up your entire company's network to malicious hacking just by replacing classes in your own browser.
Once you have downloaded and unpacked the CDK, you can use the cabarc program to create cabinets. Make sure that your PATH setting includes the directory where you unpacked the CDK. The command-line for the cabarc program is very similar to that of a zip archiver: cabarc n cabfilename files For example, to pack all the .class files and all the .gif files in the current directory into a cabinet file called mycabinet.cab, you would use the following command: cabarc n mycabinet.cab *.class *.gif If you want to include a whole directory tree, add the -r and -p options to cabarc. For instance, to package the current directory, including all subdirectories, and store them in a file called mycab.cab, you would use the following command: cabarc -r -p n mycab.cab *.* Once you have created a cabinet file, you can use it in a Web page by including the following applet parameter: <PARAM name="cabbase" value="cabfilename"> The following example .html file loads an applet called MyApplet from a cabinet file called mycab.cab: <HTML> <HEAD> <TITLE>Cabinet Example</TITLE> </HEAD> <BODY> <APPLET codebase="." code="MyApplet.class" width=250 height=250> <PARAM name="cabbase" value="mycab.cab"> You need a Java enabled browser to see this program. </APPLET> </BODY> </HTML> You don't have to do anything special to access images and audio clips that are stored in cabinet files. Just access them via the codebase or document base URLs as you normally would. The IE browser will take care of the rest.
Note For Java 1.1, Sun has defined an archive format called JAR (Java ARchive) that is very similar to the cabinet format. It also supports data compression and can store multiple types of files. Since the JAR format will be part of future releases of Java, it will be available on any Java-compliant platform.
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f14-1.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f14-2.gif
CONTENTS
G G G G
Differences Between Applets and Applications Allowing an Applet to Run as an Application The Applet's Runtime Environment Creating an Applet Context
Note The term application, when used in conjunction with Java, usually indicates a program running stand-alone. The term applet always refers to a program running within a browser. Unfortunately, this separation implies that the two are always separate things. A distributed application, in the traditional sense, is made up of many components. In the Java world, some of these components may be applets, and some may be stand-alone applications, but they all fit together to make a distributed application.
Listing 13.1 Source Code for StandaloneApplet.java import java.awt.*; import java.applet.*; // // // // // StandaloneApplet is an applet that runs either as an applet or a standalone application. To run standalone, it provides a main method that creates a frame, then creates an instance of the applet and adds it to the frame.
public class StandaloneApplet extends Applet { public void init() { add(new Button("Standalone Applet Button")); } public static void main(String args[]) { // Create the frame this applet will run in Frame appletFrame = new Frame("Some applet");
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch13.htm (2 of 15) [8/14/02 10:54:55 PM]
// The frame needs a layout manager, use the GridLayout to maximize // the applet size to the frame. appletFrame.setLayout(new GridLayout(1,0)); // Have to give the frame a size before it is visible appletFrame.resize(300, 100); // // // // // // Make the frame appear on the screen. You should make the frame appear before you call the applet's init method. On some Java implementations, some of the graphics information is not available until there is a frame. If your applet uses certain graphics functions like getGraphics() in the init method, it may fail unless there is a frame already created and showing. appletFrame.show();
// Create an instance of the applet Applet myApplet = new StandaloneApplet(); // Add the applet to the frame appletFrame.add(myApplet); // Initialize and start the applet myApplet.init(); myApplet.start(); } }
Figure 13.1 shows StandaloneApplet running within a Web browser, while Figure 13.2 shows it running as a standalone application. Figure 13.1 : Many applets act as simple AWT containers. Figure 13.2 : Sometimes a simple frame is all you need to run an applet stand-alone.
The AppletContext interface provides an applet with methods for loading images and audio clips, as well as opening up new URLs in the browser, and finding out what other applets are running in the current environment. Each browser has its own AppletContext class that knows how to perform specific tasks within the browser. When a browser loads an applet, it calls setStub in the Applet object, which sets the applet's stub (as you might guess). This stub, in turn, has a method called getAppletContext, which returns the applet's AppletContext object. If you want to implement your own AppletContext object, you must also create your own AppletStub object. Otherwise, there would be no way to associate your AppletContext object with an applet-there's no setAppletContext method in the Applet class. Note As an applet programmer, you never access the AppletContext and AppletStub interfaces directly. The Applet class presents all the methods available in the AppletContext and AppletStub interfaces. The methods in the Applet class simply call the corresponding methods in the AppletContext and AppletStub interfaces. This technique is called delegation.
Figure 13.3 shows the relationship between the Applet, the AppletStub, and the AppletContext. Figure 13.3 : The applet stub is directly associated with the applet, and provides access to the applet context. If you look at the Applet class, you'll notice that it has methods for playing audio clips. Like most of the other methods provided by the Applet class (with the exception of the AWT container methods), the audio methods simply call methods in the AudioClip interface.
Listing 13.2 Source Code for RunAppletContext.java import import import import // // // // // java.applet.*; java.util.*; java.awt.*; java.net.*;
This class provides a generic applet context for standalone applications. It is implemented as a singleton object, which means that there is only one instance of this class within the runtime environment. It stores all the loaded applets in a hash table so it can provide working getApplet and getApplets methods.
public class RunAppletContext extends Object implements AppletContext, AudioClip { // The pointer to the lone instance of this class protected static RunAppletContext context; // The table of all the known applets in the runtime environment. protected Hashtable applets; protected RunAppletContext() { applets = new Hashtable(); } // Returns the lone instance of the RunAppletContext. If there isn't // an instance, it creates a new one. public synchronized static RunAppletContext instance() { if (context == null) { context = new RunAppletContext(); } return context; } // Adds an applet to the table of known applets public void addApplet(Applet applet, String name) { applets.put(name, applet); } // Locates an applet in the table public Applet getApplet(String name) { return (Applet) applets.get(name); }
// Returns an enumeration of all the known applets public Enumeration getApplets() { return applets.elements(); } // // // // Tries to load an audio clip using Sun's AppletAudioClip which is distributed with the JDK. This class may not be available in all Java implementations since it is not a documented part of the JDK. public AudioClip getAudioClip(URL url) { try { return new sun.applet.AppletAudioClip(url); } catch (Exception e) { return this; } } // Uses the AWT Toolkit class to fetch an image from a URL public Image getImage(URL url) { return Toolkit.getDefaultToolkit().getImage(url); } // // // // Since we aren't running in a browser and there aren't really any classes to render HTML in Java, we have to wimp out with the showDocument method and just print a message that the applet wanted to load a URL. public void showDocument(URL url) { System.out.println("Wanted to show document on: "+url); } public void showDocument(URL url, String target) { System.out.println("Wanted to show document on: "+url+ " in frame "+target); } // Just print to System.out for showStatus. public void showStatus(String status) { System.out.println(status); } // If we can't create an instance of sun.applet.AppletAudioClip, we
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch13.htm (6 of 15) [8/14/02 10:54:55 PM]
// return a pointer to this same object, which happens to also implement // the AudioClip interface, but it doesn't do anything with them. // The following three methods are the methods for AudioClip: public void play() {}; public void loop() {}; public void stop() {}; }
To use a custom applet context, you need a custom applet stub since the stub is the class that returns the applet context. The stub contains the very useful getDocumentBase, getCodebase, and getParameter methods. Listing 13.3 shows a handy RunAppletStub that allows you to customize the code and document bases as well as the applet parameters by using the system properties. It also returns an instance of RunAppletContext for the applet's context.
Listing 13.3 Source Code for RunAppletStub.java import java.applet.*; import java.net.*; import java.awt.*; // // // // // // // // // // // // This class provides an applet stub for applets running as standalone applications. You can set the document base by setting the "docbase" system property. Likewise, you can set the code base through the "codebase" property. You can provide applet parameters by setting system properties with the applet's name followed by the parameter. For example: <PARAM name="stooge" value="moe"> for an applet named MyApplet, could be set in this stub with by setting the system property "MyAppletstooge" to "moe". You can also just set the "stooge" property, but it will try using the appletname in front first. This allows you to run multiple applets at once that have the same parameter names.
public class RunAppletStub extends Object implements AppletStub { Frame appletFrame; Applet applet; String appletName; String startDir; public RunAppletStub() { }
// // // //
startDir is the local directory where this applet is started, or another directory if you prefer. If you don't specify a code base or a document base, the startDir is used for those. The directory separators must be '/' and not '\' or the URL class gets confused. public RunAppletStub(Frame appletFrame, Applet applet, String name, String startDir) { this.appletFrame = appletFrame; this.applet = applet; this.appletName = name; this.startDir = startDir; RunAppletContext.instance().addApplet(applet, name); } public void setParams(Frame appletFrame, Applet applet, String name, String startDir) { this.appletFrame = appletFrame; this.applet = applet; this.appletName = name; this.startDir = startDir; RunAppletContext.instance().addApplet(applet, name); } public boolean isActive() { return true; }
// Return the document base URL. Try getting the docbase system parameter. // If that isn't available, use the startDir directory. public URL getDocumentBase() { String docbase = System.getProperty("docbase"); try { if (docbase == null) { return new URL("file://"+startDir); } else { return new URL(docbase); } } catch (MalformedURLException e) { return null; } } // Return the code base URL. Try getting the codebase system parameter. // If that isn't available, use the startDir directory. public URL getCodeBase()
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch13.htm (8 of 15) [8/14/02 10:54:56 PM]
{ String codebase = System.getProperty("codebase"); try { if (codebase == null) { return new URL("file://"+startDir); } else { return new URL(codebase); } } catch (MalformedURLException e) { return null; } } // fetch a parameter for the applet from the system properties. First // try the applet name followed by the param name. If that's null, // try just the param name. public String getParameter(String paramName) { String prop = System.getProperty(appletName+paramName); if (prop != null) return prop; return System.getProperty(paramName); } public AppletContext getAppletContext() { return RunAppletContext.instance(); } // appletResize is the only reason we need a reference to the applet's // frame. If the applet wants to resize, we resize the frame, then // the applet. public void appletResize(int width, int height) { appletFrame.resize(width+10, height+20); applet.resize(width, height); } }
All you have to do in your applet to use the RunAppletStub is create the stub and call the setStub applet method. Listing 13.4 shows the stand-alone applet updated to use the RunAppletStub class.
import java.applet.*; // // // // // StandaloneApplet is an applet that runs either as an applet or a standalone application. To run standalone, it provides a main method that creates a frame, then creates an instance of the applet and adds it to the frame.
public class Standalone2 extends Applet { public void init() { add(new Button("Standalone Applet Button")); } public static void main(String args[]) { // Create the frame this applet will run in Frame appletFrame = new Frame("Some applet"); // The frame needs a layout manager, use the GridLayout to maximize // the applet size to the frame. appletFrame.setLayout(new GridLayout(1,0)); // Have to give the frame a size before it is visible appletFrame.resize(300, 100); // // // // // // Make the frame appear on the screen. You should make the frame appear before you call the applet's init method. On some Java implementations, some of the graphics information is not available until there is a frame. If your applet uses certain graphics functions like getGraphics() in the init method, it may fail unless there is a frame already created and showing. appletFrame.show();
// Create an instance of the applet Applet myApplet = new Standalone2(); // Add the applet to the frame appletFrame.add(myApplet); // Now try to get an applet stub for this class. RunAppletStub stub = new RunAppletStub(appletFrame, myApplet, "standalone-applet", "http://localhost/"); myApplet.setStub(stub); // Initialize and start the applet myApplet.init(); myApplet.start(); }
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch13.htm (10 of 15) [8/14/02 10:54:56 PM]
You have to write this special main method only a few times before you start wondering whether you couldn't create a loader that automatically did all that stuff for you. The RunApplet class, included on the CD with this book, can load multiple applets. When you start an applet, you can specify the applet's width, height, name, and starting directory. For example, the following command line starts the applet Applet1 with a size of 400300, a name of myapplet, and a starting directory of /home/mark: java RunApplet Applet1,width=400,height=300,name=myapplet,startDir=/home/mark Make sure there are no spaces in the parameters for a single applet; otherwise, they will be confused with parameters for another applet. You can run multiple applets by putting them all on the same command line. The following command runs applets named Applet1 and Applet2: java RunApplet Applet1 Applet2 Notice that the width, height, name, and startDir parameters are optional. The RunApplet class is arranged slightly differently from the preceding Standalone2 class. Most of the work that is done in the main method in Standalone2 is now in a method called StartApplet. Listing 13.5 shows the startApplet method for the RunApplet class.
Listing 13.5 startApplet Method from RunApplet.java // Creates the frame, sets the stub, starts the applet public static void startApplet(Applet applet, int width, int height, String name, String startDir) { // Create the applet's frame Frame appletFrame = new Frame(name); // Allow room for the frame's borders appletFrame.resize(width+10, height+20); // Use a grid layout to maximize the applet's size appletFrame.setLayout(new GridLayout(1, 0)); // Add the applet to the frame appletFrame.add(applet); // Show the frame, which makes sure all the graphics info is loaded // for the applet to use.
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch13.htm (11 of 15) [8/14/02 10:54:56 PM]
appletFrame.show(); // Create and set the stub AppletStub stub = new RunAppletStub(appletFrame, applet, name, startDir); applet.setStub(stub); // initialize the applet applet.init(); // Make sure the frame shows the applet appletFrame.validate(); // Start up the applet applet.start();
The bulk of the RunApplet class is taken up by the main method, which spends all its time parsing command-line arguments. For each command-line argument, the main method creates a StringTokenizer object that uses a comma as the separator. For each token, the method checks to see whether it contains any of the allowable parameters, and, if so, parses the parameter. The applet's startDir parameter is used by the applet stub to return the document base and code base URLs. The URL class requires all directories to use the forward slash (/), as opposed to the backward slash (\) used by Windows and OS/2. The main method has to scan through the startDir parameter and replace any backward slashes with forward slashes. It uses a StringBuffer object to do this. The StringBuffer class allows you to build and edit strings more efficiently than using the String class, because you can directly change the characters in a StringBuffer object. The main method simply turns the startDir parameter into a StringBuffer object, scans through the buffer replacing \s with /s, and then converts the StringBuffer object back into a String. Listing 13.6 shows the source code for the main method. Tip The technique of using a StringBuffer object to manipulate a String object is used very often by Java, but you don't always know it. Whenever you combine an integer and a string, like "Count: "+5, the Java compiler actually generates calls to the StringBuffer class to create the new string.
Listing 13.6 main Method from RunApplet.java public static void main(String[] args) { if (args.length < 1) { System.err.println("Please supply the applet name."); }
// For each arg, parse out the applet class name and other params for (int i=0; i < args.length; i++) { StringTokenizer tokenizer = new StringTokenizer( String className = null; // default to 300x200 frame int width = 300; int height = 200; String name = null; String startDir = null; while (tokenizer.hasMoreTokens()) { String token = tokenizer.nextToken(); // Look for width= parameter, if found, get the integer. If there's // an error parsing the int, just ignore it if (token.startsWith("width=")) { try { width = Integer.valueOf( token.substring(6)). intValue(); } catch (Exception ignore) { } // Look for the height parameter, ignore if there's an error } else if (token.startsWith("height=")) { try { height = Integer.valueOf( token.substring(7)). intValue(); } catch (Exception ignore) { } // Look for the optional applet name } else if (token.startsWith("name=")) { name = token.substring(5); // Normally, you just give the applet's class name in the parameter // list, but if you like, you can be more specific and say // applet=xxx. } else if (token.startsWith("applet=")) { className = token.substring(7); // Set the home directory for the applet. If not set, will // use the current directory (from System property "user.dir") } else if (token.startsWith("startdir=")) { startDir = token.substring(9);
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch13.htm (13 of 15) [8/14/02 10:54:56 PM]
} else { if (className == null) { className = token; } else { System.err.println( "Invalid parameter - "+ token); } } } if (className == null) { System.err.println( "No class name specified in: "+ args[i]); } if (name == null) name = className; // If no startDir set, use the "user.dir" property if (startDir == null) { startDir = System.getProperty("user.dir")+"//"; } // This little piece of bogosity changes any \'s in the start dir // to /'s, since the URL classes require /'s. StringBuffer buff = new StringBuffer(startDir); for (int j=0; j < buff.length(); j++) { if (buff.charAt(j) == '\\') { buff.setCharAt(j, '/'); } } // Convert the string buffer back to a string. startDir = new String(buff); // Load the applet's class try { Class appletClass = Class.forName(className); Applet runme = (Applet) appletClass. newInstance(); // Start the applet startApplet(runme, width, height, name, startDir); } catch (Exception e) { // If there's an error, just say which applet had the problem, // but don't quit. System.err.println("Error starting applet - "+
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch13.htm (14 of 15) [8/14/02 10:54:56 PM]
args[i]); System.err.println(e); } } }
CONTENTS
G G
Huffman Coding and Lempel-Ziv Compression Delayed Downloading H Delayed Instantiation H Downloading in the Background Providing Local Libraries H Installing Local Libraries for Hotjava and Appletviewer H Installing Local Libraries for Netscape H Installing Local Libraries for Internet Explorer Downloading Classes in Zipped Format H Zip Downloading in Netscape Navigator Version 3 H A Zipfile Class Loader Packaging Classes in Jars and Cabinets
Waiting for an applet to finish downloading can be annoying at times, especially for a big applet. A quote like, "Great applet, but it takes five minutes to download," does not inspire people to run out and try it. Although there aren't any tricks to shove bits through the network any faster, you can make your applets aware that things may take a while and give the user something to do while they wait. It would certainly be a boon to many users, but the ability to download classes in compressed form does not necessarily give a big speed boost to everyone. In fact, maybe not at all for the people who need it the most-the modem users. Most modems these days are able to do data compression by themselves. This saves you some time when downloading text files and other uncompressed data at rates faster than the modem's communication speed. For compressed zip files and to some extent GIF files, however, you transfer data only at the modem's normal speed. Data compression removes the redundancy in a file. The more redundant the file, the better it compresses. Note Since images usually contain a high amount of redundancy, the GIF and JPEG standards also include data compression. This is why you don't get much speed gain by transmitting images over a compressing modem.
Text data compresses very well because it is fairly redundant. Binary data does not usually compress as well as text, but certain types of binary data do. Once you compress a file and remove that redundancy, you cannot compress it any more. When a compressing modem is trying to send a compressed file, it can't find anything to compress: All the redundant information has already been removed. Although you probably won't have to write your own compression or decompression routines, it is helpful to have an idea of how a data compression routine goes about compressing data. This may give you a better idea of why some things compress better than others. The most common form of compression today is actually a combination of two popular compression algorithms-Huffman coding and Lempel-Ziv (LZ) compression.
pattern, LZ compression removes the repeated copies and inserts a pointer to the original pattern. For example, suppose you had the original DADDABC pattern. The LZ compression would take DA and call it sequence 1. Then, the next D would be replaced with a code for "repeat sequence 1 for 1 character." The next two characters, DA again, would be replaced with "repeat sequence 1." And, then, BC would stay by itself. For a short sequence like this, it would probably take longer to encode the repeat information than it would to encode the original sequence. LZ compression programs tend to look for longer patterns, though. Some of them look at patterns that are 12 characters, often even more than that. This is another way to eliminate the redundancy. Where Huffman coding gets rid of extra bits in characters, LZ compression shortens the repetition of patterns. If you combine these two features together, you get a powerful form of compression. Most of the common compression programs use this combination in the form of an algorithm called Deflate. If you were to compress a file that had already been compressed, the Huffman coding would not be able to come up with a better sequence of bits, since it had already generated the optimal set. Chances are, it would come up with a balanced tree resulting in no compression. The LZ compression would not be able to find any repeating sequences because they had already been eliminated the first time. This is why you don't get a performance boost when you download a compressed file with a compressing modem.
Delayed Downloading
One of the many nice features of Java is that it can load classes while a program is running. There are some limits on this, however. If a method references a class, that class must be loaded before the method is executed. Java uses a one-time lookup mechanism for efficiency.The first time an instruction referencing another class is executed, the Java runtime does a lookup on the referenced class. Once the class is found, the instruction is changed to refer directly to the referenced class, bypassing the lookup. If you never execute the lookup instruction, the instruction is never changed. You cannot count on this exact behavior, however. Some Just-In-Time (JIT) compilers resolve all the references when a method is executed. This means that all the referenced classes must be loaded before the method is called. Once a method is compiled, any unresolved references remain unresolved, even if the referenced class is loaded later. Note Almost any operation involving an object of a particular class will cause that class to be loaded. If you declare an object of a particular class, that class is not immediately loaded. You can even safely test that object to see if it is null without causing a load. Almost anything else, however, will trigger a load. Some of the things that require a load are using instanceof, invoking a method, or passing the object as a parameter to a method. In the last case, you may not need to load the class if the parameter type is the same class as the object. If they are different classes, the object's class must be loaded to determine if that object is an instance of the parameter's class.
Delayed Instantiation
If you know that you won't be needing an object until a certain time, you can delay the instantiation of that object. For example, suppose you have a spreadsheet applet that can create nice graphs of the data. The graphing class may be fairly large.
Someone who just wants to enter data in the spreadsheet doesn't want to wait for the graphing class to be downloaded before they can begin. Rather than instantiating the graphing class in the init method, you wait until someone really wants to do graphing before downloading the graphing code. Your applet might look something like this: public class SpreadsheetApplet extends Applet { SpreadsheetGraphing graphing; // will be loaded later public void init() { // perform setup } public void createGraph() { if (graphing == null) { graphing = new SpreadsheetGraphing(); } . . . If the createGraph method is never called, the graphing software is never loaded. Much of the early excitement about Java was over this very feature. The idea that you grab only the code you need as you need it is a refreshing alternative to the huge pieces of software on the market today with millions of bytes dedicated to features used only by a small percentage of people.
Listing 21.1 Source Code for BackgroundLoader.java // This class loads other classes in the background so they // are ready for you when you need them. It supports a callback // mechanism to let you know when a class has been loaded. public class BackgroundLoader extends Object implements Runnable { Thread loaderThread; String[] classes; // the classes to load // who to notify
LoaderCallback callback;
// This constructor just loads one class with no notification // The loading doesn't take place until you call the start method. public BackgroundLoader(String oneClass) { this.classes = new String[1]; this.classes[0] = oneClass; } // This constructor loads a single class, and performs a callback // when the class is loaded. It doesn't start loading until start is called. public BackgroundLoader(String oneClass, LoaderCallback callback) { this.classes = new String[1]; this.classes[0] = oneClass; this.callback = callback; } // This constructor loads a whole set of classes with no callback. // Again, it doesn't start loading until start is called. public BackgroundLoader(String[] classes) { this.classes = classes; } // This constructor loads a whole set of classes and performs a callback // It doesn't start loading until start is called. public BackgroundLoader(String[] classes, LoaderCallback callback) { this.classes = classes; this.callback = callback; } public void run() { // If there's nothing to load, we're done if (classes == null) return;
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch21.htm (5 of 19) [8/14/02 10:55:03 PM]
for (int i=0; i < classes.length; i++) { try { // Class.forname will initiate the loading of a class Class.forName(classes[i]); // If there's a callback, call it. if (callback != null) { callback.classLoaded(classes[i]); } } catch (Exception e) { // Ignore any errors in loading the class. Let the error occur when // the program tries to instantiate the class. You never know, it // might not try. } } } public void start() { loaderThread = new Thread(this); loaderThread.start(); } public void stop() { loaderThread.stop(); loaderThread = null; } }
Listing 21.2 shows the LoaderCallback interface used by the BackgroundLoader class.
Listing 21.2 Source Code for LoaderCallback.java public interface LoaderCallback { public void classLoaded(String className); }
Listing 21.3 shows a sample applet that uses the background loader. It doesn't start the downloading until you click a button.
Listing 21.3 Source Code for TestLoadApplet.java import java.net.*; import java.applet.*; import java.awt.*; // // // // // // // This class tests the use of the BackgroundLoader. It presents a button that, when pressed, initiates the downloading of a class called "Fooble". When the class is successfully loaded, it presents a new button that lets you use the Fooble class. It uses the LoaderCallback mechanism to detect when the Fooble class has been loaded.
public class TestLoadApplet extends Applet implements LoaderCallback { Button loadButton; Button useButton; Fooble foo; public void init() { loadButton = new Button("Push to start loading"); add(loadButton); } // classLoaded is called when a class is loaded successfully. public void classLoaded(String className) { useButton = new Button("Push to use fooble"); remove(loadButton); // remove the old button add(useButton); // add the new button validate(); // update the layout } public boolean action(Event evt, Object which) { if (evt.target == loadButton) { // Start loading "Fooble" in the background, and use this class // as the callback BackgroundLoader bl = new BackgroundLoader( "Fooble", this); // Start the loader (most important!) bl.start(); } else if (evt.target == useButton) { // If useButton exists and is pushed, we know the Fooble has been loaded // so we can now instantiate it and invoke methods on it foo = new Fooble();
Listing 21.4 shows the Fooble class used by the TestLoadApplet applet.
Listing 21.4 Source Code for Fooble.java public class Fooble { public void bar() { System.out.println("Fooble sez: Bar!"); } }
Although Java's security model reduces the danger of malicious programs, you should be wary when installing local libraries, and only install libraries from sites, companies, or people you trust. Security restrictions are more relaxed for locally installed classes.
somewhere. If you are the one who installed your Java system, look back at the installation instructions for your Java system. They will probably refresh your memory. If someone else installed Java for you, you need to either find them or find the installation instructions.
Listing 21.5 readZipStream Method from ZipClassLoader.java // // // // // readZipStream is the heart of this class. It reads the class files from an input stream in zip format. It only pays attention to the local blocks and ignores the central and end blocks (if you know about the zip format). Once it reads in a class it stores it in a hash table, but does NOT load the classes automatically. You must call loadClass to do this. protected void readZipStream(DataInputStream zipStream) throws IOException { byte[] localHeader = new byte[LOCALLEN];
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch21.htm (11 of 19) [8/14/02 10:55:03 PM]
byte[] centralHeader = new byte[CENTRALLEN]; byte[] endHeader = new byte[ENDLEN]; byte[] signature = new byte[SIGLEN]; try { while (true) { // Figure out what type of block we are reading zipStream.readFully(signature); // Convert the signature to an integer int sig = getInt(signature, 0); // If it's a central block, skip the whole block if (sig == CENTRALSIG) { // Read in the central header bytes zipStream.readFully(centralHeader); // Figure out how many extra bytes follow the central int skipLen = getShort(centralHeader, getShort(centralHeader, getShort(centralHeader, // Skip those extra bytes zipStream.skipBytes(skipLen); // Go process the next block continue; // If this is an end block, skip the block and process the next one } else if (sig == ENDSIG) { // read the full end block header zipStream.readFully(endHeader); // figure out how many extra bytes there are int skipLen = getShort(endHeader, 16); // skip the extras zipStream.skipBytes(skipLen); continue; // If we get any other signature other than local, there's an error } else if (sig != LOCALSIG) { throw new IOException( "Invalid Block Signature"); } // read the local header zipStream.readFully(localHeader); // get the length of the data for this file int dataLen = getInt(localHeader, 14); header 24) + 26) + 28);
// get the length of the file name int nameLen = getShort(localHeader, 22); // Figure out how many extra bytes there are int skipLen = getShort(localHeader, 24); // Read in the file name byte[] nameBuf = new byte[nameLen]; zipStream.readFully(nameBuf); // Convert the file name to a string String className = new String(nameBuf, 0); // Skip any extra bytes zipStream.skipBytes(skipLen); // If this is an empty file, just go to the next block if (dataLen == 0) continue; // Read in the actual bytes for the file byte[] dataBytes = new byte[dataLen]; zipStream.readFully(dataBytes); // Add the class to the hash table classData.put(className, dataBytes); } } catch (EOFException e) { return; } }
Since the Zip format originated on the PC platform, all numbers in a zip file are stored in Intel byte order (also known as littleendian). The readZipFile method uses the methods getShort and getInt to read little-endian numbers from a byte array. Listing 21.6 shows these methods.
Listing 21.6 getShort and getInt Methods from ZipClassLoader.java // getShort reads two bytes from a byte array starting at offset <offset> // and converts them to an integer using Intel byte ordering. protected int getShort(byte[] bytes, int offset) { return ((bytes[offset+1]&255) << 8) + (bytes[offset+0]&255); } // getInt reads four bytes from a byte array starting at offset <offset> // and converts them to an integer using Intel byte ordering.
protected int getInt(byte[] bytes, int offset) { return ((bytes[offset+3]&255) << 24) + ((bytes[offset+2]&255) << 16) + ((bytes[offset+1]&255) << 8) + (bytes[offset+0]&255); }
Tip If you do enough work with different byte orders, it won't take you long to realize that you need a utility class for converting to and from little-endian byte order. If you write such a class, you might consider methods to transfer to and from byte arrays, as well as just rearranging the bytes within a short or integer number.
The readZipStream method reads only the individual .class files from a zip archive and stores them in a hash table. It doesn't actually do any class loading. For that, you must call the loadClass method in the class loader. Normally, the loadClass method in a class loader is a protected method. In this case, however, you need to tell the class loader to begin loading a class, and the only way to do that is to expose the loadClass method. Listing 21.7 shows the loadClass method from the ZipClassLoader class. It expects the classes to have been loaded into a hash table called classData. Once it loads a class, it caches the loaded class in a table called loadedClasses. If the class loader is asked to load a class that it doesn't know about, it calls the system class loader to see if it is a local class.
Listing 21.7 loadClass Method from ZipClassLoader.java public synchronized Class loadClass(String className, boolean resolve) throws ClassNotFoundException { Class newClass = (Class) loadedClasses.get(className); // If the class was in the loadedClasses table, we don't // have to load it again, but we better resolve it, just // in case. if (newClass != null) { if (resolve) // Should we resolve? { resolveClass(newClass); } return newClass; } // The classes are stored in the classData table by their original filename // which will be the class name followed by ".class" byte[] classBytes = (byte[]) classData.get(className+".class"); if (classBytes != null) { // Define the new class newClass = defineClass(classBytes, 0,
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch21.htm (14 of 19) [8/14/02 10:55:03 PM]
classBytes.length); } else { // Before we throw an exception, see if the system already knows // about this class try { newClass = findSystemClass(className); return newClass; } catch (Exception any) { throw new ClassNotFoundException(className); } } // Store the class in the table of loaded classes loadedClasses.put(className, newClass); // If we are supposed to resolve this class, do it if (resolve) { resolveClass(newClass); } return newClass; }
Listing 21.8 shows a modified version of the QuickLoader applet from Chapter 2. The ZipLoader applet tries to load a class from a zip archive. In addition to the applet parameter that tells it which applet to run, it accepts the zipfile parameter that tells it the name of the zip file to use. Also notice that there are no method calls to the zip class loader from inside the run method. If the class doesn't exist and you run your applet with a JIT, the JIT may choke on the run method if the zip class loader isn't installed locally. If you isolate the references to the zip class loader, the run method is able to function in the absence of the zip class loader. To see why this could happen, imagine that you did make direct references to the class loader from the run method. Before the run method is called, the JIT compiles the run method into native code, resolving any class references. Since the run method directly references the ZipClassLoader class, the JIT is unable to resolve all the references and it refuses to execute the method. Even if it executed the method, and the method loaded the ZipClassLoader class successfully, it would still not be able to invoke methods in ZipClassLoader, because all the method calls had already been compiled by the JIT.
Listing 21.8 Source Code for ZipLoader.java import import import import import java.applet.Applet; java.applet.AppletStub; java.awt.Graphics; java.awt.GridLayout; java.awt.Label;
import java.net.URL; // // // // // // // // // // This applet is responsible for loading another applet in the background and displaying the applet when it finishes loading. The name of the applet to load is supplied by a <PARAM> tag. For example: <PARAM name="applet" value="RealApplet"> which would load an applet class called RealApplet It uses the ziploader.ZipClassLoader, if available, to load in the classes. The "zipfile" param supplies the name of the zip file to read. For example: <PARAM name="zipfile" value="real.zip">
public class ZipLoader extends Applet implements Runnable, AppletStub { String appletToLoad; String zipArchive; Label label; Thread appletThread; ziploader.ZipClassLoader loader; public void init() { // Get the name of the applet to load appletToLoad = getParameter("applet"); // Get the name of the zip archive zipArchive = getParameter("zipfile"); // If there isn't one, print a message if (appletToLoad == null) { label = new Label("No applet to load."); } else { label = new Label("Please wait - loading applet "+ appletToLoad); } add(label); } // // // // Have to do this in a method separate from the run method, otherwise some JITs get upset if the ziploader package isn't installed. This method check to see if the ZipClassLoader exists, and if so, instantiates it.
public void tryLoader(String zipArchive) { try { // See if the class exists Class.forName( "ziploader.ZipClassLoader"); // Try to load the zip loader loader = new ziploader.ZipClassLoader(
new URL(getCodeBase(), zipArchive)); } catch (Exception ignore) { } } // This method loads a class that is stored in the ZipClassLoader. // It must be a separate method from run to keep some JITs from // getting upset when the loader isn't installed. public Class loadZipClass(String className) throws ClassNotFoundException, InstantiationException, IllegalArgumentException { return loader.loadClass(className, true); } public void run() { // If there's no applet to load, don't bother loading it! if (appletToLoad == null) return; Class appletClass = null; try { // See if the zip class loader is installed if (zipArchive != null) { tryLoader(zipArchive); } if (loader != null) { // Try loading the class from the zip file appletClass = loadZipClass(appletToLoad); } // If the class wasn't created from the zip loader, try a regular load if (appletClass == null) { appletClass = Class.forName(appletToLoad); } // Create an instance of the applet Applet realApplet = (Applet)appletClass.newInstance(); // Set the applet's stub - this will allow the real applet to use // our document base, code base, and applet context. realApplet.setStub(this); // Remove the old message and put the applet up remove(label); // The grid layout maximizes the components to fill the screen area - we // want the real applet to be maximized to our size.
setLayout(new GridLayout(1, 0)); // Add the real applet as a child component add(realApplet); // Crank up the real applet realApplet.init(); realApplet.start(); } catch (Exception e) { // If we got an error anywhere, print it label.setText("Error loading applet."); e.printStackTrace(); } // Make sure our screen layout is redrawn validate(); } public void start() { appletThread = new Thread(this); appletThread.start(); } public void stop() { appletThread.stop(); appletThread = null; } // appletResize is the one method in the AppletStub interface that // isn't in the Applet class. We'll just use the applet resize // method and hope it works. public void appletResize(int width, int height) { resize(width, height); } }
Files," tells you everything you need to know about packaging classes and other files into cabinets. Like the JAR format, the Cabinet format supports data compression, allowing you to download your classes faster if you aren't already compressing your download with a compressing modem.
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f21-1.gif
CONTENTS
G
G G
No Java? No Problem H Displaying an Image in Place of an Applet Passing Parameters to Applets Improving Applet Startup Time
While Java has been one of the hottest new technologies to hit the Web, it is still unfamiliar to many users. There are still only a handful of Web browsers that support Java. The rest simply ignore the <APPLET> tag that identifies an applet to the browser. For users whose browsers don't support Java, you want to provide at least some suggestion that there is something there that the browser cannot display. Otherwise, they might not realize that they are missing something. You also need to consider the fact that some applets may take a while to download. Most browsers just display a large blank area while downloading an applet, not giving the user an indication that there is anything more to display. You need to let the user know that there is something more to see and that they should be patient.
No Java? No Problem
Once you've written a Java applet and you want to display it in a Web page, you use the <APPLET> tag. This is probably one of the first things you learned when you started programming in Java. Making the jump from a simple "Hello World" applet to enhancing your company's Web page is a big step. You now have to consider the possibility that people cannot run Java. Many Web browsers still do not understand the <APPLET> tag. A browser that does not understand the <APPLET> tag simply skips over it. On the other hand, a browser that does understand the <APPLET> tag skips over any other tags and text up to the closing </APPLET> tag (except for the <PARAM> tags, of course). You can take advantage of this by providing alternative content, such as an image or text, to go in place of the applet. Note The most popular Java-enabled Web browsers are the Netscape Navigator (version 2 and later) and Microsoft Internet Explorer (version 3). Sun's HotJava browser not only runs Java, it is written entirely in Java.
You should consider displaying an image in place of an applet if your Web page expects something to occupy the applet's space. In other words, sometimes when you lay out your Web page, you place the applet somewhere and expect it to occupy a certain amount of space. The rest of the text is laid out accordingly. If you suddenly try to view the page on a browser that doesn't understand the <APPLET> tag, there will be nothing occupying that space and your page layout will be far from what you expected. You can still reserve that space, however, by using an image as the alternative content. The advantage of an image is that you can specify its exact size the same way you can with an applet. In fact, you may have noticed already that the <APPLET> tag contains a number of options that are identical to those of the <IMG> tag. If the image you want to display is smaller than the applet's display area, you can either expand the image or you can put padding around the image. Unfortunately, some older browsers, like Mosaic, won't even expand images, so if you are trying to accommodate really old browsers, you may want to avoid expanding the image. Note Some of the non-Java browsers you are likely to encounter are Netscape version 1, Microsoft Internet Explorer version 2 and earlier, Mosaic, and most custom browsers offered by Internet providers. In addition, Netscape version 3 does not support Java under Microsoft Windows version 3.
Listing 2.1 shows a Web page that displays an applet with an image as the alternative content. The image being displayed is only 100100 pixels, but the applet is 200200. The <IMG> tag specifies a width and height of 200, causing the browser to expand the image to fit the area.
Listing 2.1 Source Code for PushButton.html <HTML> <TITLE> Pushbutton Applet </TITLE> <HEAD> </HEAD> <BODY> Here is some text before the applet. <APPLET codebase="." code="PushButton.class" width=200 height=200> <IMG src="javacup.gif" width=200 height=200> </APPLET> Here is some text after the applet. </BODY> </HTML>
Figure 2.1 shows this page with the applet running. Figure 2.1 : A Java-enabled browser displays the applet defined by the <APPLET> tag.
Figure 2.2 shows how this page looks on a browser that doesn't understand the applet tag but is still able to expand images. Figure 2.2 : A browser that does not support Java can display an image in place of the applet. You can also match the image size to the applet size by adding padding to the image. This might be useful on browsers that cannot expand images to fit a particular size. Unfortunately, these browsers might not support the HSPACE and VSPACE attributes. Mosaic, for instance, doesn't expand images and it doesn't support HSPACE and VSPACE. Listing 2.2 shows a Web page that contains the same 200200 applet and uses the same 100100 image as the alternative content. Rather than expanding the image to fit a 200200 space, it pads the image by 50 pixels on each side, making the effective image size 200200.
Listing 2.2 Source Code for PushButton2.html <HTML> <TITLE> Pushbutton Applet </TITLE> <HEAD> </HEAD> <BODY> Here is some text before the applet. <APPLET codebase="." code="PushButton.class" width=200 height=200> <IMG src="javacup.gif" width=100 height=100 hspace=50 vspace=50> </APPLET> Here is some text after the applet. </BODY> </HTML>
Figure 2.3 shows how the page appears on browsers that do not support applets. Figure 2.3 : The HSPACE and VSPACE attributes add padding around an image. Note Figures 2.1, 2.2, and 2.3 feature the Microsoft Internet Explorer browser. Figure 2.1 uses Internet Explorer version 3, while Figures 2.2 and 2.3 use IE version 2, which does not support Java. Since IE version 2 was shipped with Windows 95, there are many people still using it.
to use the same applet in a number of Web pages without creating multiple versions of the applet. You can use the parameter mechanism to pass parameters from a Web page to the applet. Tip If you are able to reuse an applet on a Web page rather than use two separate applets, you save a lot of downloading time. The browser doesn't download the same applet twice.
One of the difficult aspects of the getParameter method in the Applet class is that it has only one option-it fetches a single named parameter. There is no support for having multiple values for a parameter or for having a default parameter value. You can solve this by creating a class that performs these functions for you. The Parameters class presented here provides a more flexible interface to applet parameters. It allows you to specify a default value for a parameter and also retrieve an array of values for a parameter. You can provide multiple values one of two ways-either by grouping them into one string with one or more separators or by providing <PARAM> tags with parameter names that have numbers appended to them. If you want to supply multiple parameter values from a single <PARAM> tag, you need to define the set of separator values that you will put in between the parameters. The following example of the <PARAM> tag provides multiple values separated by colons: <PARAM name="foo" value="somevalue:anothervalue:thisvalue:lastvalue"> You can also provide multiple parameters by appending numbers to the parameter name, starting at 0: <PARAM <PARAM <PARAM <PARAM name="foo0" name="foo1" name="foo2" name="foo3" value="somevalue"> value="anothervalue"> value="thisvalue"> value="lastvalue">
The numbered parameters are more useful when you want to have several sets of multiple parameters. You may have a scrolling marquee, for instance, in which you supply the marquee text and the speed. You would like to be able to group them like this: <PARAM name="text0" value="This is my marquee"> <PARAM name="speed0" value="100"> <PARAM name="text1" value="I hope you like it"> <PARAM name="speed1" value="200"> Listing 2.3 shows the source code for the Parameters class.
import java.util.Vector; import java.util.StringTokenizer; import java.applet.Applet; /** * Provides extra ways to access applet parameters. Allows multiple * values for a parameter either by separated values, or by adding an * index value to each parameter (i.e. param0=first param1=second, etc.) * You can either call static methods or create an instance and call * instance methods. The static methods require that you pass the * applet each time. Because you create instances by passing the Applet * to the constructor, the instance methods don't require you to pass * the applet. * @author Mark Wutka */ public class Parameters { protected Applet applet; /** * Creates a Parameters instance that will fetch parameters for a * particular applet. * @param applet The applet whose parameters will be retrieved. */ public Parameters(Applet applet) { this.applet = applet; } // All the instance methods just pass through to the static methods // to avoid code redundancy. /** * Returns the named parameter, or null if not set (this is identical * to the Applet.getParameter method). * @param paramName The name of the parameter to retrieve. */ public String getParameter(String paramName) { return getParameter(paramName); } /** * Returns the named parameter, or defaultValue if not set. * @param paramName The name of the parameter to retrieve. * @param defaultValue The parameter value to use if there is no * PARAM tag for this parameter. */
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch2.htm (5 of 12) [8/14/02 10:55:07 PM]
public String getParameter(String paramName, String defaultValue) { return getParameter(applet, paramName, defaultValue); } /** * Returns an array of parameters. The corresponding parameter names * in the PARAM tags should end with numbers starting with 0. For * example, for a parameter named "foo", the PARAM tags should use names * of foo0, foo1, foo2, etc: * <PRE> * <PARAM name="foo0" value="somevalue"> * <PARAM name="foo1" value="anothervalue"> * <PARAM name="foo2" value="thisvalue"> * <PARAM name="foo3" value="lastvalue"> * </PRE> * If you skip a number, the rest of the parameters will be ignored. * * @param paramName The base name for the parameters to be fetched. */ public String[] getParameters(String paramName) { return getParameters(applet, paramName); } /** * Returns an array of parameters. The parameters should be separated * by a specific separator character, or a set of separator characters. * For example, for the following call to getParameters: * <PRE> * param.getParameters("foo", ";:,"); * </PRE> * You could use :, ;, and , as separators: * <PRE> * <PARAM name="foo" value="somevalue:anothervalue;thisvalue,lastvalue"> * </PRE> * * @param paramName The name of the parameter to fetch. * @param separators A string containing the separators for the parameters. */ public String[] getParameters(String paramName, String separators) { return getParameters(applet, paramName, separators); } /** * Returns the named parameter, or null if not set (this is identical * to the Applet.getParameter method).
* @param applet The applet whose parameters will be fetched. * @param paramName The name of the parameter to retrieve. */ public static String getParameter(Applet applet, String paramName) { return applet.getParameter(paramName); } /** * Returns the named parameter, or defaultValue if not set. * @param applet The applet whose parameters will be fetched. * @param paramName The name of the parameter to retrieve. * @param defaultValue The parameter value to use if there is no * PARAM tag for this parameter. */ public static String getParameter(Applet applet, String paramName, String defaultValue) { String returnValue = applet.getParameter(paramName); if (returnValue == null) return defaultValue; return returnValue; } /** * Returns an array of parameters. The corresponding parameter names * in the PARAM tags should end with numbers starting with 0. For * example, for a parameter named "foo", the PARAM tags should use names * of foo0, foo1, foo2, etc: * <PRE> * <PARAM name="foo0" value="somevalue"> * <PARAM name="foo1" value="anothervalue"> * <PARAM name="foo2" value="thisvalue"> * <PARAM name="foo3" value="lastvalue"> * </PRE> * If you skip a number, the rest of the parameters will be ignored. * * @param applet The applet whose parameters will be fetched. * @param paramName The base name for the parameters to be fetched. */ public static String[] getParameters(Applet applet, String paramName) { // Put the parameters into a vector first, because you don't // know how many parameters you are getting. Vector vec = new Vector(); for (int i=0; true; i++) { // Try getting next numbered parameter String paramStr = applet.getParameter(paramName+i);
// If it isn't there, you're done if (paramStr == null) break; // If you got the parameter, add the it to the vector vec.addElement(paramStr); } // Create a string array to hold the values String[] returnValues = new String[vec.size()]; // Copy the vector values into the new string array vec.copyInto((Object[])returnValues); return returnValues; } /** * Returns an array of parameters. The parameters should be separated * by a specific separator character or a set of separator characters. * For example, for the following call to getParameters: * <PRE> * param.getParameters("foo", ";:,"); * </PRE> * You could use :, ;, and , as separators * <PRE> * <PARAM name="foo" value="somevalue:anothervalue;thisvalue,lastvalue"> * </PRE> * * @param paramName The name of the parameter to fetch. * @param separators A string containing the separators for the parameters. */ public static String[] getParameters(Applet applet, String paramName, String separators) { String paramStr = applet.getParameter(paramName); // If the parameters weren't there, just return an empty array if (paramStr == null) { return new String[0]; } // Put the parameters into a vector first, because you don't // know how many parameters you are getting. Vector vec = new Vector(); // The tokenizer will separate out the parameters from the string StringTokenizer tok = new StringTokenizer(paramStr, separators); // Grab the parameters from the string while (tok.hasMoreTokens()) {
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch2.htm (8 of 12) [8/14/02 10:55:07 PM]
vec.addElement(tok.nextToken()); } // Create a string array to hold the values String[] returnValues = new String[vec.size()]; // Copy the vector values into the new string array vec.copyInto((Object[]) returnValues); return returnValues; } }
Tip The getParameters method in the Parameters class deals with a common problem. It must create an array of strings without knowing ahead of time how many strings there are. To solve this problem, you store the strings in a vector. Once you have all the strings you need, you can create an array of strings, using the size method in the vector to determine how many strings there are. Once you create the array, you simply copy the strings from the vector to the array. The copyInto method in the Vector class will do the copying for you so you don't have to do it manually.
The appletResize method in this case is performing a technique called "delegation." The appletResize method delegates the responsibility of performing the resizing to the resize method. You often use this technique in object-oriented programming where one object has a set of methods that delegate their responsibility to methods in another object.
Listing 2.4 shows a quick applet loader that can be used to display information while you are loading a much larger applet. This applet is less than 2K in size, so it should take only a few seconds to load.
Listing 2.4 Source Code for QuickLoader.java import import import import import java.applet.Applet; java.applet.AppletStub; java.awt.Graphics; java.awt.GridLayout; java.awt.Label;
// This applet is responsible for loading another applet in the // background and displaying the applet when it finishes loading. // The name of the applet to load is supplied by a <PARAM> tag. // For example: // <PARAM name="applet" value="RealApplet"> // which would load an applet class called RealApplet // public class QuickLoader extends Applet implements Runnable, AppletStub { String appletToLoad; Label label; Thread appletThread; public void init() { // Get the name of the applet to load appletToLoad = getParameter("applet"); // If there isn't one, print a message if (appletToLoad == null) { label = new Label("No applet to load."); } else { label = new Label("Please wait - loading applet "+ appletToLoad); } add(label); }
public void run() { // If there's no applet to load, don't bother loading it! if (appletToLoad == null) return; try { // Get the class for the applet we want Class appletClass = Class.forName(appletToLoad); // Create an instance of the applet Applet realApplet = (Applet)appletClass.newInstance(); // Set the applet's stub - this will allow the real applet to use // this applet's document base, code base, and applet context. realApplet.setStub(this); // Remove the old message and put the applet up remove(label); // The grid layout maximizes the components to fill the screen area - you // want the real applet to be maximized to our size. setLayout(new GridLayout(1, 0)); // Add the real applet as a child component add(realApplet); // Crank up the real applet realApplet.init(); realApplet.start(); } catch (Exception e) { // If we got an error anywhere, print it label.setText("Error loading applet."); } // Make sure the screen layout is redrawn validate(); } public void start() { appletThread = new Thread(this); appletThread.start(); } public void stop() { appletThread.stop();
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch2.htm (11 of 12) [8/14/02 10:55:07 PM]
appletThread = null; } // appletResize is the one method in the AppletStub interface that // isn't in the Applet class. You can use the applet resize // method and hope it works. public void appletResize(int width, int height) { resize(width, height); } }
Once you are able to embed applets on a Web page, you can concentrate on the really fun part-creating interesting applets. Make your applets interesting, visually pleasing, and fast. And remember that not everyone will be able to run Java. Make sure you leave something for those poor souls.
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f2-1.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f2-2.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f2-3.gif
CONTENTS
G G
Reducing Image Size Image Strips H Using the Graphics.clipRect Method H Creating Another Graphics Context Storing Only Parts on an Image Strip
Images often account for a large amount of an applet's down-load time. Even though both the GIF and JPEG formats used by Java involve data compression, the images can still be rather large. Since these images are already compressed, you won't realize much benefit from additional data compression, either. You need to find ways of either reducing the original image size or getting the images in a more efficient fashion.
Since JPEG allows you to store images with varying quality, you can reduce the size of a JPEG just by storing it as a lowerquality image. The quality of an image is actually determined by the amount of image compression. The compression is given as a percentage ranging from 0 to 100. The higher the amount of compression, the lower the quality. You don't have to use 0 percent compression all the time when storing high-quality images. Many images can be stored with 20 percent or even 40 percent compression with no loss of quality. On the other side, 100 percent compression does not reduce a file to nothing. The variations in file size are not always in direct proportion to the change in compression factor. Figure 22.1 shows a 24-bit image stored with 40 percent compression, whereas Figure 22.2 shows an image stored with 80 percent compression. Figure 22.1 : JPEG can compress many images with no loss of quality. Figure 22.2 : JPEG images tend to get grainier the more they are compressed. Even though the compression of the second figure is twice the amount of the first figure, the reduction in file size is much greater. The file size for the first figure is approximately 96K, whereas the file size for the second figure is about 16K. If you are really concerned with image size, try storing the image in both GIF and JPEG formats and see which is smaller. If JPEG is the way you want to go, try varying amounts of compression to see how much quality you are willing to lose in exchange for a smaller file.
Image Strips
Every time you download a file over the network, a certain amount of overhead is involved in setting up the network connection, no matter how small the file. If you have to download a large number of files, you lose a lot of time in the connection overhead alone. To compound the problem, if you download a large number of files simultaneously, you can't predict which file will be loaded first. This may not be so bad when you are downloading data files but if you are doing animation, it can be a pain. You want to present something to the user as quickly as possible, preferably immediately, even if it's just a "Please wait" message. Depending on the animation, you might want to grab the first frame and display it while waiting for the rest. You might also build up the animation gradually, showing the frames in order as you get them. For instance, if you have the first two frames, loop through them, adding the third frame to the loop when it is loaded. You can save yourself a lot of time if you just combine your images onto a single, larger image and download it. Although it's true it takes longer than downloading a single frame, the overall time to download the single image is less than the time it would take to download 12 frames individually. The image containing all your animation frames is called an image strip. Tip
You don't have to be doing animation to use this technique. It works any time you need to load several images into your applet.
Figure 22.3 shows an image strip consisting of multiple views of a person with a real head and cartoon body. Figure 22.3 : Combining multiple images onto one image strip can save you time when downloading. You can create an image strip with almost any paint program, as long as it works with a GIF or JPEG format. One thing to look for, however, is the ability to determine pixel coordinates. You need to know exactly where on the image strip each image is located. Many paint programs show you the current cursor location somewhere on the screen, which helps tremendously. If the program has a zoom feature, it really takes the guesswork out of finding the images. The trick to displaying images from an image strip is that you make use of clipping. Although you could use the CropImageFilter class to view just a portion of the image, the class adds a lot of unnecessary overhead. The clipping functions built into the AWT do the same function only much, much faster. When you draw an image from an image strip, you are essentially viewing the image strip through a lens the size of the image you want to draw. You move the image strip around underneath the lens to view a different image. It's like using a microscope. The microscope lens is in a fixed position. If you want to see a different part of the microscope slide, you have to move the slide around. Since the lens is fixed and you must move the image, you have to move the image in the opposite direction from the direction you would move the lens. In other words, if you normally move the lens 50 pixels to the right and 20 pixels down, you must move the image 50 pixels to the left and 20 pixels up. Figure 22.4 shows the relative positioning of the lens and the image. Figure 22.4 : You must draw an image strip relative to the lens. In Java lingo, the lens you use to view the image strip is called the clipping area. A clipping area is the area in which you can draw. You may have noticed that Java doesn't give you an error when you try to draw images that are way outside the bounds of your applet but it also doesn't draw outside the bounds. This is because all your drawing is done within a clipping region. The default clipping region for your applet is the entire area of the applet. You can change the clipping area, however, with the Graphics.clipRect method.
For example, suppose you want to draw an 8060 image at location 40,30. The corresponding clipRect call, assuming the variable g is an instance of Graphics, would be: g.clipRect(40, 30, 80, 60); Once you have created the clipping region, you still draw the image relative to the whole graphics area. In other words, the clipping region creates something like a graphics stencil that protects the rest of the graphics area from being painted, but you act like you are still painting the entire graphics area. Remember that when you use image strips, you really draw the entire image strip every time; you just create a small window on top of the image strip so you see only one image at a time. Once you create a clipping region, you still have to figure out the x and y coordinates where the image strip should be drawn. The formula for the image strip's x and y coordinates is: int imageStripX = clippingRegionX - imageX; int imageStripY = clippingRegionY - imageY; The imageX and imageY variables are the x and y coordinates of the image you want to draw relative to the image strip. In other words, if you want to draw an image from an image strip that is at location 50,10 on the strip, you would use 50 for the imageX and 10 for the imageY variables. For the example of a clipping region at 40,30 and an image location of 50,10, you would draw the image strip at -10, 20 (that's 40-50, 30-10). To see why this is so, think of what would happen if you drew the image strip at 0,0. The image you really want is over at 50,10. Now, shift the image 50 pixels to the left and 10 pixels up (draw it at -50,-10). Now the image you want to draw is at location 0,0 on the screen. You really want it at 40,30, however, so you add 40 to the x coordinate and 30 to the y coordinate, moving the image you want over to location 40,30. Now if you look at what you did to the actual x,y of the full image strip, you moved the x coordinate left 50 and right 40, for a total movement of left 10, making its x coordinate -10. You shifted the y coordinate up 10 spaces and down 30 spaces, making a total movement of 20 pixels down, giving a y coordinate of 20. Note Since you can't enlarge the clipping area, once you reduce the clipping area to the size of the image, you can't draw anything outside of that boundary. If you need to draw multiple things in your paint method, do the image strip drawing last. This becomes a real problem if you do offscreen drawing. Normally, when you do offscreen drawing, you create an offscreen graphics context one time, just after you create the offscreen image. Once you change the clipping region on the offscreen graphics context, it stays changed. If you want to reset it, you have to create another offscreen graphics context by calling the getGraphics method in the offscreen image. You should probably also call the dispose method in the old graphics context first to free up its resources.
Instead of changing the clipping region, you can create a new graphics drawing area that is a portion of the current drawing area. Then, instead of calling the drawImage method in your current drawing area, you call the same method in the subarea. The create method in the Graphics class creates a subarea within the main drawing area. If you change the clipping region in the subarea, it doesn't affect the main area. You don't need to clip the subarea, however, because you can just create it to be the size of the clipping area you want. To create a subarea at location 40,30 that is 8060 pixels, you would do this: Graphics subArea = g.create(40, 30, 80, 60); You could then draw your image in this subarea: subArea.drawImage(imageStrip, -75, -25, this); Once you are done with the subarea, you should free up its resources by calling the dispose method: subArea.dispose();
Note When you draw images on a subarea, you do not add the x and y locations of the subarea to the coordinates for the image strip. In other words, if you use a subarea to draw an image that is at location 75,25 on an image strip, you always draw the image strip at location -75,-25, no matter where you create the subarea. This is different from the method where you just create a clipping region. The coordinates of the upper-left corner of a clipping region are the coordinates relative to the drawing area. The coordinates of the upper-left corner of a subarea are 0,0.
You can use the following method in your programs to draw images from an image strip without doing all the clipping yourself: public void drawStripImage(Graphics g, Image imageStrip, int drawX, int drawY, int stripX, int stripY, int imageWidth, imageHeight) { Graphics subArea = g.create(drawX, drawY, imageWidth, imageHeight); subArea.drawImage(imageStrip, -stripX, -stripY, this); subArea.dispose(); } In the preceding method, g is the original Graphics object from your paint method, imageStrip is the image strip you are drawing from, drawX,drawY are the coordinates where you want to draw the image, stripX,stripY is the location of the image on the image strip, and imageHeight and imageWidth are the width and height of the image.
There are several tools available on the Internet for creating transparent GIFs. On the Windows platform, one of the most popular tools is PaintShop Pro, available as a shareware program from http://www.jasc.com/pspdl.html. Remember that shareware programs are not free. If you use it, you should pay for it. The GIFTOOL program, from http://www.homepages.com/tools/giftool, is available on a wide variety of platforms and is also a shareware program. GIFTOOL is a little tougher to use since it is a command-line tool, but its availability on many platforms is appealing. The idea behind transparent GIFs is that you mark one of the colors in the GIF color table as being a transparent pixel. Obviously, you must be using an indexed color model to create a transparent image. This is why JPEG cannot support transparent pixels-JPEG always uses 24-bit color, which never needs a color index.
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch22.htm (6 of 8) [8/14/02 10:55:14 PM]
When you draw images based on pieces, you must pay special attention to the relative positions of the different parts. When you draw a multipart figure at a particular location, how do you decide exactly where to draw the pieces? For the animated figure in Figure 22.6, you might say that the figure's location is determined by the upper left corner of the head. In other words, if you want to draw the figure at location 40,20, you draw the head so that the head part of the image is drawn at 40,20. You must then determine the position of the other parts relative to the head. For example, in Figure 22.6, the body portion is drawn 48 pixels down and 6 to the right from the upper left corner of the head. These locations are determined by using a paint program or other tools. Once you determine the relative positions of the pieces, you can store them in the following class:
Listing 22.1 Source Code for ImageStripImage.java public class ImageStripImage { // distFromXOrigin and distFromYOrigin give the position where // this image should be drawn relative to the location, or origin, // of the multi-part image. public int distFromXOrigin; public int distFromYOrigin; // stripX,StripY give the location of this image on the image strip public int stripX; public int stripY; public int width; public int height; // the width of this image // the height of this image
public ImageStripImage(int distX, int distY, int stripX, int stripY, int width, int height) { this.distFromXOrigin = distX; this.distFromYOrigin = distY; this.stripX = stripX; this.stripY = stripY; this.width = width; this.height = height; } }
Once you have a piece of an image defined by this structure, you can draw it using this variation of the drawStripImage method:
public void drawStripImage(Graphics g, Image imageStrip, int drawX, int drawY, ImageStripImage imageInfo) { Graphics subArea = g.create(drawX + imageInfo.distFromXOrigin, drawY + imageInfo.distFromYOrigin, imageInfo.width, imageInfo.height); subArea.drawImage(imageStrip, -imageInfo.stripX, -imageInfo.stripY, this); subArea.dispose(); } This variation of the drawStripImage method adjusts the location of the image piece by that piece's relative position to the overall position of the image. There is a full example of an image strip animation available on the CD that comes with this book. It is called ImageStripApplet.java.
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f22-2.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f22-3.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f22-4.gif
CONTENTS
G G G G G
Using Java Objects Instead of CGI The Servlet API The Web Server as a Computing Server Adding Web Access to Your Java Applications Migrating off the Web Server in the Future
Several Web servers now allow you to create Java applets that use Web services. This allows you use Java instead of some of the popular languages like C++ and Perl. If you are developing applications in Java, you can provide Web access to these applications without complicated, native method calls. As the Web servers improve, they will eventually be able to get Java programs from their clients and run them, turning the Web server into a computing server. Although Web servers are very popular right now, the limitations of the HTTP protocol will become more of a hindrance than a help as the world of distributed objects takes shape. Your application may get away with HTML forms this year but it may need to use CORBA or RMI to communicate with a complex applet next year. You can design your applications right now with the possibility of CORBA or RMI in the future.
Whenever the server gets a request that is handled by a CGI program, it must start the CGI program, which has some fixed amount of overhead. After the CGI program finishes processing a request, it terminates. If a CGI program needs to maintain information across requests, it must store the information in a database or a file, and read it in again the next time it starts up. These start-up costs can be very high if the CGI program has to establish a session with a database every time. FastCGI is an improvement over CGI. Instead of running a new program every time, FastCGI programs are always running. When a new request comes in, the Web server passes information to a FastCGI program via an interface protocol. Although this is certainly faster than regular CGI, the communication between the Web server and the FastCGI program can still be rather slow. The most desirable option so far is to run the request handler as part of the Web server. Some commercial Web servers have hooks that allow you to add request handlers directly to the server. These hooks, or plug-ins, give you the speed you need. Of course, when you want to run the same service on a different hardware platform or a different operating system, you have to create another version of the plug-in. Figure 23.1 illustrates the relationship between the Web server and the request handling code for CGI, FastCGI, and plug-in modules. Figure 23.1 : Traditional Web servers have evolved from simple, slow CGI to high-speed plug-ins. Java is an ideal platform for using Web services. It runs on multiple platforms, it can be dynamically loaded, and it has a good security system. In a Java Web server, the objects that handle requests are written entirely in Java. These request-handling objects are called servlets. Unlike traditional CGI request handlers, servlets do not go away when they finish processing a request. This eliminates the heavy start-up overhead. Unlike FastCGI, servlets run within the Web server itself, eliminating the communications overhead incurred when the server passes a request to the handler. And unlike plug-ins, servlets can run on any platform that supports Java. Servlets can also take advantage of Java's security framework, allowing different levels of security for different servlets. For instance, you could define a security policy that allowed a servlet to access only certain directories on your file system. In addition, you could limit other Java features, like network access.
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch23.htm (2 of 8) [8/14/02 10:55:20 PM]
This feature is ideal for Web-space providers who have been unable to provide CGI access to their customers for fear of a malicious CGI program destroying the system. Now they can provide a Java Web server and allow their customers to write servlets that can only access the customer's files, and not those belonging to other customers. If you get the same old story from your Web provider about why they can't give you CGI, suggest to them that they set up a Java Web server. If they won't, find a provider who will.
If you have some large, numerical non-Java application, for instance, you could provide access to the application via native methods. A customer would send you a servlet that makes various calls to your numerical application. This is discussed in more detail in Chapter 33, "Web-Enabling Legacy Systems."
Note Two of the factors for deciding whether to put the servlet and the application on two different hosts are the amount of computation performed by the application and the amount of interaction between the application and the servlet. If the application needs a lot of CPU time, it would be better off on a separate host. If the application and the servlet exchange large volumes of data or pass many messages between them, they would be better off on the same host.
Suppose you want to add Web access to the banking application from Chapter 18, "Using CORBA IDL with Java." You could create a servlet to handle each of the four operations in the banking interface. Each servlet would have a pointer to a banking object, possibly even the same object, if you want to use the banking object as a singleton object. A banking servlet could create an instance of the banking implementation in its init method: public void init() { bank = new banking.BankingImpl(); } Then the service method, which handles incoming requests, would translate the incoming HTTP request to a method call to the BankingImpl object as shown in Listing 23.1.
Listing 23.1 Method Call to BankingImpl Object public void service(ServletRequest req, ServletResponse resp) { // Get the table of request paramaters Hashtable params = req.getQueryParameters(); // Get the account number, account password, and account type String account = params.get("account"); String password = params.get("password"); String accountTypeName = params.get("accountType");
// Convert the account type name into one of the allowable // account types, or return an error if it's an illegal type int accountType; if (accountTypeName.equals("checking")) { accountType = banking.Account.CHECKING; } else if (accountTypeName.equals("savings")) { accountType = banking.Account.SAVINGS; } else { res.writeErrorResponse(SC_BAD_REQUEST, "Invalid account type"); return; } // Get the balance using the BankingImpl object try { int balance = bank.getBalance( new Account(account, password, accountType); // Store the resulting information in the response/ res.setStatus(SC_OK); res.setContentType("text/html"); res.writeHeaders(); // Get a print stream for writing the HTML for the response PrintStream out = new PrintStream( res.getOutputStream()); // Generate an HTML response containing the balance out.println("<HTML><HEAD>"); out.println("<TITLE>Bank Account Balance</TITLE>"); out.println("</HEAD><BODY>"); out.println("<H1>Current Account Balance:</H1>"); // The BankingImpl object stores balances in cents, we have // to convert it to dollars manually. out.println("<P>$"+balance/100+"."+balance%100); out.println("</BODY></HTML>"); out.flush(); } catch (banking.InvalidAccountException) { // If there was an invalid account exception, pass this on // to the client res.writeErrorResponse(SC_UNAUTHORIZED, "Invalid account"); return;
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch23.htm (6 of 8) [8/14/02 10:55:20 PM]
} catch (Exception e) { // If there was any other exception, something's wrong internally res.writeErrorResponse(SC_INTERNAL_SERVER_ERROR, "Got error performing request"); return; } }
As you can see, the servlet makes use of the existing BankingImpl object without actually doing any of the banking operations itself. In this configuration, the BankingImpl object must be running in the same Java environment as the Web server. You could also replace the BankingImpl object with a CORBA or RMI stub, and make remote method invocations to a banking object running somewhere else.
As always, you will find that it takes more effort to develop a Web service by splitting it into two parts. You will save time in the long run, however, because you can add new ways to access your application without changing the application at all.
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f23-1.gif
CONTENTS
G G
What Is Jeeves? The Jeeves HTTP Server H Architectural Overview H Installing and Running the Jeeves HTTP Server H Administering the Jeeves Web Server H HTTP Server Security Extending Jeeves' Functionality with Servlets H Employing the Servlet API H Using the Jeeves Development Toolkit Building a Database Servlet H Getting the Information from the Users H Connecting Your Servlet to a JDBC Database H Inserting Data in the Database H Searching the Database Building a Simple Autonomous Agent System with Jeeves H Using Object Serialization to Transport Agents Across the Internet H Building the Remote Agency H Creating a Generic Agent Interface H Implementing a Database Search Agent H Building the Home Agency H Launching the Agent H Debriefing the Agent
Jeeves is a Java-based Web server development toolkit that includes a fully functional HTTP Web server. This chapter describes how to write servlets that extend the function of the HTTP server using the Web server toolkit. The chapter begins by introducing the HTTP server's architecture. You will go through the process of administering the server. This background knowledge lays the foundation for the rest of the chapter. In the second part of the chapter, you learn how to write servlets that extend Jeeves' functions. This section begins with an introduction to the servlet API, which is at the heart of Jeeves' functionality and extensibility. You then learn about Jeeves' rich collection of tools that enhance servlet development. Finally, two examples show you what can be done with servlets and Jeeves. The first is a database servlet that puts a Web front end on any database that supports the Java Database Connectivity (JDBC) interface. The second takes advantage of Java's object serialization to create a simple example of an autonomous agent system.
What Is Jeeves?
Jeeves is often described simply as a Java Web server, but it is much more. Jeeves is a server development toolkit. At the center of the Jeeves toolkit is a package of generic server classes. With these classes, any developer can quickly build connection-oriented servers. Another important component of the toolkit is the servlet. Servlets are Java objects that comply with the servlet API and are used to add functions to Web servers. The servlet API is Sun's proposed standard for extending Web server functions with Java. Note In addition to Jeeves, Acme Serve is a basic Web server that complies with the servlet API. It is available at http://www.acme.com/.
In addition to the servlet API and the generic server classes, the Jeeves toolkit includes security classes, administrative classes, utility classes, and a set of servlets that provide basic Web server functions. The Jeeves HTTP server was developed from this toolkit and is a fully functioning Web server that provides all the features common to other Web servers. Tip For more information about Jeeves, check out http://www.javasoft.com/products/jeeves.
Architectural Overview
The Jeeves HTTP server is built on the framework provided by the generic server classes discussed earlier. This framework is the core of the Jeeves server development toolkit. Here is a brief description of the workings of a generic Jeeves server, followed by a description of the specific workings of the HTTP server. An object of the sun.server.Server class waits in a loop for connection requests. Connections are placed on a queue while the server determines if there are handler objects of the sun.server.ServerHandler class available in the handler thread pool. If none are available and the maximum number of handler threads has not been reached, the server starts a new handler thread. If, on the other hand, the number of handlers exceeds the minimum needed, and some have been idle for a period longer than the specified timeout parameter, the idle handlers expire. In the case of the HTTP server, once the server receives an HTTP request, it is queued for servicing by the pool of HTTP server handler threads. The HTTP handler then authorizes and applies name translation rules to the request, and passes the request on to the appropriate servlet. Servlets provide the core function of the Jeeves HTTP server, as well as providing a means for extending that function. The HTTP
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch24.htm (2 of 24) [8/14/02 10:55:30 PM]
server includes a set of core servlets that provide common Web server functions. For instance, the FileServlet fulfills HTTP GET requests, returning the requested file to the client. The Invoker servlet is used to dynamically invoke servlets that have been explicity requested by a client using an URL of the form: http://ServerHostName/servlet/<servletName> The Invoker supports only local servlets but will soon be able to dynamically load servlets from across a network. The SSInclude servlet parses server-side include files (files with an .shtml extension) and calls any servlet that was referenced. The CgiServlet provides backward compatibility for the large body of existing CGI programs. The ImageMapServlet uses server-side image maps. Finally, there is the Admin Servlet that works together with the Admin applet to help with administrative tasks (you'll learn more about this in the next section).
Caution If you are running Jeeves on a Windows 95 machine, you must make sure that the logs directory is under the main installation directory. Unzip tends not to extract directories that have no files, as is the case with the logs directory.
Table 24.1 Jeeves Configuration Files File Name httpd.properties rules.properties alias.properties servlet.properties mime.properties acl.properties Properties Server name, port number, minimum threads, maximum threads, timeout, ramcache, keepalive, keepalive timeout, and location of other property files Translation rules for invoking servlets Translation rules for path aliases Servlet codebase, servlet code, and initArgs MIME configuration Access control file
Administering Jeeves from a Java-Enabled Browser You can manage the Jeeves Web server remotely using any Java-enabled Web browser. Once the server is running, you can access the administration Web page by pointing your browser at the following URL: http://ServerHostName:8888/admin/admin.html You are prompted for a user name and password. The default administration account name and password are admin. Once authorized, you see the page shown in Figure 24.1. Figure 24.1 : HTTP configuration using the Admin applet. The window on the left includes a list of administrative tasks, including HTTP configuration, log configuration, file aliasing, servlet aliasing, servlet loading, MIME configuration, user configuration, group configuration, access control list (ACL) configuration, and resource protection. Modifying Basic Web Server Parameters Jeeves has many tunable parameters. Figure 24.1 shows the parameters you can modify from the administration Web page. There are other properties, such as the server user (UNIX version only) and server host name, that can be changed only from the httpd.properties file. Jeeves lets you set the number of handler threads that are started and how long an idle thread remains before being destroyed. In the httpd.properities file, you'll find the following thread properties: server.min.threads, server.max.threads, and server.timeout. Jeeves uses connection keepalive to improve performance by keeping the connections to client browsers open even after the request has been fulfilled. This reduces the overhead of bringing the connection up and down for multiple requests from the same client. The keepalive count property determines the number of hits from a single client that are received before the connection is brought down. The keepalive timeout determines the time in seconds the connection stays up after a request has been fulfilled. Configuring Web Server Logging You can specify where log files are stored and the level of logging detail for the Access, Error, and Event logs. These changes are made from the Log Configuration screen (see Figure 24.2) or the httpd.properties file. Figure 24.2 : Log Configuration using the Admin applet. The Access log is in Common Log format, which lets you use existing log-analyzing scripts on them. All the log files reside in the
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch24.htm (4 of 24) [8/14/02 10:55:30 PM]
$JEEVES-HOME/logs directory. Creating File Aliases You can map virtual paths in the requested URL to an arbitrary real path name on the server's disk. These changes take effect immediately if done from the File Aliasing screen (see Figure 24.3); otherwise, you can make the changes to the alias.properties file. Figure 24.3 : File aliasing using the Admin applet. Configuring MIME The mime.properties file and the Mime Section of the administration Web page allows you to map Mime types to file extensions. This information is sent from the server to the client browser. The browser uses this information to figure out what to do with the file it is about to receive from the server. For example, in the case of a file with a .mov extension, the browser should start up a QuickTime viewer. You can also change the mime.properties file by hand. Loading Servlets into the Web Server To execute servlets, you must map the servlet name to a class that lies somewhere in the server's CLASSPATH environment variable. You can do this from the Servlet Loading screen of the administration Web page (see Figure 24.4). Figure 24.4 : Servlet loading using the Admin applet. Remote servlets can also be loaded by specifying their URL to the location field of the administration page. When you use the Web page, the changes take effect immediately. When you use the servlets.properties file, you can map the servlet called myservlet to the MyServlet class in the mypackage package with the following entry: myservlet.code=mypackage.MyServlet When you change the servlet.properties file, the changes do not take effect until the server is restarted. You can now invoke myservlet with the following URL: http://<server_host>/servlet/myservlet The virtual path /servlet is mapped to the Invoker, which then calls the referenced servlet. Creating Servlet Aliases Servlets can also be mapped to arbitrary document names. When you use the Servlet Aliasing screen (see Figure 24.5), all changes are dynamic. Otherwise, change the rules.properties file. Again, changes to the configuration files take effect after the server has been restarted. Figure 24.5 : Servlet aliasing using the Admin applet. To map myservlet to myservlet.html, put the following line into the rules.properties file: /myservlet.html=myservlet
Jeeves uses an extensible, access-control list framework for controlling requests for files and servlets. Only the Basic HTTP authentication scheme is allowed, but Jeeves accepts the configuration of different authentication schemes when they become available. Access control lists can be associated with any file, directory, or servlet. If a file or directory is not explicitly protected by an ACL, it inherits the protection of its parent directory. If there is no ACL for the entire directory structure, access is granted. If a servlet is not explicitly protected by an ACL, a default is used. If it doesn't exist, access is granted. Servlets can also use their own access-control list, using the security classes available in Jeeves. Jeeves also makes use of security realms. Realms are used to set broad security policies. When users, groups, or access-control lists are added to the Web server, they are assigned to a realm. People who have common security needs can be put into a Single realm, such as the adminRealm. When you want to protect a resource, you associate it with an access-control list in a realm. Jeeves comes with two built-in realms, adminRealm and defaultRealm. Servlet Security The four basic types of servlets are core servlets, local servlets, signed network servlets, and unsigned network servlets. These servlets are treated differently with respect to security. The core servlets and local servlets are thought to be trusted and are granted full access to the server's resources. Signed network servlets are granted a limited subset of privileges, as determined by the site administrator. Unsigned servlets are not trusted and are only executed in a restrictive environment, called the server sandbox. Protecting Web Resources Using the Resource Protection section of the administration Web page (see Figure 24.6), you can assign schemes and realms to Web resources. These resources include documents and servlets. Figure 24.6 : Protecting resources with the administration Web page. Adding Users to Security Realms You can add users to different realms. The easiest way to add users is through the Users screen of the administration Web page (see Figure 24.7). Simply select the realm, and enter the user's name and password. Figure 24.7 : Adding a user to a realm with the Admin applet. Creating Groups of Users You can group together users who should share the same privileges using the Groups section of the administration Web page (see Figure 24.8). Select the realm and group you want to change and enter the user you want to add. You can also create new groups by entering the new group name in the field above the user name. Figure 24.8 : Adding a user to a group with the Admin applet. Creating and Modifying ACLs You can create new ACLs in a realm or add entries to an existing realm from the ACL screen on the administration Web page (see Figure 24.9). To add an entry to an existing ACL, select it from the center window and click the Add ACL Entry button. Figure 24.9 : Creating and modifying ACLs with the administration Web page.
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch24.htm (6 of 24) [8/14/02 10:55:30 PM]
A new window appears. On the far left is a select box with a plus sign and a minus sign. Choose the plus sign to grant privileges or choose the minus sign to restrict them. The next select box lets you choose whether you are modifying the privileges of a group or of an individual user. There is a text field to enter the group or user name. Finally, select the privileges you want (such as GET, POST, or PUT) for that user or group. To add a new ACL, choose the Create ACL button. A box appears prompting you for the ACL name. To delete an ACL, select it from the list and choose the Delete ACL button.
The server developer must use the four interfaces to make the server comply with the servlet API. You then use the server's implementations of these interfaces along with the Servlet, ServletInputStream, and ServletOutputStream classes to write servlets. In this section, you learn the basics behind the classes and interfaces that make up the servlet package. Extending the Servlet Class The Servlet class provides the basic function necessary to create servlets. This class includes the methods shown in Table 24.2. Table 24.2 Methods in the Servlet Class Method Name service(ServletRequest, ServletResponse) init() GetInitParameter(String) GetServletContext() log(String) GetServletInfo() destroy() SetStub(ServletStub) Description Services a single request from a client. Called by system when servlet is first loaded. Gets the named initialization parameter. Returns the servlet context object. Logs a message to the servlet log. Returns a string containing information about the servlet. Destroys servlet and cleans up after it. Sets servlet stub; this is done by the system.
The first step in writing Servlets is extending the Servlet class and overriding the service method. The following servlet prints "Hello World" to the client browser. The first step is to get the OutputStream from the ServletResponse object and create a PrintStream with it. Next, you set the response status to OK using the static variable SC_OK from the ServletResponse class. Then, you set the content type of the response to text/plain and write out the headers. Finally, print the Hello World string to the PrintStream. import java.servlet.*; public class SimpleServlet extends Servlet { public void service( ServletRequest req, ServletResponse res ) { PrintStream ps = new PrintStream(res.getOutputStream()); res.setStatus(ServletResponse.SC_OK); res.setContentType("text/plain"); res.writeHeaders(); ps.println("Hello World"); ps.flush(); } } After you compile the class, load it into the HTTP server using either the servlet.properties file or the servlet loading screen of the administration Web page, as was discussed in the first part of this chapter. You can access the servlet with the following URL: http://<server>/servlet/<servlet_name>
where <servlet_name> is the name you assigned to your servlet when you loaded it into the HTTP server. Sending Information with the ServletResponse Interface The ServletResponse interface allows you to send information to the client's browser. It includes methods for getting an output stream directed at the client, setting the header information, sending errors to the client, and setting the status of the response. Table 24.3 shows more of ServletResponse's methods. ServletResponse also includes a list of static integer variables used in setting the response status. The previous example used the SC_OK variable. Other responses include SC_CREATED, SC_NO_CONTENT, SC_MOVED_PERMANENTLY, SC_MOVED_TEMPORARILY, SC_BAD_REQUEST, SC_UNATHORIZED, SC_FORBIDDEN, SC_NOT_FOUND. For a complete list of methods and variables, see the servlet API documentation. Table 24.3 ServletResponse Methods Method getOuputStream() SendError(int, String) sendRedirect(String) SetContentLength(int) SetContentType(String) SetDateHeader(String, long) SetHeader(String, String) setIntHeader(String, int) WriteHeaders() SetStatus(int) Description Returns the output stream for writing responses. Sends an error message to the client. Sends a redirect response to the client using a specified redirect URL. Sets the content length for this response. Sets the content type for this response. Sets the date header field. Sets the value of a header field. Sets the value of an integer header field. Writes the status line and message headers for this response to the output stream. Sets the status code and a default message for this response.
Receiving Information with the ServletRequest Interface In addition to the ServletResponse object, Servlets get a ServletRequest object as an argument. This object lets the servlet get information directly from the client making the request, as well as from the server that called the servlet. ServletRequest includes methods for getting an input stream from the client, gathering header information, and extracting path and query information from the requested URL. Table 24.4 shows the ServletRequest methods. For complete information, see the servlet API documentation. Table 24.4 ServletRequest Methods Method Name getAuthType() GetContentLength() getContentType() GetDateHeader(String, long) getHeader(String) Description Returns the authentication scheme of the request or null if none. Returns the size of the request entity data or -1 if not known. Returns the MIME type of the request entity data. Returns the value of a date header field. Returns the value of a header field.
getHeader(int) GetHeaderName(int) GetInputStream() GetIntHeader(String, int) getMethod() GetPathInfo() GetPathTranslated() GetProtocol() GetQueryParameter(String) GetQueryParameters() GetQueryString() GetRemoteAddr() GetRemoteHost() GetRemoteUser() GetRequestPath() GetRequestURI() GetServerName() GetServerPort() GetServletPath()
Returns the nth header field. Returns the name of the nth header field. Returns an input stream for reading request data. Returns the value of an integer header field. Returns the method with which the request was made. Returns optional extra path information following the servlet path and preceding the query. string. Returns extra path information translated to a real path. Returns the protocol and version of the request. Returns the value of the specified query string parameter. Returns a hash table of query string parameter values. Returns the query string part of the servlet URL. Returns the IP address of the agent that sent the request. Returns name of the host making the request. Returns the name of the user making the request or null if not known. Returns the part of the request URI that corresponds to the servlet path, plus optional path information. Returns the request URI. Returns the host name of the server. Returns the port number on which this request was received. Returns the part of the request URI that refers to the servlet being invoked.
Getting Information with the ServletContext Interface The getServletContext method from the Servlet class returns a ServletContext. This object lets you find out information about the environment in which the servlet is running. The getServerInfo method returns the name and version of the server running. The getServlet method returns a servlet with its name, and the getServlets method returns an enumeration of all the available servlets in this context.
values from the HTML form. The following example shows how to get the value from the form field called field_name. Public class SimpleFormServlet extends FormServlet { public void sendResponse(ServletResponse res, Hashtable table) { String field_value = table.get("field_name"); ... } } Using the Filter Interface to Embed Servlets in HTML Pages Using the Jeeves Filter interface, you can create servlets that can be embedded in HTML pages using a server-side include statement. The following server-side include statement calls myServlet and passes the name1 and name2 parameters to it in the form of a hash table: <SERVLET CODE="myServlet" name1="value1" name2="value2"> The following servlet inserts the date into the Web page it is embedded in: import java.servlet.*; import java.io.*; import java.util.*; public class SSIServlet extends Servlet implements Filter { public void service(InputStream is, OutputStream os, Hashtable params) throws java.io.IOException { Date now = new Date(); PrintStream ps = new PrintStream(os); ps.println(today); ps.flush(); } } Notice that the service method accepts different arguments from normal. Instead of ServletRequest and ServletResponse, the arguments to service are an InputStream, OutputStream, and Hashtable. The hash table is used to store the parameters passed to the servlet by the server-side include statement. Generating HTML with the Jeeves HTML Classes Sun provides a package of classes that generate HTML. The package includes the HtmlContainer and HtmlElement interfaces, as well as the HtmlContainerImpl, HtmlPage, HtmlTag, HtmlTagPair, and HtmlText classes. Start by creating a new HtmlPage object. Then, using the addTag, addTagPair, and addText methods, insert the necessary HTML. The following example shows a simple use of these classes: import java.servlet.*; import sun.server.html import java.io.*; public class HtmlServlet extends Servlet { pubic service(ServletRequest req, ServletResponse res) throws Exception { res.setContentType("text/html"); res.setStatus(ServletResponse.SC_OK);
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch24.htm (11 of 24) [8/14/02 10:55:30 PM]
OutputStream os = res.getOutputStream(); HtmlPage page = new HtmlPage("A Simple HTML page"); page.addTagPair("H1", "This is a Simple HTML Page"); page.addTag("p"); page.addText("This page was generated by the sun.server.html package"); page.write(os); os.flush(); } }
ps.println("<HTML><HEAD>"); ps.println("<TITLE>Registration Information</TITLE>"); ps.println("</HEAD><BODY>"); ps.println("<H2>This is the information you submitted</H2>"); ps.println("<TABLE Border>"); ps.println("<CAPTION>Your Registration Information</CAPTION>"); ps.println("<TR><TD><B>Name</B></TD><TD>" +name +"</TD></TR>"); ps.println("<TR><TD><B>Title</B></TD><TD>" +title +"</TD></TR>"); ps.println("<TR><TD><B>Company</B></TD><TD>" +company +"</TD></TR>"); ps.println("<TR><TD><B>Address</B></TD><TD>" +address +"</TD></TR>"); ps.println("</TABLE>"); ps.println("</BODY></HTML>"); ps.flush(); }
The next step is to connect to a SQL database. The Java Database Connectivity package makes this possible. The servlet also uses a very simple SQL generator. The generator class takes an object of the DBRecord class as an argument and returns a string containing SQL commands. DBRecord is a container class that holds the table's name, its primary key, and the names and values of the fields to be changed. To increase the performance of the database queries, the servlet connects to the database in its init() method. This makes the servlet connect to the database as soon as it is loaded. By telling Jeeves to load the servlet when the server starts up, you can reduce the overhead associated with reestablishing the connection for each request. See the previous section on loading servlets to learn how to make Jeeves load the servlet at startup. Public class JDBCServlet extends FormServlet { Connection con; Statement stmt; public void init() { try { Class.forName("imaginary.sql.iMsqlDriver"); String url = "jdbc:msql://pandora.scripps.edu:1112/userreg"; connection = DriverManager.getConnection(url, "guest", ""); } catch (Exception e) { System.out.println(e.getMessage()); e.printStackTrace(); } } public void destroy() throws Exception { ocn.close(); } } The JDBCServlet's init() method made the connection to an msql database with the imaginary.sql.iMsqlDriver. The connection could have been made to any database with a JDBC driver. Even databases that use only ODBC could be used with an ODBC-JDBC bridge. Note The Weblogic Company has both pure Java JDBC drivers for most of the major RDBMSs and ODBC-JDBC bridges. To find out more, check out http://www.weblogic.com/.
Caution Since the Jeeves Web server is multithreaded, a servlet may be called by several handler threads simultaneously. You must make your servlets thread-safe by using synchronized methods and blocking when necessary.
Listing 24.1 JDBCSERVLET.JAVA-The sendResponse Method of the JDBCServlet Public void sendResponse(Servlet res, Hashtable table) throws Exception { Statement stmt; SQLgen dbaction; ResultSet rs; // Create a new DBRecord object and fill its fields with // the values obtained from the HTML form. DBRecord rec = new DBRecord(); rec.setTable("userreg"); String name = (String) table.get("name"); if (name.length() > 0) { rec.setField(new DBField("name", name); } String title = (String) table.get("title"); if (title.length() > 0) { rec.setField(new DBField("title", title); } String company = (String) table.get("company"); if (company.length() > 0) { rec.setField(new DBField("company", company); } String address = (String) table.get("address"); if (address.length() > 0) { rec.setField(new DBField("address", address); } String action = (String) table.get("action"); if (action.equals("Insert")) { rec.setActionInsert(); } // Create a generator object with the completed DBRecord object. dbaction = new SQLgen(rec); // Create a JDBC Statement object. stmt = con.createStatement(); // Execute the SQL query generated by the SQLgen object rs = stmt.executeQuery(dbaction.getSQL()); stmt.close(); // Create a new PrintStream to output an acknowledgment of the users // registration PrintStream ps = new PrintStream(res.getOutputStream());
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch24.htm (14 of 24) [8/14/02 10:55:30 PM]
log("JDBCServlet running"); res.setStatus(ServletResponse.SC_OK); res.setContentType("text/html"); res.writeHeaders(); ps.println("<HTML><HEAD>"); ps.println("<TITLE>Registration Complete</TITLE>"); ps.println("</HEAD><BODY>"); ps.println("<H2>Thank you for registering</H2>"); ps.println("</BODY></HTML>"); ps.flush(); }
Listing 24.2 JDBCSERVLET.JAVA-Complete Code for the JDBC Servlet import java.sql.*; import java.io.*; import java.util.*;
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch24.htm (15 of 24) [8/14/02 10:55:30 PM]
import sun.server.http.*; import java.servlet.*; import db.*; public class JDBCServlet extends FormServlet { Connection con; Statement stmt; public void init() { try { Class.forName("imaginary.sql.iMsqlDriver"); String url = "jdbc:msql://pandora.scripps.edu:1112/userreg"; con = DriverManager.getConnection(url, "guest", ""); } catch (Exception e) { System.out.println(e.getMessage()); e.printStackTrace(); } } public void destroy(){ try { con.close(); } catch (Exception e) { System.out.println(e.getMessage()); e.printStackTrace(); } } public void sendResponse(ServletResponse res, Hashtable table) throws Exception { Statement stmt; SQLgen dbaction; ResultSet rs; // Create a new DBRecord object and fill its fields with // the values obtained from the HTML form. DBRecord rec = new DBRecord(); rec.setTable("userreg"); String name = (String) table.get("name"); if (name.length() > 0) { rec.setField(new DBField("name", name)); } String title = (String) table.get("title"); if (title.length() > 0) { rec.setField(new DBField("title", title)); } String company = (String) table.get("company"); if (company.length() > 0) { rec.setField(new DBField("company", company)); } String address = (String) table.get("address"); if (address.length() > 0) { rec.setField(new DBField("address", address)); } String action = (String) table.get("action");
if (action.equals("Insert")) { rec.setActionInsert(); } if (action.equals("Search")) { rec.setActionInsert(); } // Create a generator object with the completed DBRecord object. dbaction = new SQLgen(rec); try { // Create a JDBC Statement object. stmt = con.createStatement(); // Execute the SQL query generated by the SQLgen object rs = stmt.executeQuery(dbaction.getSQL()); stmt.close(); // Create a new PrintStream to output an acknowledgment of the users // registration PrintStream ps = new PrintStream(res.getOutputStream()); log("JDBCServlet running"); res.setStatus(ServletResponse.SC_OK); res.setContentType("text/html"); res.writeHeaders(); ps.println("<HTML><HEAD>"); ps.println("<TITLE>Registration Complete</TITLE>"); ps.println("</HEAD><BODY>"); ps.println("<H2>Thank you for registering</H2>"); if (action.equals("Search")) { ps.println("<TABLE Border>"); ps.println("<TR><TD><B>Name</TD><TD>Title</TD>"); ps.println("<TD>Company</TD><TD>Address</TD></TR>"); while(rs.next()) { name = rs.getString(1); title = rs.getString(2); company = rs.getString(3); address = rs.getString(4); ps.println("<TR><TD>" +name +"</TD><TD>" +title +"</TD><TD>" ); ps.println(company +"</TD><TD>" + address +"</TD></TR>"); } ps.println("</TABLE>"); } if (action.equals("Insert")) { ps.println("<H2>This is the information you submitted</H2>"); ps.println("<TABLE Border>"); ps.println("<CAPTION>Your Registration Information</CAPTION>"); ps.println("<TR><TD><B>Name</B></TD><TD>" +name +"</TD></TR>"); ps.println("<TR><TD><B>Title</B></TD><TD>" +title +"</TD></TR>"); ps.println("<TR><TD><B>Company</B></TD><TD>" +company +"</TD></TR>"); ps.println("<TR><TD><B>Address</B></TD><TD>" +address +"</TD></TR>"); ps.println("</TABLE>"); } ps.println("</BODY></HTML>");
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch24.htm (17 of 24) [8/14/02 10:55:30 PM]
Note The object serialization classes are not included with the JDK as of release 1.0.2. They can be downloaded with the Remote Method Invocation package from the Javasoft Web site.
Note For more information on object serialization, see Sun's white paper: Java Object Serialization Specification, available at the Javasoft Web site (http://www.javasoft.com/).
Note Jeeves provides an elaborate access-control mechanism that can be used to protect the server from dangerous agents. You can also create servlet-specific authentication procedures with the security classes.
Listing 24.3 REMOTEAGENCYSERVLET.JAVA-The RemoteAgencyServlet Class import java.io.*; import java.servlet.*; import java.net.*; public class RemoteAgencyServlet extends Servlet { public String getServletInfo() { return "Remote Agent Servlet"; } public void service(ServletRequest req, ServletResponse res) throws Exception { // Load the agent from the client's agent launcher ObjectInputStream ois = new ObjectInputStream(req.getInputStream());
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch24.htm (19 of 24) [8/14/02 10:55:30 PM]
// Cast the incoming object to AgentInterface, which is a simple that // all agents must implement. AgentInterface agent = (AgentInterface) ois.readObject(); // Call the agents run method. agent.run(); // Now that the agent has completed its task, find out where it lives. URL agenthome = agent.getAgentHome(); int port = agenthome.getPort(); String host = agenthome.getHost(); String file = agenthome.getFile(); // Now that you know where the agent lives send it on its way. // Open up a connection to the agents home, which is a servlet compliant // web server. Socket socket = new Socket(host, port); PrintStream ps = new PrintStream(socket.getOutputStream()); // Request to HomeAgentServlet from the web server ps.println("POST" +file); ps.flush(); // Create an ObjectOutputStream ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream()); oos.writeObject(agent); oos.flush(); } }
Listing 24.4 JDBCAGENT.JAVA-The JDBCAgent Class import import import import import java.sql.*; java.io.*; java.util.*; java.net.URL; db.*;
public class JDBCAgent implements AgentInterface { Connection con; Statement stmt; DBRecord rec; JDBCInfo jdbcinfo; Vector v; URL agenthome; public try { String String String String void run() { url = jdbcinfo.getURL(); driver = jdbcinfo.getDriver(); user = jdbcinfo.getUser(); password = jdbcinfo.getPassword();
Class.forName(driver); con = DriverManager.getConnection(url, user, password); stmt = con.createStatement(); SQLgen dbaction = new SQLgen(rec); ResultSet rs = stmt.executeQuery(dbaction.getSQL()); while(rs.next()) { Hashtable hashtable = new Hashtable(); hashtable.put("name", rs.getString(1)); hashtable.put("id", rs.getString(2)); hashtable.put("company", rs.getString(3)); hashtable.put("location", rs.getString(4)); v.addElement(hashtable); } stmt.close(); } catch (Exception e) { System.out.println(e.getMessage()); e.printStackTrace(); } } public Vector getResultVector() { return v; } public void setQuery(DBRecord rec) { this.rec = rec; } public void setJDBCInfo(JDBCInfo jdbcinfo) { this.jdbcinfo = jdbcinfo; } public void setAgentHome(URL agenthome) { this.agenthome = agenthome;
Listing 24.5 HOMEAGENCYSERVLET.JAVA-The HomeAgency Class import java.io.*; import java.servlet.*; import java.net.*; public class HomeAgencyServlet extends Servlet { public String getServletInfo() { return "Agent Home Servlet"; } public void service(ServletRequest req, ServletResponse res) throws Exception { // create ObjectInputStream from the InputStream originating a the RemoteAgencyServlet. ObjectInputStream ois = new ObjectInputStream(req.getInputStream()); // Read in the Agent object from the stream. AgentInterface agent = (AgentInterface) ois.readObject(); // open a file to store the agent in until debriefing FileOutputStream fos = new FileOutputStream("/agents/storage/agent99"); // Create an ObjectOutputStream pointing to the file ObjectOutputStream oos = new ObjectOutputStream(fos); // Write the agent to the file. oos.writeObject(agent); oos.flush(); fos.close(); } }
The AgentLauncher sets the agent's mission and then transports it to the RemoteAgencyServlet. It uses a class called JDBCInfo to set the specific database information. JDBCInfo is a container class that holds information on which database driver to use, the URL, and the user name and password for the database. The AgentLauncher then places the query in a DBRecord object. Finally, it contacts the agency servlet on a remote HTTP server and uses object serialization to transport the agent to the remote agency (see Listing 24.6).
Listing 24.6 AGENTLAUNCHER.JAVA-The AgentLauncher Class import import import import java.io.*; java.util.*; java.net.*; db.*;
public class AgentLauncher { public static void main(String[] argv) throws Exception { JDBCAgent jdbcagent = new JDBCAgent(); JDBCInfo jdbcinfo = new JDBCInfo(); jdbcinfo.setDriver("imaginary.sql.iMsqlDriver"); jdbcinfo.setURL("jdbc:msql://agency.agentworld.com:1112/agentdb"); jdbcinfo.setUser("agent99"); jdbcinfo.setPassword(""); jdbcagent.setJDBCInfo(jdbcinfo); DBRecord rec = new DBRecord(); rec.setTable("agents"); rec.setPrimaryKey("recordnumber"); rec.setField(new DBField("location", "North America")); rec.setActionSelect(); jdbcagent.setQuery(rec); URL homeurl = new URL("http://pandora.scripps.edu/servlet/homeagency"); jdbcagent.setAgentHome(homeurl); Socket socket = new Socket("buddha", 8888); PrintStream ps = new PrintStream(socket.getOutputStream()); ps.println("POST /servlet/RemoteAgency"); ps.flush(); ObjectOutputStream oos = new ObjectOutputStream(ps); oos.writeObject(jdbcagent); oos.flush(); } }
The AgentDebriefing class, shown in Listing 24.7, restores the agent from the file that it is stored in and requests the information it was sent to gather. This program is run by the user when the agent has returned. The HomeAgency could have been designed to send mail, letting the user know when an agent returns.
Listing 24.7 AGENTDEBRIEFING.JAVA-The AgentDebriefing class import import import import java.io.*; java.util.*; java.net.Socket; db.*;
public class AgentDebriefing { public static void main(String[] argv) throws Exception { JDBCAgent jdbcagent = new JDBCAgent(); FileInputStream fis = new FileInputStream("/agents/storage/agent99"); ObjectInputStream ois = new ObjectInputStream(fis); JDBCAgent agent = (JDBCAgent) ois.readObject(); Vector v = agent.getResultVector(); for (Enumeration e=v.elements(); e.hasMoreElements();) { Hashtable hash = (Hashtable) (e.nextElement()); System.out.println("Name: " +hash.get("name")); System.out.println("id: " +hash.get("id")); System.out.println("Company: " +hash.get("company")); System.out.println("location: " +hash.get("location")); } } }
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f24-2.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f24-3.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f24-4.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f24-5.gif
CONTENTS
G
G G G G G
Architectural Overview H Handling the HTTP Protocol with the Daemon Module H Managing the Server Information Space with the Resource Module H Maintaining Server State via Object Persistence H Pre and Post Request Processing with Resource Filters Jigsaw Interface H The HTTPResource Class H The FilteredResource Class H The DirectoryResource Class H The FileResource Class Installation and Setup of the Jigsaw HTTP Server Adding Content to the Jigsaw Server Extending the Server with Java Writing Resource Filters in Java Handling Forms and the POST Method in Java
Jigsaw is the first Web server written entirely in Java that is freely available and uses HyperText Transport Protocol (HTTP). Two of its major design goals are portability and extensibility. The Jigsaw server runs on most machines for which a Java environment is available. The author has tested the server in a number of these environments. Some examples are Microsoft Windows 95/NT, SunMicrosystems Solaris, and Linux. This chapter was developed using Jigsaw running on Linux 2.0 and a port of the Sun JDK. It was also tested with the Kaffe Java interpreter. The Jigsaw server can be extended by writing new resource objects in Java. One possible extension would be a replacement for CGI scripts. Using this extension does not preclude the use of normal CGI scripts. The support of regular CGI scripts allows you to migrate existing CGI applications into Jigsaw. Portability adds tremendous value to the Jigsaw server when you select a hardware and software base for your Web applications. This chapter focuses on the extensibility of the server.
Architectural Overview
Jigsaw is an object-oriented Web server. Each resource exported by the server is mapped to a Java object. Each resource can be configured independently and maintains its own state through a persistency mechanism provided by the Jigsaw runtime. The major components of the Jigsaw server are the daemon module and the resource module.
replies. The most important part of the daemon module is the HTTPD object. This object runs the main processing loop of the server handling incoming connections and managing other objects in the server process, such as:
G G G G G
The authentication realm manager, which handles authentication of selected server resources. The client pool, which handles accepted connections. The logger, which logs server activity. The root resource of the server, which links the protocol module to the resource modules. The resource store manager, which is responsible for wrapping each file or directory into a Resource instance. This module also keeps track of all the loaded resources and unloads them when they do not appear to be needed.
The final important concept of Jigsaw is resource filters. A resource filter is a Java resource that contains a set of attributes and one or more methods. Like all other Jigsaw resources, its attributes are persistent. This provides some powerful possibilities, as you will see later in the filter example Each HTTP request is processed by a target resource instance. Most resource classes provided by Jigsaw inherit from the FilteredResource class. All instances of this class inherit a set of filters that are subclasses of ResourceFilter. This provides a callback to the filter twice during resource processing. Once during lookup, before the target has been selected, the ingoingFilter method is called with the request as a parameter. After the request has been processed by the target resource, the outgoingFilter method is called with both the request and reply as parameters.
Jigsaw Interface
Jigsaw provides many classes and attributes, listed in Table 25.1, that you can use to extend and control the behavior of the server. For the sake of space, you will only look at classes central to the Jigsaw server design, or those needed by the examples.
Url
Server
Quality
title
Content-language
This is the language of the resource. It is used by the NegotiatedResource to select among its set of variant resources.The value of this attribute can be extracted from the resource content if it is an HTML file that includes some appropriate <META> tag. Otherwise, it is provided for informational purposes.type: This attribute is a computed and/or editable LanguageAttribute. default value: This attribute is undefined. This is the encoding method. This can only be a single token as described in the HTTP/1.0 protocol specification. type: This attribute is a computed and/or editable EncodingAttribute. default value: This attribute is undefined. This is the MIME type of the resource. type: This attribute is a computed and/or editable MIMETypeAttribute.default value: This attribute is undefined. This is the length of the resource's content. type: This attribute is a computed IntegerAttribute. default value: This attribute is undefined. It is up to subclasses of this resource to either generate it dynamically or cache it from the FileResource. The FileResource gets this information from calls to the file system. This is the date of the last modification to this resource. type: This attribute is a computed and/or editable DateAttribute. default value: This attribute is undefined. See the default value of content-length above for additional information. This is the date on which this resource expires. type: This attribute is a computed and/or editable DateAttribute. default value: This attribute is undefined. See the default value of content-length above for additional information. This is any icon to be associated with this resource. type: This attribute is an editable StringAttribute. default value: This attribute is undefined. This attribute defines the allowed drift between the real content of a resource and the one that is sent as request replies. The bigger this value, the more efficient the server can be, since it can reuse cached request replies for a longer time. This attribute takes affect only if it is defined and if the resource provides a meaningful last-modified attribute value. type: This attribute is an editable IntegerAttribute. default value: This attribute is undefined.
Content-encoding
Content-type
Content-length
Last-modified
Expires
Icon
Maxage
The directory resource is the basic resource to export file-system directories. It keeps track of all its children resources, creates them dynamically if needed, and is also able to create negotiated resources on-the-fly. The DirectoryResource class inherits HTTPResource and FilteredResource (see Table 25.2). Table 25.2 Attributes of DirectoryResource Attribute directory Description This is the physical directory that this resource exports. type: This attribute is a computed FileAttribute and is not saved.default value: This attribute is computed by concatenating, in the appropriate filesystem-dependent way, the parent's resource directory value with this directory identifier. This is the name of the file to be used as the resource store database in this directory. type: This attribute is an editable FilenameAttribute and is mandatory. default value: This attribute is computed by concatenating, in the appropriate file-system-dependent way, the parent's resource directory value with this directory identifier. Should the directory produce a relocation reply when accessed through an invalid URL? A common way of handling invalid directory access is to produce a relocation reply so that the browser gets access to the directory through a valid URL. The URL http://www.w3.org/pub is invalid because pub is a directory. The correct URL is http://www.w3.org/pub/. When this flag is set to true, the directory resource produces the appropriate relocation reply. type: This attribute is an editable BooleanAttribute and is not saved. default value: This attribute value defaults to true. Should this directory automatically stay in sync with the underlying physical directory? The directory resource maintains a cache of its list of children, which may be outdated if you change the directory through direct file system access.When this flag is true, the directory resource makes its best effort to stay in sync with the file system by adopting the following lookup algorithm. First, look up children in the cache list. If this fails, check to see if an appropriate file exists. If such a file exists, hand it to the ResourceIndexer and install the resulting resource, if any, as a new child of the directory resource.type: This attribute is an editable BooleanAttribute. default value: This attribute defaults to true. This attribute should name an existing child resource that will be used as the index resource of the directory. All accesses to the directory will be delegated this resource. type: This attribute is an editable StringAttribute. default value: This attribute is computed by concatenating, in the appropriate file-system-dependent way, the parent's resource directory value with this directory identifier. This is the name of a directory that holds the icons for this directory. Each HTTPResource has an optional icon attribute. When a directory resource needs to produce a listing it dereferences each icon relative to its icon directory. type: This attribute is an editable StringAttribute, specifying the path tothe icon directory. default value: This attribute defaults to /icons.
storeid
relocate
extensible
index
icondir
dirstamp
This is the date on which the directory resource last checked its consistency against the underlying physical directory. type: This attribute is a computed DateAttribute that is noneditable.default value: This attribute defaults to -1 (undefined). Should the directory resource automatically create a NegotiatedResource? If this flag is true, the directory resource automatically creates negotiable resources on top of normal resources. Each time a new resource is added to the directory, the resource looks for a resource having the new child name with possibly different extensions. If it succeeds, either the resource found is already a negotiated resource, in which case the new child is added as one of its variant resources, or the negotiated resource must not already exist. The directory resource then creates it with only one variant, the new child resource. type: This attribute is an editable BooleanAttribute. default value: This attribute defaults to false.
negotiable
putable
Filestamp
Start your favorite browser and go to URL http://www.w3.org/pub/WWW/Jigsaw/#Getting. Select jigsaw.zip for Microsoft Windows 95/NT or jigsaw.tar.gz for UNIX. When the file has finished downloading, unzip or gunzip and un-tar, as needed. Both archives unpack into a directory structure starting at Jigsaw. The archive has long filenames so make sure you have an unzip that can handle this situation correctly. In the following discussion, the term Windows refers to Microsoft Windows 95/NT. Similarly, UNIX refers to the UNIX operating system. In this sample setup, unpack the Windows archive to D:\ and /usr/www in UNIX. Call this BASEDIR for short. Make sure to replace BASEDIR as appropriate for your setup. In this example, use D:\ or /usr/www instead of BASEDIR. Now let the Java interpreter know where to find the Jigsaw classes. On Windows, type the command: SET CLASSPATH=BASEDIR\Jigsaw\classes\jigsaw.zip And on UNIX, choose one of the following, depending on your shell: SH: CLASSPATH=BASEDIR/Jigsaw/classes/jigsaw.zip ; export CLASSPATH or CSH: setenv CLASSPATH BASEDIR/Jigsaw/classes/jigsaw.zip. You are now ready to run the server for the first time. On Windows, type the following command: java w3c.jigsaw.http.httpd -host your-host.your-domain -root BASEDIR\Jigsaw\Jigsaw. If you are running UNIX, type: java w3c.jigsaw.http.httpd -host your-host.your-domain -root BASEDIR/Jigsaw/Jigsaw. Replace your-host.your-domain with the host name and domain of your machine. Jigsaw starts executing, and you should see one of the following: In Windows: loading properties from: d:\Jigsaw\Jigsaw\config\httpd.props [httpd]: listening at:http://your-host.your-domain:9999 In UNIX: loading properties from: /usr/WWW/Jigsaw/Jigsaw/config/httpd.props [httpd]: listening at:http://your-host.your-domain:9999
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch25.htm (7 of 18) [8/14/02 10:55:47 PM]
Now start your favorite browser and go to URL http://your-host.your-domain:9999. Finally, read the configuration tutorial and other documentation that comes with the server. No configuration changes are needed to follow along with the examples in this chapter. Note If your server will be accessible by others, you'll want to use the section on protecting the Admin resource in the Jigsaw documentation. (http://your-host.your-domain:9999/User/Tutorials/ configuration.html#authentication)
Caution There is a security problem in the current version of Jigsaw. Make sure you understand the implications, especially if you are running UNIX. As of version 1.0, Jigsaw does not give up its root privileges, so you may want to use another port such as 8080 or the default 9999, and run the server as a normal user. There are plans to add system calls to switch to a nonprivileged user in the next Jigsaw release.
Tip Using the telnet program, you can verify that your server is working without having to access a browser. On UNIX, type telnet your-host.your-domain 9999 when you see: Connected to your-host.your-domain Escape character is "^]". type HEAD / HTTP/1.0 and press Enter twice. The server should return: HTTP/1.0 200 OK Content-Length: 701 Content-Type: text/html Last-Modified: 25 May 1996 15:05:56 GMT Server: Jigsaw/1.0a Date: 22 Aug 1996 23:00:33 GMT You can also start the Windows telnet program and select Connect Remote ystem. Enter the Host Name and replace the telnet Port with 9999 in the dialog box. Then type the line: HEAD / HTTP/1.0 and press Enter twice.
Now that you have the server running, you can add some content. By default, additional content is added to Jigsaw in the BASEDIR/Jigsaw/Jigsaw/WWW directory tree. Be sure you change your directory to the one listed above before continuing. Obtain either example.zip or example.tar. Unzip or untar the archives as appropriate. This extracts a small collection of html and gif files under the que directory. Now start your browser and go to HTTP://your-host.your-domain/que/. This brings up a directory-style listing of the files just extracted, as seen in Figure 25.1-not quite what you wanted, so now open the URL HTTP://your-host.yourdomain/Admin/Editor/que. Figure 25.1 : Results of opening http://your-host.Your-domain: 9999/que/. Move on to the form now displayed and change the entry index: to index.html. Then click the OK button at the bottom of the page as shown in Figure 25.2. Reopen the URL HTTP://your-host.your-domain/que/. This time you see the content of index.html shown in Figure 25.3 instead of the directory listing. Figure 25.2 : Form to add index.html as the index for the que resource. Figure 25.3 : Browser rendered contents of que/index.html. Note You may need to clear your browser's cache so the URL is displayed correctly.
Finally, add an existing Java class to your que resource. Open URL HTTP://your-host.your-domain/Admin/Editor/que and select the AddingResources link at the bottom of the page. Enter Memory in the name: field. Enter w3c.jigsaw.status.GcStat in the class: field and press the OK button as seen in Figure 25.4. Figure 25.4 : Form to add a Java resource to the server namespace. Opening the URL HTTP://your-host.your-domain/que/Memory should now show the memory status for your server, as seen in Figure 25.5. Figure 25.5 : Results of accessing the Java resource added as Que/Memory . As you can see, it is easy to add existing document trees to the server and specify a start page for the tree.
G G
If you are adding a dynamic resource like the GcStat class, as described in the previous section, create a subclass of HTTPResource. If the intent is to serve files, create a subclass of FileResource. If your resource is being designed to handle forms, create a subclass of PostableResource.
If your new resource will have child resources, create a subclass of DirectoryResource.
For this example, use HTTPResource. The only other initial decision you need to make is the package name for your new resource. Jigsaw does not impose any restrictions on the name you assign your package as long as the Java interpreter can find it via the CLASSPATH environment variable. Caution Security Note: Keep in mind the possibility of someone adding code to your server via CLASSPATH if it points to a world/group writable directory.
Unzip or untar the classes.zip or classes.tar file to BASEDIR/Jigsaw. This creates files under BASEDIR/Jigsaw/que/que/examples. You now know enough to create the BASEDIR/Jigsaw/que/que/examples/HelloJigsaw.java (refer to Listing 25.1) source file, as follows: package que.examples import w3c.jigsaw.http.*; import w3c.jigsaw.resources.*; import w3c.jigsaw.html.*; public class HelloJigsaw extends HTTPResource { Now you need to decide on the attributes for your new resource. To keep things simple, only deal with the message text returned by your resource. // message attribute index protected static int ATTR_MESSAGE = -1 ; static { Attribute attrib = null ; Class HelloClass = null ; try { HelloClass = Class.forName("que.examples.HelloJigsaw"); } catch (Exception ex) { ex.printStackTrace() ; System.exit(1) ; } After declaring your attributes, register them with the AttributeRegistery. The registry keeps track of all the attributes of all resource classes. For each class the registry knows about, it maintains an ordered list of the attributes declared by the class. The attribute registry returns an index for each attribute that is registered. You can use the index as a parameter to the setValue and getValue methods of the AttributeHolder class to obtain the attribute value. // register our message attribute: attrib = new StringAttribute("message", "Hello Jigsaw World!", Attribute.EDITABLE); ATTR_MESSAGE = AttributeRegistery.registerAttribute (HelloClass, attrib) ; } Now, implement the behavior of your resource. The only HTTP method this resource allows is the GET method. Generate a reply at
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch25.htm (10 of 18) [8/14/02 10:55:48 PM]
each invocation of this resource using the HtmlGenerator class provided by Jigsaw. // Print our message in response to the HTTP GET request public Reply get(Request request) throws HTTPException { // create HTML generator and fill in titles: HtmlGenerator gen = new HtmlGenerator("HelloJigsaw"); gen.append("<h1>Our first Jigsaw extension demo</h1>"); // print our message: gen.append("<p>"+getValue(ATTR_MESSAGE, null)); // finish off the reply Reply rep = request.makeReply(HTTP.OK) ; rep.setStream(gen) ; return rep ; } }
Listing 25.1 HelloJigsaw.java-A Class to Respond to the HTTP GET Method package que.examples ; import w3c.jigsaw.http.*; import w3c.jigsaw.resources.*; import w3c.jigsaw.html.*; public class HelloJigsaw extends HTTPResource { // message attribute index protected static int ATTR_MESSAGE = -1; static { Attribute attrib = null; Class HelloClass = null; try { HelloClass = Class.forName("que.examples.HelloJigsaw"); } catch (Exception ex) { ex.printStackTrace(); System.exit(1); } // register our message attribute: attrib = new StringAttribute("message", "Hello Jigsaw World!", Attribute.EDITABLE); ATTR_MESSAGE = AttributeRegistery.registerAttribute(HelloClass, attrib); } // Print our message in response to the HTTP GET request public Reply get(Request request) throws HTTPException { // create HTML generator and fill in titles: HtmlGenerator gen = new HtmlGenerator("HelloJigsaw"); gen.append("<h1>Our first Jigsaw extension demo</h1>"); // print our message:
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch25.htm (11 of 18) [8/14/02 10:55:48 PM]
gen.append("<p>"+getValue(ATTR_MESSAGE, null)); // finish off the reply Reply rep = request.makeReply(HTTP.OK) ; rep.setStream(gen); return rep; } }
Now add your new resource to the server. First, stop the server and update the CLASSPATH environment variable so the server can find your new class. Under Windows: SET CLASSPATH=BASEDIR\Jigsaw\classes\jigsaw.zip;BASEDIR\Jigsaw\que And on UNIX: SH: CLASSPATH=BASEDIR/Jigsaw/classes/jigsaw.zip:BASEDIR/Jigsaw/que ; export CLASSPATH or CSH: setenv CLASSPATH BASEDIR/Jigsaw/classes/jigsaw.zip:BASEDIR/Jigsaw/que. Restart the server. Open the URL http://your-host.your-domain:9999/Admin/Editor/que. Select the AddingResources link at the bottom of the page. Type Hello in the name: field. Type que.examples.HelloJigsaw in the class: field and click OK as shown in Figure 25.6. Figure 25.6 : Form to add your Hello Java resource to the server's namespace. If Jigsaw returns the error message "The field class has an incorrect value," verify that the class name was entered correctly. If the class value is correct, check the CLASSPATH variable in your environment. You are returned to the Admin/Editor/que screen if the change was successful. Opening URL http://your-host.yourdomain:9999/que/Hello will execute the new class returning the text "Hello Jigsaw World!"
import w3c.jigsaw.resources.*; public class CountingFilter extends ResourceFilter { // counter attribute index. protected static int ATTR_COUNTER = -1 ; static { Attribute counterattrib = null ; Class CountingClass = null ; try { CountingClass = Class.forName("que.examples.filter.CountingFilter") ; } catch (Exception ex) { ex.printStackTrace() ; System.exit(1) ; } Now create an attribute for your class, an integer. This attribute is persistent so this is all you need to do to keep a filtered count for the lifetime of your server. counterattrib = new IntegerAttribute("counter" , new Integer(0) , Attribute.EDITABLE) ; ATTR_COUNTER = AttributeRegistery.registerAttribute(CountingClass, counterattrib); } This method is called during resource lookup with the HTTP request as the parameter. public synchronized int ingoingFilter(Request request) { // get our counter attribute int i = getInt (ATTR_COUNTER, 0) ; // put it back plus one setInt(ATTR_COUNTER, i+1) ; Returning DontCallOutgoing informs the target filtered resource that you have done your work and your outgoingFilter method does not need to be called after resource processing. Also, you do not need to declare an outgoingFilter method because your superclass provides an empty method. return DontCallOutgoing ; } } Listing 25.2 shows the CountingFilter class.
Listing 25.2 CountingFilter.java-Count Number of "Hits" on Filtered Resource package que.examples.filter; import w3c.jigsaw.http.*; import w3c.jigsaw.resources.*; public class CountingFilter extends ResourceFilter { // counter attribute index.
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch25.htm (13 of 18) [8/14/02 10:55:48 PM]
protected static int ATTR_COUNTER = -1 ; static { Attribute counterattrib = null ; Class CountingClass = null ; try { CountingClass = Class.forName("que.examples.filter.CountingFilter") ; } catch (Exception ex) { ex.printStackTrace() ; System.exit(1) ; } counterattrib = new IntegerAttribute("counter" , new Integer(0) , Attribute.EDITABLE) ; ATTR_COUNTER = AttributeRegistery.registerAttribute(CountingClass, counterattrib) ; } public synchronized int ingoingFilter(Request request) { // get our counter attribute int i = getInt (ATTR_COUNTER, 0) ; // put it back plus one setInt(ATTR_COUNTER, i+1) ; return DontCallOutgoing ; } }
Now plug your filter into the server. If you followed along with the previous section, your server does not need any changes. If not, go back and update your CLASSPATH environment variable and restart the server as detailed in the previous section. Open URL http://your-host.your-domain:9999/Admin/Editor/que to edit the properties of the que directory resource. Follow the AddFilter link at the bottom of the page. Enter que.examples.filter.CountingFilter in the Filter's class: field and click OK, as shown in Figure 25.7. This creates two additional links at the bottom of the que.examples.filter.CountingFilter page. Figure 25.7 : Form to add your Java filter to the server's namespace. These new links access the properties of the filter. Here you see a single attribute counter that is initially set to 0. You do not need to enter anything in the identifier: field on this page. A link to ShadowByque.examples.filter.CountingFilter is also added to the page. This takes you to attributes shadowed by que.examples.filter.CountingFilter. Nothing needs to be changed there. Now when the resource que is requested from your server, the ingoingFilter method of CountingFilter will be called incrementing the integer attribute counter. You can reload the filters attribute page to view the counter as shown in Figure 25.8. Figure 25.8 : Viewing the value of your counter filter attribute. Note
Due to a bug in version 1.0 of Jigsaw, this field display is not updated. You need to restart the server via /Admin/PropertiesEditor for the updates to be viewable.
import java.util.*; public class JigsawPost extends PostableResource { protected static int ATTR_NAME = -1 ; = "Name:";
Here you could place any number of form elements as attributes. This code is like the other extension examples you covered in the previous two sections. static { Attribute attrib = null ; Class JPostClass = null ; try { JPostClass = Class.forName("que.examples.postable.JigsawPost") ; } catch (Exception ex) { ex.printStackTrace() ; System.exit(1) ; } // register our attribute(s) attrib = new StringAttribute(NAME , "", Attribute.EDITABLE) ; ATTR_NAME = AttributeRegistery.registerAttribute(JPostClass, attrib) ; } // method to handle data from POST request public Reply handle (Request request, URLDecoder data) throws HTTPException { Here you do the work of handling the data resulting from the POST method. Now you could pick up form data, validate it, and perhaps place it in a database or send the data using a Java interface to sendmail. For this example, print out the posted data. This gives you a nice debugging resource.
// print out the variables we received // a handy object to have around when testing postable forms Enumeration en = data.keys() ; HtmlGenerator gen = new HtmlGenerator ("POST method decoded values") ; gen.append ("<p>List of variables and corresponding values:</p><ul>") ; while ( en.hasMoreElements () ) { String name = (String) en.nextElement() ; gen.append ("<li><em>" + name+"</em> = <b>" + data.getValue(name) + "</b></li>"); } gen.append ("</ul>") ; Reply reply = request.makeReply(HTTP.OK) ; reply.setStream (gen) ; return reply ; } } Adding this postable object to the server is like the examples in the previous sections. Open URL http://your-host.yourdomain/Admin/Editor/que. Select the Adding-Resources link. Enter PostTest for the name: field and que.examples.postable.JigsawPost for the class: field. Press OK to add the resource as shown in Figure 25.9. Figure 25.9 : Form adding your PostTest Java resource to the server's namespace. If you open the URL http://your-host.your-domain:9999/que/PostTest, Jigsaw returns the error message "Document not found. The document /que/PostTest is indexed but not available." To test the object, use the following URL: http://your-host.your-domain:9999/que/PostTest?name=dave; which returns the output shown in Figure 25.10. Figure 25.10: Result of accessing the PostTest resource. Normally, to use a resource like this you would use HTML code such as: <FORM METHOD="POST" ACTION="/que/PostTest">Name: <INPUT TYPE="text" NAME="name" MAXLENGTH=32><br> <INPUT TYPE="reset" VALUE="Reset"> <INPUT TYPE="submit" VALUE="Ok"> An sample form can be viewed with the URL http://your-host.your-domain:9999/que/testform.html seen in Figure 25.11. Figure 25.11: Example of the Post Test HTML form. Listing 25.3 shows the JigsawPost class.
Listing 25.3 JigsawPost.java-Class to Implement HTTP POST Method package que.examples.postable; import import import import w3c.jigsaw.forms.*; w3c.jigsaw.html.*; w3c.jigsaw.http.*; w3c.jigsaw.resources.*;
import java.util.*; public class JigsawPost extends PostableResource { protected static int ATTR_NAME = -1 ; = "Name:";
protected final static String NAME static { Attribute attrib = null ; Class JPostClass = null ; try {
JPostClass = Class.forName("que.examples.postable.JigsawPost") ; } catch (Exception ex) { ex.printStackTrace() ; System.exit(1) ; } // register our attribute(s) attrib = new StringAttribute(NAME , "", Attribute.EDITABLE) ; ATTR_NAME = AttributeRegistery.registerAttribute(JPostClass, attrib) ; } // method to handle data from POST request public Reply handle (Request request, URLDecoder data) throws HTTPException { // print out the variables we received // a handy object to have around when testing postable forms Enumeration en = data.keys() ; HtmlGenerator gen = new HtmlGenerator ("POST method decoded values") ; gen.append ("<p>List of variables and corresponding values:</p><ul>") ; while ( en.hasMoreElements () ) { String name = (String) en.nextElement() ; gen.append ("<li><em>" + name+"</em> = <b>" + data.getValue(name) + "</b></li>"); } gen.append ("</ul>") ; Reply reply = request.makeReply(HTTP.OK) ; reply.setStream (gen) ;
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch25.htm (17 of 18) [8/14/02 10:55:48 PM]
return reply ; } }
You should now know enough about Jigsaw to add exciting new content to the World Wide Web. Drop me a note and tell me about your projects. My e-mail address is dave@daves.net.
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f25-1.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f25-2.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f25-3.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f25-4.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f25-5.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f25-6.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f25-7.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f25-8.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f25-9.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f25-10.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f25-11.gif
CONTENTS
G G G G
G G
What Are Digital Signatures? Allowing More Access for Signed Applets Using a Third Party for Applet Signatures Potential Security Problems with Digital Signatures H Using Phony Signatures H Receiving Old Software H Mistaken Trust in Signed Applets H Running a Phony Web Browser Obtaining a Digital Signature Certificate Other Uses for Digital Signatures
One of the biggest hindrances to some Java developers has been the security restrictions placed on applets. Many applet developers want to be able to connect to other sites on the network or to access files on the local hard disk. You could argue that the restriction on file access is a good thing, since you may soon be writing applets for computers that have no local storage. The network restrictions, however, are another matter. The reason for the harsh restrictions on network connections is that many users sit safely behind their company's firewall. A firewall protects the company network from outside intruders who might want to steal data or tamper with the systems. Generally, a firewall allows access out to the Internet but does not allow sites on the Internet to access hosts on the other side of the firewall. A Java applet with no security restrictions thwarts the firewall because it can access all the hosts on the
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch26.htm (1 of 12) [8/14/02 10:56:06 PM]
local network. Anyone who wants to snoop around for data or damage your systems could write an applet that connects to various machines on your network, test the machines for any security holes, and then exploit those holes. This could all happen without you knowing it. The applet might be a simple scrolling-text applet sending you nice happy messages while it merrily ravages your network. This security problem comes about because you can download a Java applet and run it without even knowing it. Normally, this is an advantage. You don't want to know that you are downloading applets and you don't want to do anything special to download them. But in the case of security, this is another matter. When you download a software package, you have a certain amount of trust toward the vendor of the software. If it's a shareware program off some big archive, you might be cautious enough to run a virus checker on the program before you run it. Of course, you should do that all the time, but many people don't learn until they get bitten by a virus. You are willing, however, to let this program have complete access to your local system. For all you know, it could snoop around your network just as well as an unrestricted applet could. Many companies get upset at you for loading "unapproved" software for just this reason. You could expose your entire company's network to an outside attack. You don't usually worry about this sort of thing from a well-known company like Netscape, Sun, or Microsoft. You have some degree of trust in them, partially because you know that they would be out of business if they got a reputation for distributing malicious software. It would be nice if Java were able to establish some level of trust for each applet it loads. If you were able to verify that an applet came from Sun, you might be willing to give it a lot more access than from pHrEakR's hAvEN. Digital signatures allow you to do precisely that.
public key encryption is simple. When you want to receive data via public key encryption, you create a public key and a corresponding private key, and then publish the public key for all to see. Whenever someone wants to send you an encrypted message, they use your public key to encode the data. When you receive it, you use your private key to decode the data. The trick here is that you can't decode the data with the public key; you can only encrypt it. And it is usually computationally impossible to determine the private key from the public key. The idea behind a digital signature is that you use a special form of encryption to create a much smaller version of your data. This smaller version of the data is the signature of the data. The encrypted information in a digital signature is not a complete representation of the data-that is, you couldn't decode it and get the original information back. In other words, a digital signature is something of a one-way encoding. You can't get the original information back, but given the original information, you can verify that it was signed with a particular key. Since the signature is generated using all of the original information, if you changed even a tiny part of the original information, the digital signature would be completely different. Furthermore, you can't predict what the new digital signature will be when you change a portion of the original information. This keeps others from tampering with digitally signed information. When someone sends you digitally signed information, they must send you both the original information and the signature generated for that information. You then use their public key to verify that the signature was generated by them. Unlike normal encryption, where the intent is to hide information, a digital signature is intended to verify the origin and contents of the information. As shown in Figure 26.1, Bob's Software digitally signs an applet using Bob's private key. Figure 26.1 : Bob's Software creates a digital signature using a private key. Next, Bob's Software sends you both the applet and the applet's digital signature, as shown in Figure 26.2. Figure 26.2 : Bob's Software transmits the applet and its signature. Now, as shown in Figure 26.3, you verify the signature against the applet using Bob's well-known public key. The signature algorithm tells you whether the signature was generated by the private key corresponding to Bob's public key. Figure 26.3 : You verify the applet to see that it was really signed by Bob's Software.
Now, suppose a malicious person who has a bit of ingenuity can intercept the applet transmission from Bob's software and substitute a phony applet. In addition to the phony applet, the malicious person either forwards Bob's original signature or creates a valid digital signature for the applet, but using a different private key. Figure 26.4 shows a possible scenario for this. Figure 26.4 : A malicious person intercepts the applet from Bob's Software and substitutes a phony one. When you receive the phony applet, you check the applet and its signature against Bob's public key, as shown in Figure 26.5. You find that the applet was not sent by Bob's Software. Figure 26.5 : You check the malicious applet and its signature against Bob's public key and find that they don't match. If the malicious person had sent you the original signature generated by Bob, it would not match the applet since the applet is not the same one Bob's Software generated the signature on. If the malicious person generates a new signature for the file, it still does not match because it was not generated using Bob's private key. You can discard the applet and call Bob's Software on the phone to ask what's going on.
Digitally signed applets can be restricted to access only certain areas of the network or certain local file
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch26.htm (4 of 12) [8/14/02 10:56:06 PM]
systems. For instance, you might create a directory called /usr/local/bob and allow Bob's Software applets to access only that directory. If Bob's Software decides they want to damage your system, the worst they could do is fill up their own directory until it takes up all your hard disk space. Suppose you have a Web browser that accepts digital signatures and you download Bob's Software's new hard drive manager applet. Your browser would first verify the signature of the applet and determine that the applet indeed came from Bob's Software. Next, the browser would consult its security information to see what kind of access Bob's Software is allowed. Presumably, this information would be relayed to the Java SecurityManager running in your browser. If you download a hard drive manager, you probably have to tell the security system to allow Bob's Software full access to the hard drive. The point is, you must tell the security system what kind of access you want to allow. Note The Jeeves server accepts digital signatures on servlets, allowing you to use servlets from other sites and determine how much you are willing to trust the servlets.
Microsoft supports digital signatures for verification of all downloaded code in version 3 of the Internet Explorer. Under the Microsoft scheme, a digitally signed applet is allowed more access to the local machine. Under Sun's security framework, you can control access based on who signs the applet. Under Microsoft's framework, all digitally signed applets have the same amount of access.
TrustMe analyzes the applet and determines if it is doing anything unusual. Or it just verifies the applet, depending on the company. The point is, TrustMe is responsible for ensuring that the applet didn't do anything malicious to your system. When you download an applet from Bob's Software, you also get the signature generated by TrustMe. As shown in Figure 26.7, your browser sees that the applet is signed by TrustMe and determines the amount of access allowed, based on the restrictions you set up for any applet signed by TrustMe. Figure 26.7 : Your browser determines the security restrictions based on TrustMe, not on Bob's Software. The advantage here is that TrustMe can verify for many different companies. Rather than you having to enter a security policy for each different company, you can enter a policy for a small number of trusted verification companies.
When you download digitally signed code, you also receive a certificate containing the server's digital signature, signed by the CA. You then use this certificate to verify the signature on the code. In other words, the server is saying to you "Here is some code that I have signed, and here is a copy of my signature that has been notarized by a CA." While the term "certificate" sounds good, it is really just a digitally signed public key. Caution It is extremely important that your browser have some built-in knowledge of a CA's key. Everything that comes from the network has the potential of being fake. The browser needs some piece of information that does not come from the network before it can make any assumptions about the validity of network information. In this case, the browser needs the CA's public key so that it can verify that the CA really did sign a particular certificate.
Most certificate authorities have very stringent security procedures to keep people from sabotaging the certification process. A significant risk for a certificate authority is having its private key compromised. Once someone gets the private key for a CA, they can create false certificates. For instance, if you had the private key for a CA, you could create a certificate for Bob's Software and sign it with the CA's key, allowing you to pretend that you are Bob's Software. A good CA uses a system that hides the key from everyone, even the employees at the CA. In these cases, the CA's signature is produced by a machine that cannot reveal the private key. Some of these machines will even destroy the private key if they are tampered with. Overall, the likelihood of someone getting the private key for a CA is very remote. As technology has improved in the area of security, humans are almost always the weak link in the security chain. You may not be able to get the private key for a CA, but you can bribe someone at a CA to sign a false certificate for you. In other words, you create a certificate that says that you are Bob's Software and get someone at the CA to sign it. Most CA's have fairly stringent security procedures, making even this kind of security breach unlikely. Unfortunately, there is no way to render it completely impossible. Another potential weakness in the area of phony signatures is that someone might be able to successfully impersonate Bob's Software and obtain a signed certificate from a CA without bribing anyone at the CA. You can always forge credentials, perform spoofing over the network, and tap or reroute phone lines. Of course, this is not an aspect of digital signatures. This situation can occur in everyday business and occasionally does. If you are creating an electronic commerce system, you should be careful which CA's you accept certificates from. A certificate authority that does not take adequate precautions in verifying the identify of a person or corporation makes fraud a lot easier. You don't want to trust certificates from a CA that issues certificates to anyone that mails them a request.
The Pretty Good Privacy (PGP) encryption package uses an interesting alternative to a certification authority. Rather than using a CA, PGP users pass keys around to each other. The idea is that you generate your own private/public key pair and then give your public key to some friends, who digitally sign your key. In other words, instead of going to a central certificate authority, your friends act as CAs for you (see Figure 26.8). Figure 26.8 : You get your public key signed by people who trust you. These people are now your introducers. They vouch for you. When a stranger gives you their public key, they also give you a list of signatures from their introducers (see Figure 26.9). This is like giving an employer a list of references. Figure 26.9 : When giving your public key to a stranger, you also give them a list of introducers. If you know one of the introducers, you have some idea that you can trust the key. If you don't know any of the introducers, you can't be sure that the key you get is really valid. This scheme works pretty well for personal use, but it has some drawbacks for business use. For one thing, it is a lot easier for someone untrustworthy to sneak in and get their key verified by normally trustworthy people. After all, the introducers are usually friends of yours or people you know over the Net. It would be easy for someone to become your "friend" just to get you to introduce them. Another problem with key passing is that if someone's private key is compromised (stolen), there's no easy way to propagate that information. In other words, you may know Fred and trust people introduced by Fred. Suppose Fred's private key has been stolen and someone begins creating keys that were supposedly signed by Fred. You may not have heard from Fred in a while and you may not know that his key was stolen. You may end up trusting a malicious person who was supposedly introduced by Fred. You can also run into this problem using a CA. Because you don't verify certificates with the CA at runtime, which would be a huge performance bottleneck, you don't know immediately if a certificate is no longer valid. To address this problem, there are lists of invalid certificates called Certificate Revocation Lists (CRL). When your browser receives a certificate, it checks it against a CRL to see if the certificate has been revoked. You may wonder what happens in a few years when the number of revoked certificates has grown tremendously. Wouldn't a CRL be very large and unwieldy? A certificate does not stay on a CRL forever
because certificates also have an expiration date. As soon as a certificate expires, it is removed from the list because any browser trying to verify the certificate would reject it for being expired.
Suppose you have been dealing with Fred's Catering for a while and you have digitally signed applets from Fred's Web page. You know that Fred's is a trustworthy company. When you order from Fred's, you enter your credit card number in Fred's ordering applet. You cannot assume that the applet really came from Fred's just because you have Fred's public key. A malicious person who wants to get your credit card number could impersonate Fred's Web page. The applet you run might look exactly like the applet for Fred's (see Figure 26.11). But when you enter your credit card number, it gets shipped off to someone else's list of now-stolen credit card numbers. Figure 26.11: Someone impersonating Fred can pass you a phony applet. The really clever thieves not only impersonate Fred's. They turn around and impersonate you to Fred's and place your order (see Figure 26.12). That way, you never suspect that someone actually saw your credit card number. Figure 26.12: A clever credit card thief looks at your number and then passes the order on to the real company. There are other ways of handling this. Just keep in mind that digital signatures do not solve this problem. One way of addressing this problem is to use a secure Web protocol like SSL. This technique is discussed in Chapter 30, "Performing Secure Transactions."
confidential information, and you never suspect your browser because Surf-O-Matic is a trustworthy company. This is not really a Java problem and it's not one you can solve yourself. It must be solved by Surf-OMatic and other browser vendors. You must be able to download a browser and verify it by a digital signature to be sure that it really came from the right vendor. Otherwise, all the nice security within Java is useless. Unfortunately, this is something of a chicken and egg situation. How do you get the verification software? If you download it off the Net, how do you know it isn't also a phony? This is one of the more maddening aspects of security. The only thing that keeps this from being a huge problem is that it is not a simple task to impersonate a file server. Even then, you can usually fool only one company at a time, unless you can impersonate the file server to the whole Internet. Most companies don't have to go to this degree of worry. But if you have highly classified information on your network that people would love to get their hands on, you probably have a big headache right now. One solution to the fakery problem is to talk to your vendor about getting an encrypted version of the software. Another solution is to send someone over to the company headquarters and get a copy directly from them.
CONTENTS
G G
Choosing the Right Kind of Encryption Guarding Against Malicious Attacks H Resisting a Playback Attack H Don't Store Keys in Your Applets H Using Public Key Encryption to Exchange Session Keys H Using Secure HTTP to Thwart Impersonations Getting Encryption Software H Getting SSLava, the Secure Sockets Library H Getting the Cryptix Library H Getting the Acme Crypto Package
Data encryption is a touchy subject. Many cryptography algorithms and their implementations are restricted to use within the United States by the National Security Agency (NSA). It is illegal to export certain kinds of encryption software outside the U.S. There has recently been a push to lift some of these export restrictions, however, because of the greater need for security on the Internet. Many American companies can't sell their software overseas because of the encryption software embedded in their code. The increase in commerce over the Internet has made encryption a need for some businesses. When you type your credit card number on an order form, you want to be sure that no one can see the information as it passes through the Internet. Since you can't prevent people from snooping on your data, your only hope of safely keeping your credit card number away from prying eyes is to encrypt the data before you send it. The purpose of encryption is to turn ordinary data into completely random-looking bits. The notion of "completely random-looking" is a real science. You may think you have some clever little way of encrypting your data but unless you really know cryptography, your scheme can probably be broken easily. Unless you really know what you're doing, stick to the known encryption algorithms and you'll be pretty safe. Data is encrypted with a key that can be a random string of bits, or some word or phrase that you pick. The key is like a
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch27.htm (1 of 16) [8/14/02 10:56:12 PM]
password. It needs the same degree of secrecy and the same care in creating a word or phrase that can't be guessed. The key is used by the encryption algorithm to scramble your data and to unscramble it on the other side. A data encryption algorithm is called a cipher. The data you are encrypting is called plaintext, whereas the encrypted version of the data is called ciphertext. The process of converting ciphertext back to plaintext is called decryption. Figure 27.1 illustrates a simple use of encryption to pass a coded message. Figure 27.1 : Encryption can be used to pass coded messages. Data encryption ciphers are grouped into two categories: block ciphers and stream ciphers. The stream cipher is a simple single-character-in, single-character-out cipher. That is, it does the encryption one character at a time. Each time a stream cipher reads a character, it uses the key and accumulated data from the other characters it has processed to figure out how to scramble the next byte of data. Unlike some of the simple ciphers you may be familiar with, a good stream cipher does not just map one character to another. If you feed two A's in a row to a stream cipher, chances are you will not get two identical characters in a row in the encrypted text. Figure 27.2 illustrates a stream cipher in action. Figure 27.2 : A stream cipher encodes a single character at a time. A block cipher, on the other hand, encrypts whole blocks of data at a time. Unlike a stream cipher, the block cipher can scramble all the bits in a block so that the bits for the first byte of the block can be scrambled and placed in strange places. Of course, the key and the actual values of the bits determine what the encoded block looks like. The first bit in a block may end up in one position using a certain key and in a different position using a different key. Figure 27.3 illustrates a block cipher in action. Figure 27.3 : A block cipher scrambles whole blocks of data at one time. There is another way to classify encryption algorithms, based on the kind of key used. Some algorithms use a private key, also called a symmetric key, whereas others use a public/private key pair, called an asymmetric pair. Private key encryption is probably the one you are most familiar with. Two parties agree on a secret key. The sender encrypts the data with the secret key, and the receiver decrypts the data with the same key. If anyone else finds out the secret key, he or she can spy on the data being exchanged. Figure 27.4 illustrates a data exchange using a private key. Figure 27.4 : Both parties agree on a private key and use that key for encryption and decryption. One of the problems with private keys is that you have to find some way of agreeing on the key ahead of time. How do two people exchange encrypted communications if they have no way to exchange keys to begin with? Public key
encryption provides a neat solution to this problem. With public key encryption, everyone who wants to get encrypted data creates a private decryption key and a public encryption key. This is called an asymmetric key cipher because the encryption key and the decryption key are different. The important part of this scheme is that although you can determine the public key based on the private key, you cannot figure out the private key from the public key. Anyone wanting to send you encrypted data would look up your public key, which can be published in a number of ways, and use it to encrypt a message to you. You would receive this message and decrypt it with your private key. Figure 27.5 illustrates a data exchange using a public key. Figure 27.5 : The data is encrypted with the public key and decrypted with the private key.
The amount of security needed Some encryption algorithms can be broken in a matter of hours; some would take many years. Others would take several times the anticipated lifetime of the universe to break given machines many times more powerful than the ones in use today. Of course, the price you pay for more security is the encryption time, among other things. If the data will be useless in an hour, you don't need an algorithm to protect it for your lifetime. The speed of the algorithm Some algorithms are prohibitively slow for common use. If you need a Cray mainframe to encrypt and decrypt the data in a reasonable time, it probably is not a good choice for an applet. Licensing fees The number of patents for encryption algorithms is amazing compared to the rest of the computing field. Many algorithms, though publicly available, are still patented and subject to licensing fees for commercial use. Availability for Java At the outset, Java and encryption algorithms didn't get along too well. This was because Java is a byte-codeinterpreted language, and encryption algorithms need a lot of computations. As Just-in-Time (JIT) compilers have emerged, Java has gotten better at meeting the high demands of these algorithms. Native versus 100 percent Java implementations Several vendors have taken a shortcut to encryption for Java by using some of the more compute-intensive parts of the algorithms as native methods. When you start relying on native methods, you lose the cross-platform advantages of Java. Export restrictions The U.S. has stringent restrictions on the export of encryption software. If your applet uses a restricted algorithm, you could be violating U.S. law if your applet is run by someone outside the U.S. Of course, if you're already outside the U.S., you don't have this problem. Sometimes an entire algorithm is not restricted, only the use of keys above a certain size.
The sequence for this is simple: 1. The sender generates a random session key and encrypts it with the receiver's public key. 2. The receiver decrypts the session key using the private key and the two are ready to talk. Unfortunately, you can't just store the public key in your applet, either. If you do, you open yourself up to an impersonation, or "man-in-the-middle" attack. Java is particularly vulnerable to this kind of attack because the code itself is downloaded over the network. An impersonator impersonates the receiver to the sender and the sender to the receiver, as shown in Figure 27.9. Figure 27.9 : An impersonator sits between two communicating parties and impersonates them both. Normally, this kind of attack doesn't work with public key encryption unless you can somehow make the sender think the public key is something other than it is. In Java, this turns out to be a simpler task, although digital signatures throw an extra wrench into the works. If an impersonator wants to set up shop, it first impersonates the receiver. When the server downloads an applet from the receiver, it really downloads the applet from the impersonator. As shown in Figure 27.10, the impersonator has cleverly substituted its own public key in place of the receiver's public key in the applet. Figure 27.10: The impersonator gives a phony applet to the sender. When the sender generates a random session key, it encrypts it with what is now the impersonator's public key and sends the encrypted session key to the impersonator. The impersonator, wanting to spy but not be found out, impersonates the sender by generating a random session key, encrypting it with the receiver's public key, and sending it to the receiver. As shown in Figure 27.11, the sender now thinks it is talking to the receiver, while the receiver thinks it is talking to the sender. The impersonator, however, is in the middle, watching all the data go by. Figure 27.11: The impersonator is now playing the part of the sender and the receiver. Anytime the sender sends information to the receiver, the impersonator intercepts it and then passes it on to the real receiver-but not before saving any interesting parts for later use. As discussed in Chapter 26, "Securing Applets with Digital Signatures," digital signatures can put a damper on this kind of activity but they cannot prevent all attacks. In particular, if the impersonator is able to sign the phony applet with some other valid signature, the sender thinks the applet is okay.
Secure HTTP pages are not subject to impersonation because the browser uses a certification authority to verify the Web page, much the same way that the digital signature mechanism verifies an applet. The nice thing is that not only is the Web page downloaded securely, everything on the page is downloaded with the same secure mechanism, including applets. Thus, you can assure your customers that if they connect to your Web page using Secure HTTP, you can guarantee the safety of any information they give to your applets. That is, assuming you are wise enough to encrypt the data before you send it to your server!
If you use SSLava within an applet, make sure that the applet itself is downloaded with a secure protocol. Otherwise, an impostor could replace the real SSLava classes with phony ones and breach your security.
The SSL protocol does solve a number of encryption headaches and it will probably be more widely available in the future. The SSLava product gives you a good way to get your feet wet with SSL. Note The SSL protocol is a part of Java 1.1, giving you encryption capabilities on any Java 1.1 platform. While you are waiting for Java 1.1 to be available everywhere, you can use the SSLava libraries.
import java.io.*; // // // // // This class wraps an input stream filter around the decrypt method from the Cryptix java.crypt.BlockCipher class. It allows you to decrypt an input stream. You can use it to read in an encrypted file as if it were unencrypted.
public class BlockCipherInputStream extends FilterInputStream { // cipher is the cipher we use to decrypt protected java.crypt.BlockCipher cipher; // currentBlock is the most recent block of data we decrypted. The // block cipher must work on blocks of data. We build up blocks from // the input, decrypt them, and let them out byte by byte byte[] currentBlock; // currentChar is the position in currentBlock of the next // character we return in the read method. int currentChar; // Create a cipher input stream on InputStream in, using the BlockCipher // cipher. public BlockCipherInputStream(InputStream in, java.crypt.BlockCipher cipher) { super(in); // save the cipher this.cipher = cipher; // Create a block that is the cipher's block size currentBlock = new byte[cipher.blockLength()]; // Flag that we have no characters in the block right now currentChar = -1; } // read gets a character from the block we've decrypted, // If we've exhausted the current block, go read another block. public int read() throws IOException { // If we've used up all the chars in the block, get another block if (currentChar < 0) {
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch27.htm (9 of 16) [8/14/02 10:56:12 PM]
readNextBlock(); } // If we got another block and we're still out of chars, there's // no chars left to get. if (currentChar < 0) return -1; // Fetch the next char in the block int ch = currentBlock[currentChar++]; // If that was the last char in the block, set currentChar to -1 // to show we need to read a new block next time. if (currentChar >= currentBlock.length) { currentChar = -1; } return ch; } // readNextBlock reads in another block from input, decrypts it, and // resets the index for the current character. protected void readNextBlock() throws IOException { // Read the next character from input. for (int i=0; i < currentBlock.length; i++) { int ch = in.read(); // If we hit EOF and we only have a partial block, flag it as an error. if (ch < 0) { // If we hit EOF and the current block is empty, we don't need to // decrypt. Just mark the current character as end-of-block and return. if (i == 0) { currentChar = -1; return; } // If we get here, it means that we hit EOF in the middle of a block. // This shouldn't happen because we only encrypt whole blocks. throw new IOException("Incomplete block."); } currentBlock[i] = (byte)ch;
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch27.htm (10 of 16) [8/14/02 10:56:12 PM]
} // Reset the index of the current character currentChar = 0; // Decrypt the current block cipher.decrypt(currentBlock); return; } }
All the BlockCipherInputStream does is read whole blocks of encrypted data, decrypt them, and then dole the unencrypted data out one byte at a time through the read method. This class is quite handy because you can now use encryption on any data stream, including a network socket. The companion to this class is the BlockCipherOutputStream, which takes one byte at a time, stores the byte in a block, and encrypts each block as it fills up. It then writes out the block onto the output stream it is filtering. Listing 27.2 shows the BlockCipherOutputStream.
Listing 27.2 Source Code for BlockCipherOutputStream import java.io.*; // This class implements an output stream filter that encrypts // data using an BlockCipher from the Cryptix java.crypt libraries. public class BlockCipherOutputStream extends FilterOutputStream { // The actual cipher we're using protected java.crypt.BlockCipher cipher; // The current block of data we're writing byte[] currentBlock; // The index of the current character we're writing. int currentChar; // Create an output stream filte rfor a particular cipher public BlockCipherOutputStream(OutputStream out, java.crypt.BlockCipher cipher) { super(out);
this.cipher = cipher; currentBlock = new byte[cipher.blockLength()]; currentChar = 0; } // write adds a character to the output block, and when the buffer // is full, it encrypts the block and sends it up the filter public void write(int ch) throws IOException { // Add the character to the block currentBlock[currentChar++] = (byte) ch; // If we've filled the block, encrypt the block and write it out if (currentChar >= currentBlock.length) { cipher.encrypt(currentBlock); out.write(currentBlock); currentChar = 0; } } // Flush fills out the remainder of the current block with 0's, then // encrypts the block and writes it out. public void flush() throws IOException { // If there's a partial block, fill it out with 0's if (currentChar > 0) { while (currentChar < currentBlock.length) { currentBlock[currentChar++] = 0; } // Encrypt the block and write it out cipher.encrypt(currentBlock); out.write(currentBlock, 0, currentBlock.length); currentChar = 0; } // Do whatever else we have to do to flush the stream super.flush(); }
public void close() throws IOException { // Before closing the stream, flush it. flush(); super.close(); } }
These classes are fairly small. All they really do is arrange data into a form more suitable for the encryption and decryption routines, which do the hard work. Listing 27.3 shows a program that tests out both the encryption and decryption streams using a pipe. It uses the IDEA cipher for encryption, which is a patented cipher and must be licensed for commercial use. It can be used noncommercially for free. Tip The technique used in the IdeaCrypt program of testing input and output stream filters by using a pipe is a handy technique. You can also use it to test out networking protocols if you are writing both sides of the protocol. The pipe takes only two lines of code to create, compared to the amount of time it takes to set up socket connections.
Listing 27.3 Source Code for IdeaCrypt.java import java.io.*; // // This program encrypts a block of data using the IDEA // cipher, then unencrypts it. public class IdeaCrypt { public static void main(String[] args) { // encodeMe is the string we want to encode String encodeMe = "This is a string to be encoded!!!"; // The IDEA cipher implemented in the Cryptix library uses a 16-byte // key - that's 128 bits!
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch27.htm (13 of 16) [8/14/02 10:56:12 PM]
String key = "<Sixteen>ByteKey"; try { // The Cryptix library likes the keys and the data to be in byte // arrays, not strings. We allocate an array for the key and // copy the key into the array. byte[] keyBytes = new byte[key.length()]; // Copy the key string to a byte array. key.getBytes(0, key.length(), keyBytes, 0); // Create an IDEA cipher for our key. java.crypt.IDEA cipher = new java.crypt.IDEA(keyBytes); // In order to demonstrate the encryption and decryption filters, we // just set up a pipe, so we can write to the pipe and read from it, // testing the encryption and decryption in one simple program. PipedInputStream pipeIn = new PipedInputStream(); PipedOutputStream pipeOut = new PipedOutputStream(pipeIn); // Create a decryption filter in the input side of the pipe. It will // decrypt the data we wrote to the pipe. BlockCipherInputStream decrypter = new BlockCipherInputStream(pipeIn, cipher); // Create an encryption filter on the output side of the pipe. This will // encrypt the data we write to the pipe. BlockCipherOutputStream encrypter = new BlockCipherOutputStream(pipeOut, cipher); // Now create a print stream on top of the encryption stream so we can // write stuff to the pipe using println. PrintStream encryptPrint = new PrintStream(encrypter); // Write out the string to be encrypted encryptPrint.println(encodeMe); System.out.println("Wrote encrypted string: "+encodeMe); // Go ahead and flush the print stream and close it. encryptPrint.flush(); encryptPrint.close(); // Create a DataInputStream on the decryption stream. This allows us to // use readLine to read the data back in. DataInputStream decryptIn = new DataInputStream(decrypter);
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch27.htm (14 of 16) [8/14/02 10:56:12 PM]
// Read the unencrypted string String unencrypted = decryptIn.readLine(); System.out.println("Read unencrypted string: "+unencrypted); } catch (Exception e) { e.printStackTrace(); } } }
The Crypto package includes several block ciphers, such as DES, DES3, and IDEA. The data encryption standard (DES) algorithm has been around for a number of years and is used heavily around the U.S. Because of the relatively small key length of DES, information is sometimes encrypted three times with DES, using two different keys. This is called DES3. The IDEA cipher is a relatively new block encryption scheme that is still undergoing some analysis of its security. The IDEA cipher is patented, and you have to get a license if you want to use it commercially. The owners of the patent, Ascom Systec AG, are based in Switzerland and can be reached over the Net at idea@ascom.ch.
In addition to block ciphers, the Crypto package has two stream ciphers. One cipher, the rot13, is a simple substitution cipher that is completely insecure. Rot13 is an alphabetic shift in which A is replaced with M, B is replaced with N, and so on. It is popular on the UseNet for hiding "spoilers" to games and movies, or for hiding potentially offensive material. Most newsreaders can decrypt rot13 so you can view the hidden information if you choose to. Don't even think of using rot13 as a security tool. It is mainly used for testing. The RC4 stream cipher is a secure cipher that is both restricted for export and patented. The patent is owned by RSA Data Security Inc., who originally kept the design of RC4 a secret. Unfortunately for RSA, someone anonymously posted the algorithm to the UseNet in 1994, exposing it to the world. RSA still owns the patent on the algorithm, however, so if you plan to use it in a commercial product, you should contact RSA for licensing information. The Crypto package also includes EncryptedInputStream and EncryptedOutputStream filters. You can filter any input or output stream with these filters, using any stream or block cipher in the Crypto package. You can also create new ciphers by subclassing the BlockCipher and StreamCipher classes. The Crypto package is used in a remote system-access application in Chapter 28, "Accessing Remote Systems Securely."
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f27-1.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f27-2.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f27-3.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f27-4.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f27-5.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f27-6.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f27-7.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f27-8.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f27-9.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f27-10.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f27-11.gif
CONTENTS
G G G G
G G G
Getting a Secure Web Server Preventing Impersonations Accessing Remote Data Passing Keys to Clients H Don't Reuse Symmetric Keys H Using Public Key Encryption to Get a Private Key H Passing a Private Key as an Applet Parameter Implementing a Single-Client Secure Server Implementing a Multiclient Secure Server Creating Other Secure Remote Access Programs
The huge explosion of Internet access is both a blessing and a curse to businesses on the Net. Because Internet data passes over insecure networks, a company must be extremely careful when passing sensitive data over the Internet. In the past, it was difficult for companies to communicate with their employees out in the field, because networking technology was fairly primitive, and laptop computers were not very portable, or too powerful. This was especially a problem for sales force automation. A salesman may be on the road for weeks or months at a time, unable to communicate with the home base. Figure 28.1 illustrates the old design of a sales force automation network. Figure 28.1 : In the past, companies had to create their own sales force automation networks. Now, laptop computers are extremely powerful with a wide range of networking options, and Internet access is available all over the world. Figure 28.2 shows how the Internet has changed the environment for sales force automation. Figure 28.2 : The widespread availability of the Internet eliminates the need for custom networks. It seems like things should be easier for people on the road, and it is for people at some companies. For others, things haven't gotten any better. The irony here is that it is still just as difficult to access secure data over the Net. The widespread infiltration of the Internet, which makes it easier for employees to communicate back to their home system, also increases the likelihood that the data can be intercepted. A devious spy from another company could snoop on private data transmissions over the Internet. In addition, many companies have custom software that must be adapted to work over the Net. Whenever the software is updated, it is very difficult to distribute the new versions to the people in the field. The company must either ship new diskettes, ship out a new laptop, wait until the salesperson is back in the office, or download software over the network. The latter is a very difficult endeavor, most often meeting with failure.
Java can help solve the distribution problem. With Java, you can either write the custom software as applets, which can be cached on the local hard drive (as shown in Figure 28.3), or create a software distribution applet that downloads new versions of the local applications, as shown in Figure 28.4. A digitally signed applet can download updates to the local software, as well as update any information stored on the local hard disk. Figure 28.3 : Because applets are downloaded at runtime, you can eliminate some installation headaches. Figure 28.4 : A custom software installation applet can also assist in software installation. You still have the problem of downloading important information without someone intercepting the information. The Secure Socket Layer (SSL) protocol enables you to access Web pages securely. SSL uses a combination of a digitally signed certificate and an encrypted network session.
Once you get a secure Web server, you must also get a digitally signed certificate in order to use SSL. There are several certificate authorities around, one of which is Verisign (http://www.verisign.com). One of the nice things about the Apache-SSL server is that it will generate a local certificate that you can use for testing. You must still obtain a signed certificate if you want someone else to access your Web server securely.
Preventing Impersonations
The Secure Socket Layer, or another secure form of HTTP, is vital in downloading applets securely. When you download one of your company's applets, you must to be sure that it really came from your company. The digital signature mechanism warns you if you download an unsigned applet, which prevents all but the most devious impersonation attacks. If someone is able to digitally sign an applet with a signature you think is valid, you will not know anything is wrong. Of course, if this happens, it means that you have trusted someone you shouldn't have. Chapter 26, "Securing Applets with Digital Signatures," contains more information about digital signatures and their relationship to applets. If you use SSL to download your applet, it cannot be impersonated, because SSL ensures that you are talking to the correct Web server.
You can use SSL to send and receive encrypted data without using any encryption software yourself. The URL class enables you to open up URLs with an https protocol type, which uses SSL for communications. Because of the way the applet security manager works, you cannot intermix http and https URL accesses in a single applet. If your applet was loaded using the http protocol type, you can open URLs only with a protocol type of http. If your applet was downloaded via https, you can open URLs only with a protocol type of https. What the restriction means is that if you need to download information securely, you must also download the applet securely. It is one of the simple ways that Java protects you from yourself. If you were allowed to download applets insecurely and then download information securely, you would be vulnerable to an impersonation attack. Since the SSL support is built into the URL class (actually, into the browser itself), you can use the methods discussed in Chapter 6 "Communicating with a Web Server," to store and retrieve files using the URL class. You cannot use any of the socket mechanisms to do this, however.
Because you are using secure sockets to download your applet, you can take advantage of the fact that the applet and its parent Web page are transmitted in encrypted form. Instead of the client generating the session key and using public key encryption to pass the session key to the server, the server can pass the key to the client applet using the <PARAM> tag. This mechanism has some peculiar drawbacks to it, stemming from the fact that you must use CGI (or its equivalent) to pass the key information back. If you are lucky enough to have a Java Web server that also does SSL, you don't have to worry about this. Unfortunately, since many, if not all, of the current Java Web servers do not support SSL, you have to come up with some unique ways of passing keys around. Note Because Java 1.1 includes the SSL protocol, all Java Web servers should soon support SSL.
The problem here is that a CGI program is supposed to generate content in response to a GET or a POST and then exit. Figure 28.7 illustrates the typical life of a CGI program. Figure 28.7 : In response to an http GET or POST, a CGI program starts up, generates a response, and exits. Unfortunately, since the response isn't sent back to the client until the CGI program terminates, by the time the client receives the random private session key, the program that generated the key is gone. There are a number of ways you can solve this problem. One way is to have your CGI program start up another program that also knows the session key. Once the client knows the session key, it connects to the program started by the CGI program. Figure 28.8 illustrates this sequence. Figure 28.8 : A CGI program can spawn another program that communicates securely with the client. This solution may be fine for some situations, but it has a number of drawbacks:
G
G G
If the spawned program takes a long time to start up, you may bog down your system if you are starting up too many copies of the program at one time. Plus, the client may not be patient enough to wait for the server program to start. The client has to be smart enough to retry if it can't get a connection the first time. If the server program needs to use a limited resource, you don't want many simultaneous copies of the program running. For instance, if it accesses a database, you don't want 50 copies of the program all opening database connections. It's even worse if the server needs a resource that can be accessed by only one program at a time. Because the client has to connect to the server, the server needs some kind of timer in case the client fails for some reason. You don't want hundreds of old copies just sitting around waiting for a client that will never call. This isn't a big deal, just something you have to take care of. It may be expensive on your system to have many copies of the Java virtual machine running at the same time. It is tricky for the server program to create a socket and pass that socket number back to the CGI program so it can tell the client where to connect.
Another solution is a bit less taxing on the system resources, but is harder to implement. In this solution, the server is a program that is already running, in the same way that the Web server is always running. When a CGI program starts up, it requests a new session key from the server and then passes the key to the client. The server then listens for an incoming connection. Figure 28.9 illustrates this relationship. Figure 28.9 : The CGI program gets a key from the server, which was already running. This architecture has some advantages over the previous one:
G G G
With only one copy of the server running, limited resources are not consumed so quickly. If the server has a long startup time, you take that hit only once, and probably not while there is a client waiting, because the server is probably started at system boot time. You need only one copy of the Java VM to run the server. This solution also has its drawbacks: The server has to handle multiple simultaneous requests. This is usually harder to implement than a single-threaded, singleclient server. The CGI program has to communicate with the server somehow, either through RMI, CORBA, or a simple socket connection. This takes some startup time. You still have the problem of setting up a timer to decide when a client has had a problem and won't be calling.
Listing 28.1 Source Code for SingleSecureServer.java import java.io.*; import java.net.*; // This class implements a single-client secure server. It is implemented // as an abstract class, leaving your specific server to fill in // the handleNewClient and makeSessionKey methods. public abstract class SingleSecureServer extends Object implements TimerCallback, Runnable { protected int clientTimeoutPeriod = 300000; // 5 minute timeout protected byte[] sessionKey; protected int tickCount; protected PrintStream responseStream; protected ServerSocket serverSock; protected Thread thread; public SingleSecureServer(OutputStream responseStream) { try { this.responseStream = new PrintStream( responseStream); } catch (Exception e) { }
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch28.htm (5 of 19) [8/14/02 10:56:27 PM]
} // Called to create the listen socket. Override this if you want a // specific port number public ServerSocket createSocket() throws IOException { return new ServerSocket(0); } // waitForClient waits for a client to connect to the server. It also // sets up a timer that goes off after a certain amount of time, this // allows us to quit if the client never connects. public void waitForClient() { // Start the timer Timer timer = new Timer(this, clientTimeoutPeriod); tickCount = 0; timer.start(); while (true) { try { // Accept a new client Socket sock = serverSock.accept(); serverSock.close(); // Turn off the timer now timer.stop(); // Do whatever has to be done for the new client handleNewClient(sock); return; } catch (Exception e) { } } } // // // // This class interfaces with the CGI program in a kludgy way - it writes information to the output stream. The CGI program then gets an input stream to our output stream and reads the information. This method writes out the port number public void sendPortNumber(int port) { responseStream.println(port); } // This method writes out the session key for the CGI program to read public void sendSessionKey() { responseStream.println(keyString(sessionKey)); }
// This function is provided in the Integer class in JDK 1.0.2 // My poor Linux version is only 1.0.1, so I had to hack one up. public static String toHexString(int i) { char hexBytes[] = new char[2]; hexBytes[0] = "0123456789abcdef".charAt((i >> 4)&0xf); hexBytes[1] = "0123456789abcdef".charAt(i&0xf); return new String(hexBytes); } // This method converts a binary session key into a string of hex digits public static String keyString(byte[] key) { String returnVal = ""; for (int i=0; i < key.length; i++) { returnVal += toHexString(key[i]&0xff); } return returnVal; } // // // // tick is called by the timer when it goes off. The timer is built to fire immediately, and then wait for a specific interval before going off again. We use the tick count to figure out if this is the immediate time, or if the time has elapsed.
public void tick() { // if tickCount is 1 after we increment it, this is just the first tick // so don't do anything if (++tickCount == 1) return; // otherwise, assume the client isn't connecting stop(); } // start does everything we need - it creates the socket, writes out // the port number, creates the session key, writes it out, and then // waits for an incoming client public void run() { serverSock = null; // Create the socket we're going to listen on try { serverSock = createSocket(); } catch (Exception e) { return; } // tell the CGI program what the port number is
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch28.htm (7 of 19) [8/14/02 10:56:27 PM]
sendPortNumber(serverSock.getLocalPort()); // create the session key makeSessionKey(); // tell the CGI program what the session key is sendSessionKey(); // make sure the CGI program gets all the information responseStream.flush(); // wait for a client to connect waitForClient(); } public void start() { thread = new Thread(this); thread.start(); } public void stop() { try { serverSock.close(); } catch (Exception e) { } thread.stop(); thread = null; } // handleNewClient does something with the incoming socket, it's // up to you to decide what public abstract void handleNewClient(Socket sock); // makeSessionKey generates a session key. public abstract void makeSessionKey(); }
All your CGI program needs to do is start the server program, read two lines, and generate an HTML page that tells the requesting browser where to get the applet. While you could write the CGI program in any language you choose, Java seems like the ideal choice for this book. Listing 28.2 shows a CGI program that starts up a secure telnet server. This server uses an excellent Java Telnet applet written by Bret Dahlgren. The source code to the applet can be found on the World Wide Web at http://w3.gwis. com/~thorn/telnet/. In order to support secure telnet sessions, the Telnet application had to be modified slightly. It now supports a sessionKey parameter which, if present, tells it to use encryption for the session. The encryption used is DES3, provided by the Acme cryptography library.
Listing 28.2 Source Code for SecureLoginStartup.java import java.net.*; import java.io.*;
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch28.htm (8 of 19) [8/14/02 10:56:27 PM]
// // // // // //
This is a CGI program that starts up a secure Telnet session using a subclass of the SingleSecureServer class. It generates an HTML response that refers to a Telnet applet and passes the port number and session key to the telnet applet. It reads the port number and session key from the SingleSecureServer (actually the SecureLoginServer).
public class SecureLoginStartup extends Object { // Send an error response to the web browser - something went wrong public static void sendErrorResponse(String error) { System.out.println("Content-type: text/html"); // Gen the HTML on-the-fly. We put it in a string so we can // compute the content length and be real polite String response = "<HTML><HEAD>\n"; response += "<TITLE> Secure Login Error </TITLE>\n"; response += "<BODY>\n"; response += "<H1>Error establishing login.</H1>\n"; response += "<P>"+error+"\n"; response += "</BODY></HTML>"; System.out.println("Content-length: "+response.length()); System.out.println(); System.out.println(response); } // sendNormalResponse sends a web page that loads up the telnet // applet for a specific port and session key public static void sendNormalResponse(int port, String key) { System.out.println("Content-type: text/html"); // Gen the HTML on-the-fly. We put it in a string so we can // compute the content length and be real polite String response = "<HTML><HEAD>\n"; response += "<TITLE> Secure Login Session </TITLE>\n"; response += "<BODY>\n"; response += "<H1>Secure Login</H1>\n"; response += "<APPLET codebase=\"/classes\" "; response += "code=\"Telnet.class\" "; response += "width=600 height=400>\n"; response += "<PARAM name=\"fields\" value=\"off\">\n"; response += "<PARAM name=\"host\" value=\""; try { response += InetAddress.getLocalHost().getHostName(); } catch (Exception e) { response += "localhost"; } response += "\">\n";
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch28.htm (9 of 19) [8/14/02 10:56:27 PM]
+= += += += +=
"<PARAM name=\"port\" value=\""+port+"\">\n"; "<PARAM name=\"sessionKey\" value=\""+key+"\">\n"; "You need Java for secure logins.\n"; "</APPLET>\n"; "</BODY></HTML>";
System.out.println("Content-length: "+response.length()); System.out.println(); System.out.println(response); } public static void main(String[] args) { try { // Start up the secure server program. You'll probably have to change // this for your system. Process externProcess = Runtime.getRuntime().exec( "/usr/local/java/bin/java SecureLoginServer"); // create an input stream for reading the parameters back from the server DataInputStream in = new DataInputStream( externProcess.getInputStream()); // Read the port String portLine = in.readLine(); int port = Integer.parseInt(portLine); // Read the session key String sessionKey = in.readLine(); // Send the web page to the browser sendNormalResponse(port, sessionKey); } catch (Exception e) { sendErrorResponse(e.toString()); return; } } }
Listing 28.3 shows the HTML information generated by this CGI program:
Listing 28.3 Output from SecureLoginStartup Content-type: text/html Content-length: 378 <HTML><HEAD> <TITLE> Secure Login Session </TITLE> <BODY> <H1>Secure Login</H1>
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch28.htm (10 of 19) [8/14/02 10:56:27 PM]
<APPLET codebase="/classes" code="Telnet.class" width=600 height=400> <PARAM name="fields" value="off"> <PARAM name="host" value="flamingo"> <PARAM name="port" value="1126"> <PARAM name="sessionKey" value="8b4243347b3a69b8aa12594153c15c8c"> You need Java for secure logins. </APPLET> </BODY></HTML> The CGI program is started by a small shell script that sets the Java CLASSPATH variable before running: #!/bin/sh export CLASSPATH=/usr/local/etc/httpd/htdocs/classes:/usr/local/java/lib/classes.zip /usr/local/java/bin/java SecureLoginStartup
The startup sequence for the secure Telnet applet is as follows: 1. The Web browser opens up the URL for the CGI program (actually, the startup script for the CGI program). 2. The CGI program creates an instance of a SingleSecureServer and initializes the server. 3. The server opens up a ServerSocket to listen for incoming connections and creates a random session key. It returns both the port number and the session key to the CGI program via System.out. 4. The CGI program generates an HTML page containing an <APPLET> tag for the Telnet applet and all the important parameters, including the session key. 5. The Telnet applet starts up, connects to the server, and, using the session key for encryption, engages in an encrypted telnet session. The class that actually creates the telnet connection and passes the information back and forth to the encryption routines is shown in Listing 28.4. It is fairly short. Essentially, it creates the telnet connection, and then uses a simple bridging class to link two streams together.
Listing 28.4 Source Code for SecureLoginClient.java import java.io.*; import java.net.*; import Acme.Crypto.*; // This class sets up an encrypted telnet session. It uses the StreamBridge // class to transfer data between the telnet streams and the encrypted // streams. public class SecureLoginClient extends Object implements BridgeCloseCallback { Socket socket; StreamBridge bridge1; StreamBridge bridge2; byte[] sessionKey; public SecureLoginClient(Socket socket, byte[] sessionKey) { this.socket = socket; this.sessionKey = sessionKey;
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch28.htm (11 of 19) [8/14/02 10:56:27 PM]
start(); } public void start() { try { // Connect to the telnet port on the local host Socket telnetSock = new Socket( InetAddress.getLocalHost(), 23); // // // // // // It is vital that you create the encryption streams in the reverse order from the other end. In other words, if you create the encrypted output stream first here, you must create the encrypted input stream first at the other end. This is because the block cipher streams require some initial data over the stream. If you create the input streams first, both sides will be waiting for input.
// Connect the output from the telnet stream to the encrypted output // stream bridge2 = new StreamBridge( telnetSock.getInputStream(), new EncryptedOutputStream( new Des3Cipher(sessionKey), socket.getOutputStream()), this, true); // Connect the output from the encrypted stream to the telnet stream bridge1 = new StreamBridge( new EncryptedInputStream( new Des3Cipher(sessionKey), socket.getInputStream()), telnetSock.getOutputStream(), this, false); bridge1.start(); bridge2.start(); } catch (Exception e) { try { socket.close(); } catch (Exception ignore) { } return; } } // bridgeClosed is called by the StreamBridge class whenever a // stream closes. We just close off the socket to make sure // everything will shut down properly public synchronized void bridgeClosed() { try { socket.close(); } catch (Exception e) {
} } }
The SecureLoginServer class, which is a subclass of SingleSecureServer, does little more than create a session key and create the SecureLoginClient to handle the client connection. Most subclasses of SingleSecureServer will probably be this simple. Listing 28.5 shows the SecureLoginServer class. Note When you generate a random session key, make sure you use a cryptographically secure random number generator. Most of the random number generators you find in programming languages are not good for cryptography because they are too predictable. For instance, if your random number generator has a period of 2^32, it repeats its pattern after 2^32 (about 4 billion) numbers. This may seem like a lot to you, but in terms of breaking codes, it's very small. Even if you generate 128-bit keys, there are only 2^32 possible 128-bit patterns that the random number generator could create, meaning your key is logically only 32 bits. In other words, if someone wanted to try every possible key, he or she would have to try only 2^32 combinations instead of the 2^128 combinations you would expect with a 128-bit key. In addition, most random number generators have some predictability, where a truly secure generator has the appearance of being completely random.
Listing 28.5 Source Code for SecureLoginServer.java import java.net.*; import java.io.*; // This class is responsible for creating a random session // key and for creating the class to handle a new client. public class SecureLoginServer extends SingleSecureServer { public SecureLoginServer(OutputStream out) { super(out); } // Create a SecureLoginClient to handle the client connection public void handleNewClient(Socket sock) { SecureLoginClient client = new SecureLoginClient(sock, sessionKey); } // Generate a random session key public void makeSessionKey() {
sessionKey = new byte[16]; Acme.Crypto.CryptoUtils.randomBlock(sessionKey); } // Start the server with the responses going to System.out, these // will be picked up by the CGI program. public static void main(String[] args) { SecureLoginServer server = new SecureLoginServer( System.out); server.start(); } }
Finally, the bridging mechanism to link two streams together is very simple. The StreamBridge class, shown in Listing 28.6, sets up a thread and constantly reads data from one stream and writes it to another. It can operate in either block mode or single character mode. The single character mode is necessary for the encryption streams, because if you do a block mode read on the encryption stream, it won't return until it fills the entire block. You don't necessarily want it that way. Tip The StreamBridge class is like a connector for streams. It connects the input of one stream to the output of another stream, and is a little like a pipe in that it results in two streams being connected together, but is unlike a pipe in that you don't have to do any work to send the data over the connected streams.
Listing 28.6 Source Code for StreamBridge.java import java.io.*; // This is a generic class for connecting one stream to another. // It has a callback to notify you when a stream closes, and // will operate in either single-character or block-read mode public class StreamBridge extends Object implements Runnable { InputStream in; OutputStream out; BridgeCloseCallback callback; Thread bridgeThread; boolean blockRead; public StreamBridge(InputStream in, OutputStream out, BridgeCloseCallback callback, boolean blockRead) { this.in = in; this.out = out;
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch28.htm (14 of 19) [8/14/02 10:56:27 PM]
this.callback = callback; this.blockRead = blockRead; } public void run() { int ch; try { // If we support block read, create a block and read as much as // we can into it each time if (blockRead) { byte[] block = new byte[1024]; int len = 0; // Keep reading blocks. We flush the output just in case. while ((len = in.read(block)) > 0) { out.write(block, 0, len); out.flush(); } } else { // If we aren't in block-read mode, read a character, write a character // and flush the stream after writing each char. while ((ch = in.read()) >= 0) { out.write((char)ch); out.flush(); } } } catch (Exception error) { } callback.bridgeClosed(); stop(); } public void start() { bridgeThread = new Thread(this); bridgeThread.start(); } public void stop() { bridgeThread.stop(); bridgeThread = null; } }
You can use this same framework to implement other secure protocols. For example, you could use the POP3 and SMTP classes
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch28.htm (15 of 19) [8/14/02 10:56:27 PM]
from Chapter 11, "Sending E-Mail from an Applet," and create a secure mail system. This framework is essentially the "Poor Man's SSL." It uses SSL to get the initial setup information back and forth, and then uses other encryption for the rest of the session.
Listing 28.7 Source Code for MultiLoginServer.java import java.net.*; import java.io.*; // This class is responsible for creating a random session // key and for creating the class to handle a new client. public class MultiLoginServer extends Object implements Runnable { protected Thread thread; protected ServerSocket serverSock; public MultiLoginServer(int listenPort)
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch28.htm (16 of 19) [8/14/02 10:56:27 PM]
throws IOException { // Create the socket that CGI program will connect to serverSock = new ServerSocket(listenPort); } public void run() { while (true) { Socket sock = null; // Accept a new client connection try { sock = serverSock.accept(); } catch (Exception ignore) { continue; } // Spawn a server to handle the new connection try { SecureLoginServer server = new SecureLoginServer( sock.getOutputStream()); server.start(); // If there was an error, close down the socket } catch (Exception oops) { try { sock.close(); } catch (Exception ignore) { } } } } public void start() { Thread thread = new Thread(this); thread.start(); } public void stop() { thread.stop(); thread = null; } public static void main(String[] args) { int port = 1234; // Allow the port address to be set as a property String portStr = System.getProperty("port"); // Parse the port address
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch28.htm (17 of 19) [8/14/02 10:56:27 PM]
try { port = Integer.parseInt(portStr); } catch (Exception ignore) { } // Start the server try { MultiLoginServer server = new MultiLoginServer(port); server.start(); } catch (Exception e) { e.printStackTrace(); System.exit(1); } } }
Finally, the CGI program that connects to the multiclient server is similar to the single-client CGI program. The only difference between the two programs is in their main methods. This being the case, it makes sense to just create a subclass of the single-client CGI program and create a new main. Listing 28.8 shows the multiclient CGI program.
Listing 28.8 Source Code for MultiLoginStartup.java import java.net.*; import java.io.*; // // // // This is a CGI program that starts up a secure Telnet session using a multi-client server that should already be running. It connects to the server to get the port number and session key which it passes back to the client.
public class MultiLoginStartup extends SecureLoginStartup { public static void main(String[] args) { try { int port = 1234; // Allow the port to be set as a system property String portStr = System.getProperty("port"); // Parse the port value try { port = Integer.parseInt(portStr); } catch (Exception e) { } // Connect to the login server Socket sock = new Socket(InetAddress.getLocalHost(), port);
// create an input stream for reading the parameters back from the server DataInputStream in = new DataInputStream( sock.getInputStream()); // Read the client port String portLine = in.readLine(); int clientPort = Integer.parseInt(portLine); // Read the session key String sessionKey = in.readLine(); // Send the web page to the browser sendNormalResponse(clientPort, sessionKey); } catch (Exception e) { sendErrorResponse(e.toString()); return; } } }
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f28-1.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f28-2.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f28-3.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f28-5.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f28-6.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f28-7.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f28-8.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f28-9.gif
CONTENTS
G G
Designing a Basic Shopping Cart Creating a Shopping Cart User Interface H Creating a Catalog Applet H Creating the Shopping Cart Applet
One of the popular mechanisms in electronic commerce is the electronic shopping cart. When you are shopping over the Web, you can select numerous items that go into your virtual shopping cart. The shopping cart keeps a continuous list of what you want to buy and how much it costs. When you have finished selecting items, you simply push a button and place your order. One of the problems with many CGI shopping carts is that all the shopping cart information resides on the Web server. Every time you add an item to your cart, you have to communicate with the Web server. If you have a slow connection, it may take you quite a while to select the items. A Java applet is an ideal place for a shopping cart. It can manage the items locally rather than saving them on the Web server. When you decide that it's time to place your order, the Java applet sends your order to the Web server.
// This class contains data for an individual item in a // shopping cart. import java.net.URL; public class ShoppingCartItem implements Cloneable { public String itemName; public int itemCost; public int quantity; public URL descriptionURL; public ShoppingCartItem() { } public ShoppingCartItem(String itemName, int itemCost, int quantity, URL descriptionURL) { this.itemName = itemName; this.itemCost = itemCost; this.quantity = quantity; this.descriptionURL = descriptionURL; } // // // // // The add method is a quick method for combining two similar items. It doesn't perform any checks to insure that they are similar, however. You use this method when adding items to a cart, rather than storing two instances of the same item, you add the quantities together. public void add(ShoppingCartItem otherItem) { this.quantity = this.quantity + otherItem.quantity; } // The subtract method is similar to the add method, but it // removes a certain quantity of items. public void subtract(ShoppingCartItem otherItem) { this.quantity = this.quantity - otherItem.quantity; } // You can store items in a hash table if you implement hashCode. It's // always a good idea to do this. public int hashCode() { return itemName.hashCode() + itemCost; } // The equals method does something a little dirty here, it only
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch29.htm (2 of 18) [8/14/02 10:56:40 PM]
// compares the item names and item costs. Technically, this is // not the way that equals was intended to work. public boolean equals(Object other) { if (this == other) return true; if (!(other instanceof ShoppingCartItem)) return false; ShoppingCartItem otherItem = (ShoppingCartItem) other; return (itemName.equals(otherItem.itemName)) && (itemCost == otherItem.itemCost); } // Create a copy of this object public ShoppingCartItem copy() { return new ShoppingCartItem(itemName, itemCost, quantity, descriptionURL); } // Create a printable version of this object public String toString() { return itemName+" cost: "+itemCost+" qty: "+quantity+" desc: "+ descriptionURL; } }
Note One interesting thing about the ShoppingCartItem class is that it cheats when it comes to object equality. It treats any two objects with the same name and cost as being equal. If they have different quantities, they are still considered equal. This is usually not a good idea, since it can lead to confusion, but in this particular instance, it works nicely. If you have an item with some quantity value, you can search through the cart for the same object (ignoring the quantities) and if you find a matching object, you can just add their quantities together.
The next item on the agenda is the shopping cart itself. Since this is a very simple model of a cart, independent of the user interface, the cart should be observable. In other words, other objects should be able to watch the shopping cart to see when it changes. This allows you to write a user interface that updates itself whenever the cart is changed, yet you keep the user interface code out of the cart. The Observer/Observable mechanism is very handy for this sort of thing. The shopping cart is a subclass of the Observable class. Whenever the cart changes, it sends a notification to its observers. When you
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch29.htm (3 of 18) [8/14/02 10:56:40 PM]
implement an observable object, you frequently need to notify the observers of different types of changes. For instance, in the shopping cart, you can add an item, remove an item, or change the quantity of an item. Since there is only one way to notify the observers that a change has taken place, you need to cram all that information into a single method call. You can accomplish this by creating an object that holds the information about the change. Listing 29.2 shows the ShoppingCartEvent class that holds the change information.
Listing 29.2 Source Code for ShoppingCartEvent.java public class ShoppingCartEvent { // Define the kinds of changes that can take public static final int ADDED_ITEM = public static final int REMOVED_ITEM public static final int CHANGED_ITEM // item is the item that is affected public ShoppingCartItem item; // eventType is the kind of change that has taken // place (add/remove/change) public int eventType; public ShoppingCartEvent() { } public ShoppingCartEvent(ShoppingCartItem item, int eventType) { this.item = item; this.eventType = eventType; } }
place 1; = 2; = 3;
Now you can create the shopping cart itself. All you really need is a vector object for storing the cart items and a variable for the total cost so far. You must also be sure that you always notify your observers whenever the cart changes. Another object may be keeping a duplicate record of all the items in the cart. If you add or remove an item from the cart without sending a notification, the other object will no longer have an accurate representation of the shopping cart. Listing 29.3 shows the ShoppingCart object.
Listing 29.3 Source Code for ShoppingCart.java import java.applet.*; import java.awt.*; import java.net.*;
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch29.htm (4 of 18) [8/14/02 10:56:40 PM]
import java.util.*; // This class is a simple container of shopping cart items. // It is observable, which means that it notifies any interested // classes whenever it changes. public class ShoppingCart extends Observable { protected Vector items; // the items in the cart protected int total; // the total item cost so far public ShoppingCart() { items = new Vector(); total = 0; } // Add a new item and update the total public void addItem(ShoppingCartItem newItem) { // See if there's already an item like this in the cart int currIndex = items.indexOf(newItem); ShoppingCartEvent event = new ShoppingCartEvent(); if (currIndex == -1) { // If the item is new, add it to the cart items.addElement(newItem); event.item = newItem; event.eventType = ShoppingCartEvent.ADDED_ITEM; } else { // If there is a similar item, just add the quantities ShoppingCartItem currItem = (ShoppingCartItem) items.elementAt(currIndex); currItem.add(newItem); event.item = currItem; event.eventType = ShoppingCartEvent.CHANGED_ITEM; } total += newItem.itemCost * newItem.quantity; // Tell the observers what just happened setChanged(); notifyObservers(event); } // Remove item removes an item from the cart. Since it removes // n items from the cart at a time, if there are more than n items
// in the cart, it just subtracts n from the quantity. public void removeItem(ShoppingCartItem oldItem) { // Find this object in the cart int currIndex = items.indexOf(oldItem); ShoppingCartEvent event = new ShoppingCartEvent(); if (currIndex == -1) { // If it wasn't there, just return, assume everything's okay return; } else { ShoppingCartItem currItem = (ShoppingCartItem) items.elementAt(currIndex); // If you are trying to subtract more items than are in the cart, // adjust the amount you want to subtract so it is equal to the // number of items in the cart. if (oldItem.quantity > currItem.quantity) { oldItem.quantity = currItem.quantity; } // Adjust the total total -= oldItem.itemCost * oldItem.quantity; currItem.subtract(oldItem); event.item = currItem; event.eventType = ShoppingCartEvent.CHANGED_ITEM; // If the quantity drops to 0, remove the item entirely if (currItem.quantity == 0) { items.removeElementAt(currIndex); event.eventType = ShoppingCartEvent.REMOVED_ITEM; } } // Tell everyone what happened setChanged(); notifyObservers(event); } // getItems returns a copy of all the items in the cart public ShoppingCartItem[] getItems() { ShoppingCartItem[] itemArray = new ShoppingCartItem[items.size()];
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch29.htm (6 of 18) [8/14/02 10:56:40 PM]
Now that you have the basic framework for a shopping cart, you can work on a user interface for it.
In order to implement this system with multiple applets, you need a way for the applets to locate each other. While applets can use the AppletContext class to find each other, your best bet is the AppletRegistry class, which was introduced in Chapter 10, "Inter-Applet Communication." The AppletRegistry class is an observable class, which means that an applet can be notified whenever new applets are loaded. If you don't use the registry, you must be prepared to occasionally poll for the other applets in case your applet starts up first. For a simple user interface, you may decide to present a scrollable list of items both in the catalog and in the shopping cart. Unfortunately, the AWT List class leaves much to be desired when it comes to scrollable lists. The biggest problem with the List class is that is only stores strings. Since the strings you store in the scrollable lists are the strings the user sees, you can't really hide any information in them. For instance, if you look at the information stored in the ShoppingCartItem class, you realize that you don't want the URL for the item's description clogging up space in the list. Listing 29.4 shows an ObjectList class that allows you to associate an object with each entry in a scrollable list.
Listing 29.4 Source Code for ObjectList.java import java.awt.*; import java.util.*; // This class is a special version of a scrollable list that // associates an object with each element in the list. public class ObjectList extends List { Vector objects; // the objects that correspond to list entries public ObjectList() { objects = new Vector(); } public ObjectList(int items, boolean multiSelect) { super(items, multiSelect); objects = new Vector(); } public synchronized void addObject(Object ob) { // add a string version of the object to the list super.addItem(ob.toString()); // add the object itself to the object vector objects.addElement(ob); } public synchronized void addObject(Object ob, int position) { // add a string version of the object to the list super.addItem(ob.toString(), position); // add the object itself to the object vector if (position >= objects.size()) { objects.addElement(ob); } else { objects.insertElementAt(ob.toString(), position); } } public synchronized void addObject(String label, Object ob) { // Allow the object to be assigned a label independently of the object super.addItem(label); objects.addElement(ob); }
public synchronized void addObject(String label, Object ob, int position) { // Allow the object to be assigned a label independently of the object super.addItem(label, position); if (position >= objects.size()) { objects.addElement(ob); } else { objects.insertElementAt(ob.toString(), position); } } public synchronized void delObject(Object ob) { // See if the object is in the vector int index = objects.indexOf(ob); // If not, just return if (index < 0) return; // Remove the object from the vector objects.removeElementAt(index); // Remove the list entry super.delItem(index); } public synchronized Object getSelectedObject() { // Get the index of the current selection int i = getSelectedIndex(); if (i == -1) return null; // Return the object currently selected return objects.elementAt(i); } public synchronized Object[] getSelectedObjects() { // Get the indices of all the selected objects int[] selectedItems = getSelectedIndexes(); // Create an array of all the selected objects Object[] whichObjects = new Object[ selectedItems.length]; for (int i=0; i < selectedItems.length; i++) { whichObjects[i] = objects.elementAt(i); } return whichObjects;
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch29.htm (9 of 18) [8/14/02 10:56:41 PM]
} public int indexOf(Object ob) { // Locate a particular object return objects.indexOf(ob); } public Object objectAt(int index) { // Return the object at a particular index return objects.elementAt(index); } public void replaceObject(Object ob, int index) { // Change a specific entry in the vector replaceItem(ob.toString(), index); // Change a specific entry in the list objects.setElementAt(ob, index); } public void replaceObject(String label, Object ob, int index) { // Change a specific entry in the vector replaceItem(label, index); // Change a specific entry in the list objects.setElementAt(ob, index); } }
Note You should use the ObjectList class in place of the List class in many, if not all, situations. It is important to be able to associate objects with the entries in a list, and even more important to keep the content of the list strings down to the bare minimum needed by the user.
Since the catalog and the shopping cart are different applets, they need some way to communicate back and forth. The ShoppingCart class is the perfect mechanism for this. Whenever someone selects an item from the catalog, the catalog applet adds the item to the ShoppingCart class. The shopping cart applet is an observer of the ShoppingCart class and sees the new item immediately. Tip If you recall the Model-View-Controller paradigm, which was discussed in Chapter 9 "Creating Reusable Graphics Components," the ShoppingCart class represents the model of the data. The catalog represents the controller, since it takes user input and translates it into changes in the model. The shopping cart applet is the view of the model, since it displays the items stored in the actual cart. The shopping cart applet also acts as a controller since it also takes input.
When the catalog applet starts up, it looks for the shopping cart applet via the applet registry. When it finds the other applet, it calls getShoppingCart to locate the instanceof the ShoppingCart class that the two applets will share. Listing 29.5 shows the ItemPickerApplet class, which implements the user interface for the catalog portion of the shopping cart system.
Listing 29.5 Source Code for ItemPickerApplet.java import import import import import java.awt.*; java.applet.*; java.net.*; java.util.*; java.io.*;
// This class represents the catalog portion of a shopping cart. // You can select items and then either view a description of // the item or add the item to the shopping cart. public class ItemPickerApplet extends Applet implements Observer { ObjectList items; ShoppingCart cart; AppletRegistry registry; public void init() { // Watch the applet registry to see when the Shopping Cart applet // becomes active registry = AppletRegistry.instance(); registry.addObserver(this); items = new ObjectList(); // Get the URL of the list of items that are for sale String itemURL = getParameter("itemList");
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch29.htm (11 of 18) [8/14/02 10:56:41 PM]
if (itemURL != null) fetchItems(itemURL); // Put the items in the center of the screen setLayout(new BorderLayout()); add("Center", items); checkForShoppingCart(); // Add this applet to the registry registry.addApplet("Item Picker", this); } public void checkForShoppingCart() { // See if the shopping cart has been loaded yet Applet applet = registry.findApplet("Shopping Cart"); if (applet == null) return; ShoppingCartApplet cartApplet = (ShoppingCartApplet) applet; // Get the shopping cart used by the shopping cart applet cart = cartApplet.getShoppingCart(); // Create the panel for adding items Panel southPanel = new Panel(); // Set up some command buttons for adding and describing items southPanel.add(new CommandButton("Describe Item", new ItemPickerDescribe(this))); southPanel.add(new CommandButton("Add Item", new ItemPickerAdd(this))); add("South", southPanel); } public void update(Observable obs, Object ob) { if (cart != null) return; checkForShoppingCart(); } // When someone presses the "Add Item" button, the doAdd method // is called. public void doAdd() { // Find out what object was selected Object ob = items.getSelectedObject(); if (ob == null) return; // Add the item to the cart cart.addItem(((ShoppingCartItem)ob).copy());
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch29.htm (12 of 18) [8/14/02 10:56:41 PM]
} // When someone presses "Describe Item", the doDescribe method // is called. public void doDescribe() { // Find out which object was selected Object ob = items.getSelectedObject(); if (ob == null) return; ShoppingCartItem item = (ShoppingCartItem) ob; // If it has a description URL, open it up in another frame if (item.descriptionURL != null) { getAppletContext().showDocument( item.descriptionURL, "descframe"); } } // parseItem extracts an item name, cost, and URL from a string. The // items should be separated by |'s. public void parseItem(String str) { StringTokenizer tokenizer = new StringTokenizer(str, "|"); if (tokenizer.countTokens() < 3) return; String name = tokenizer.nextToken(); int cost = 0; try { cost = Integer.parseInt(tokenizer.nextToken()); } catch (Exception ignore) { } URL descURL = null; try { descURL = new URL(tokenizer.nextToken()); } catch (Exception ignore) { } items.addObject(name, new ShoppingCartItem(name, cost, 1, descURL)); } // fetchItems gets a list of available items from the web server and // uses parseItem to parse the individual items. If a line begins with // the # character, it is ignored (# is typically a comment character).
public void fetchItems(String urlName) { try { URL url = new URL(urlName); DataInputStream inStream = new DataInputStream( url.openStream()); String line; while ((line = inStream.readLine()) != null) { if (line.charAt(0) == '#') continue; parseItem(line); } } catch (Exception e) { } } }
Notice that the ItemPickerApplet uses the CommandButton class that was also introduced in Chapter 10, "InterApplet Communication." The CommandButton class did not have to be changed at all in order to be used with this application. The only necessary items are a few command classes that provide the glue between the command buttons and the catalog applet. Listing 29.6 shows the ItemPickerDescribe command class. The other command classes are almost identical to the ItemPickerDescribe class.
Listing 29.6 Source Code for ItemPickerDescribe.java public class ItemPickerDescribe extends Object implements Command { ItemPickerApplet cart; public ItemPickerDescribe(ItemPickerApplet cart) { this.cart = cart; } public void doCommand() { cart.doDescribe(); } }
Now that most of the hard work has been done, the shopping cart interface itself is fairly easy. Basically, the shopping cart applet must be an observer of the shopping cart. Whenever the applet receives an update notification telling it that the shopping cart has changed, the applet simply changes its local list of items, which is a scrollable list (actually, it's an ObjectList). The shopping cart applet is also responsible for sending the order to the Web server. For posting data to the Web server, you can adapt the PostSockURL or URLPost classes from Chapter 6, "Communicating with a Web Server." Listing 29.7 shows the ShoppingCartApplet class.
Listing 29.7 Source Code for ShoppingCartApplet.java import import import import import java.applet.*; java.awt.*; java.util.*; java.net.*; java.io.*;
// This class provides a user interface for the ShoppingCart class public class ShoppingCartApplet extends Applet implements Observer { protected ShoppingCart cart; protected ObjectList itemList; protected TextField customerName; protected TextField totalField; public ShoppingCartApplet() { // Make this class an observer of the shopping cart cart = new ShoppingCart(); cart.addObserver(this); // Create the list of objects in the cart itemList = new ObjectList(); // Create the field for the total cost so far totalField = new TextField(10); totalField.setEditable(false); totalField.setText("Total: "+cart.total); setLayout(new BorderLayout()); // Create a field for the customer name customerName = new TextField(20); // Combine the label and the name field on a single panel Panel namePanel = new Panel(); namePanel.add(new Label("Customer Name: ")); namePanel.add(customerName); // Put the name field up at the top and the item list in the center
add("North", namePanel); add("Center", itemList); // Create buttons for removing items and placing an order and put // them along the bottom. Panel southPanel = new Panel(); southPanel.add(new CommandButton( "Remove Item", new ShoppingCartRemove(this))); southPanel.add(new CommandButton( "Place Order", new ShoppingCartOrder(this))); southPanel.add(totalField); add("South", southPanel); // Tell the applet registry about this applet AppletRegistry.instance().addApplet("Shopping Cart", this); } public String makeItemString(ShoppingCartItem item) { return item.itemName+" Qty: "+item.quantity+ " Cost: "+item.itemCost; } public void update(Observable whichCart, Object ob) { ShoppingCartEvent event = (ShoppingCartEvent) ob; if (event.eventType == ShoppingCartEvent.ADDED_ITEM) { // If there is a new item in the cart, add it to the scrollable list itemList.addObject(makeItemString(event.item), event.item); totalField.setText("Total: "+cart.total); itemList.validate(); } else if (event.eventType == // If an item has been removed from the cart, remove it from the list ShoppingCartEvent.REMOVED_ITEM) { itemList.delObject(event.item); totalField.setText("Total: "+cart.total); itemList.validate(); } else if (event.eventType == ShoppingCartEvent.CHANGED_ITEM) { // If an item has changed, update the list int index = itemList.indexOf(event.item); itemList.replaceObject(makeItemString( event.item), event.item, index); totalField.setText("Total: "+cart.total); itemList.validate(); }
} // If the user clicks on "Remove Item," remove it from he list public void doRemove() { Object ob = itemList.getSelectedObject(); if (ob == null) return; ShoppingCartItem item = ((ShoppingCartItem)ob).copy(); item.quantity = 1; cart.removeItem(item); } // doPlaceOrder uses PostSockURL to post the order to a web // server. You will need to customize this method to fit your needs. public void doPlaceOrder() { try { URL postURL = new URL( getDocumentBase().getProtocol(), getDocumentBase().getHost(), getDocumentBase().getPort(), "/shopping"); ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); PrintStream outStream = new PrintStream(byteOut); outStream.println("Custname: "+ customerName.getText()); ShoppingCartItem[] items = cart.getItems(); for (int i=0; i < items.length; i++) { outStream.println( items[i].itemName+"|"+ items[i].quantity); } String response = PostSockURL.post(postURL, byteOut.toString()); System.out.println(response); } catch (Exception e) { e.printStackTrace(); } } public ShoppingCart getShoppingCart() { return cart; } }
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch29.htm (17 of 18) [8/14/02 10:56:41 PM]
Figure 29.1 shows the shopping cart applet in action. Figure 29.1 : The shopping cart applet works in conjunction with a catalog applet. When you request a description of an item, the catalog applet opens another browser window for viewing descriptions. Since the descriptions are simple URLs, you can include any data that you could normally place on a Web page. Figure 29.2 shows the description window for the shopping cart applet. Figure 29.2 : An item description can be described by simple HTML pages. In addition to providing an introduction to electronic shopping carts, this chapter reuses several key components from previous chapters. This makes the programs in this chapter easier to write, and it shows that the components that were presented as being reusable really are reusable. One of the upcoming additions to Java, the Java Electronic Commerce Framework (JECF), will help you when developing shopping cart applications. Your shopping cart will be allowed to conduct financial transactions with the user's Java Wallet, which provides payment via credit card access, electronic cash, and other forms of electronic transactions. Chapter 31, "Java Electronic Commerce Framework (JECF)," provides an overview of the services provided by the JECF. Finally, remember to use secure communications when performing any transactions that involve personal information such as credit card numbers or social security numbers. It is a good idea to provide secure access at all times, because some customers will want all aspects of a transaction to be hidden from prying eyes.
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f29-1.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f29-2.gif
CONTENTS
G
G G
G G
G G
The Difficulties of Electronic Commerce H Theft of Information H Fraudulent Programs H Proprietary Solutions H Static Solutions H Platform-Dependence Creating Online Services with the JECF Storing Information in the Wallet Database H Keeping Data Safe H Performing Transactions Implementing a Shopping Cart Applet with the JECF Offering Services with Cassettes H Creating Other Wallet Services H Ensuring Cassette Security H Dealing with System Failures JECF Availability Getting More Information About the JECF
The Internet has provided a tremendous opportunity for business. Many millions have already been made by businesses related to the Internet and companies that have taken advantage of the Internet's unique features. Like any financial medium, the Internet has sparked concerns about the safety and reliability of doing business online. Part of this hesitation stems from the fact that there is no real standard for conducting secure, safe, and simple transactions online. Although several methods do exist for Internet
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch31.htm (1 of 10) [8/14/02 10:56:47 PM]
commerce, they are all built upon proprietary protocols and existing, static payment methods. Although most of these systems now have secure encryption-based security, none provides a comprehensive solution for developers and entrepreneurs seeking to conduct business over the Internet. This is where the JECF comes into play. As an open, cross-platform, extensible API, it has all the advantages of Java without the drawbacks of proprietary protocols. In short, it is the first all-inclusive solution for online commerce. Note Although the JECF is still only in the specification phase, it is important to understand the concepts behind it. You may not be able to create an application using the JECF today, but you can start planning for it today. You can design your current applications so they can take advantage of the JECF when it becomes available. A good system designer always looks to the horizon to see what's coming and designs with future extensions in mind.
Theft of Information
The first main difficulty in electronic commerce is, ironically, both the easiest to solve and the source of the most consternation: the theft of important information. People who are wary of electronic commerce fear that their credit card number, or other personal information, will be stolen by "hackers" or other miscreants and used for illicit purposes. Luckily, with the advent of public-key encryption, and its implementation in the Java Security API and Netscape's Secure Sockets Layer (SSL) 3.0, intercepting encrypted information across the Internet is nearly impossible.
Fraudulent Programs
Good encryption does not necessarily equal good security, however. Especially with a comprehensive commerce system, there are many points at which a hostile applet or application could gain access to sensitive system information. These problems can potentially increase in an object-oriented environment such as Java. For instance, an applet posing as a legitimate service provider could make illegitimate charges on a user's credit card, unless sufficient preventative measures are taken.
Proprietary Solutions
Other barriers to electronic commerce include proprietary protocols and solutions. Every time a developer tries to write an application for conducting online commerce, he or she must overcome the same set of problems (encryption, security, and so on) from scratch. This is redundant and makes commercial Internet development expensive and time-consuming. In addition, developers who use existing protocols are often locked in to a specific commerce system. For instance, many credit card companies have their own centralized network for conducting electronic transactions. However, in order to access this network, developers must use the protocols established by that specific company-which may or may not be compatible with other companies. An application that supports many protocols can become bulky and hard to manage.
Static Solutions
Further, existing commerce solutions are based on existing forms of commerce-mainly credit cards and checks. A comprehensive solution must account for many other forms of transactions, and most importantly, must be able to adapt to future forms, like game tokens, cash cards, "smart" cards, and so on. A dynamic extensible architecture such as this is essentially absent from the field of Internet commerce.
Platform-Dependence
Even commerce systems that overcome these problems often run into the barrier of single-platform support. Because the Internet is made up of numerous types of computers running many different operating systems, a commerce system needs to support this wide range.
The JECF is centered around the Java Wallet. Each user has his or her own wallet which contains a number of cassettes. Cassettes are to a wallet what an applet is to a browser. Cassettes provide some form of financial service to a wallet. In a way, cassettes are like credit cards. When you are shopping and go to pay for something, you open your wallet and select an appropriate credit card (or that old-fashioned cash stuff). When you perform an online transaction, you may select a payment cassette from your wallet. The online service then interacts with your cassette, which may, in turn, interact with your credit card company or an electronic cash server.
Performing Transactions
The Wallet database has classes for conducting all kinds of transactions. These include record-keeping transactions, as well as user interactions and exchanges of money, goods, or services. Each type of
transaction has its own class type that determines its parameters and functionality. Transactions can also be extended to include more interesting exchanges, such as barter agreements or contracts. Further, the JECF can create tallies, which are essentially electronic receipts for products purchased. All of this provides a robust framework for conducting commerce. Database Transactions and User Transactions These are the two most common forms of transactions in the database. Database transactions include the storage of transaction information and are discussed in detail in the "Backing out Pending Transactions" section of this chapter. User transactions are those in which the Wallet's owner makes a purchase using a payment cassette, and are discussed throughout this chapter, most notably in the shopping cart and cassette sections. Making Electronic Purchases An exchange transaction is an extension of the standard user transaction in which money is exchanged for goods. In an exchange transaction, any kind of value can be traded, including money, but also including trades for stock, goods, services, time, and so on. In fact, this opens up the possibility of having online contracts, which could be legally as legitimate and binding as paper contracts. This requires that digital signing be extended from applets and cassettes to create signable documents, which requires a whole other set of protocols and classes for authenticating user signatures and enforcing contracts signed online. These issues, however, are still a long way off - signable applets will probably not become a reality until 1997, and signable documents will probably take another year. Performing Multi-Party Transactions Complex transactions are just like regular exchange transactions, except that they can include more than two parties. This is ideal for multiple-party sales, or loans involving a third party. This could also make it possible for groups of people to form contracts with companies-perhaps an online mutual fund could contract with all of its members at once. This type of transaction has the advantage of being fully adaptable to a variety of circumstances-it is not locked into traditional two-party contracts, but can be used for just about anything. This helps maintain the dynamic nature of the JECF. Implementing Digital Cash with Microtransactions Microtransactions are used by companies to issue cash substitutes such as cash cards or tokens. These transactions allow small, predefined charges to be made against a pre-authorized account. This would allow gaming companies to issue online tokens redeemable for games, with the money charges being made against a special company account. Or, a company could issue coupons or vouchers that could be used to purchase certain products from a third party, which would be paid out of the issuing company's account. Microtransactions are important because they allow companies to engage in non-cash sales and promotions. Because many people are wary about spending their own money online, this gives Internet
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch31.htm (5 of 10) [8/14/02 10:56:47 PM]
vendors the opportunity to let users "win" tokens or coupons and then redeem those for prizes. Services such as Riddler (http://www.riddler.com) allow users to play games and earn "CAPS," which can be redeemed for certain prizes that the company buys. While this is effective, the JECF allows such companies to issue same-as-cash tokens that can be redeemed at other vendor locations, thus freeing them to provide a much wider variety of services.
standpoint of companies, this provides an easy way to give their customers online access to their accounts in a secure, standard, cross-platform way.
makes the whole system much more secure. Creating Unique Cassette Identities The identity of a Cassette is provided by the entity that signed it. Every Cassette must have a special Identity object that is used to give the user (and the JECF system) accurate information about the behaviors of the cassette. This Identity is digitally signed to prevent tampering. Further, to prevent another cassette from "borrowing" its signed status, the cassette makes use of a Ticket object. Tickets are an indirect method of accessing the Identity object. Because of security considerations, the Identity object cannot be directly accessed; this might allow a malicious applet to steal its Identity. Instead, a Cassette generates a Ticket object based upon its Identity. Although a Ticket can be created from an Identity, the reverse is not true. When queries are made of the Ticket, it fetches the required information from the Cassette Identity, but does not contain this information itself. The process is very much like the pointers used by Java to represent data in memory; the Ticket (pointer) allows the data (Identity) to be accessed, but does not actually contain the data itself. This, combined with the digital signature and shopping cart security measures, makes it virtually impossible for anyone to gain access to a user's personal financial information by posing as a legitimate vendor.
Despite all of the measures the Java Wallet takes to prevent a dispute from arising from an erroneous transaction, these are still possible. For instance, if a transaction is interrupted en route to the vendor, but is still transmitted by the payment Cassette to a credit card company, a user might wind up paying for a product they never received. Although this scenario is unlikely, the JECF still provides a method to resolve disputes among any combination of the buyer, seller, and cassette vendor. This functionality is implemented in the Problem class, which allows for convenient correspondence between any of the three parties. This eliminates the need for tedious phone calls, conference calls, and hard copies of correspondence. Instead, parties use the Problem Resolution System to send Problem reports back and forth. These can be easily responded to, and allow problem troubleshooters to keep track of each problem separately. All transactions are sent and received via e-mail, so problems can be answered immediately.
JECF Availability
The JECF API is expected to be released as an "alpha" test product in the last quarter of 1996. Currently, only a brief White Paper and code outline are available, although JavaSoft says it is reviewing the API with major commerce vendors. Many companies have also endorsed the JECF concept, but only time will tell which products will support it. In any event, the JECF will definitely not be ready for fullfledged application development until sometime in 1997, possibly even as late as 1998. Until that time, developers and users will have to make do with existing inferior, yet viable, solutions.
G G G G
http://java.sun.com/products/commerce/ JavaSoft's JECF pages commerce@java.sun.com JavaSoft's e-mail address for queries regarding the JECF http://www.yahoo.com/Business and Economy/Companies/Computers/Software/Financial/Electronic Commerce/ Yahoo!'s Electronic Commerce listing http://java.sun.com/javaone/abstracts.html Session abstracts from the JavaOne conference. Includes several talks about the JECF, as well as slides in Adobe's PDF format http://www.gamelan.com Repository of all Java things http://www.teamjava.com Site devoted to Java developers and companies http://www.javaworld.com Online magazine, good site for the latest Java news and information http://www.visa.com/ Visa's home page, good site for information about online commerce solutions
Although still a long way off, the JECF is one of the most exciting new additions to the Java API. If adopted by the Internet, business, and legal communities (it already has a long list of sponsors), the JECF could one day replace, or at least enhance, the current process of selling products and completing
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch31.htm (9 of 10) [8/14/02 10:56:47 PM]
contracts. This is one of the truly revolutionary ramifications of Java's development and it promises abundant benefits. As long as it is implemented cautiously and thoroughly, the Java Electronic Commerce Framework could become the most significant and secure form of commerce yet devised.
CONTENTS
G G G
G G
Letting Customers Digitally Sign Orders Using Encryption in All Network Communications Creating Java Services for Netscape Servers H Creating a Server-Side "Hello World" H Installing a New Server-Side Java Applet H Handling Forms from Server-Side Applets H Sending Files as a Response H Returning Multi-Part Responses H Maintaining Information Between Applet Invocations Making Server-Side Applets Work on Different Web Servers Performing Secure Transactions
When you do business on the Web, you need to assure your customers that their personal information is safe. Obviously, you need to keep the credit card numbers secure, but sometimes you need to protect more than that. Sometimes the contents of an order need to be kept quiet. You may be selling items or services of a personal nature, for instance. Sometimes competitors can learn about a company's plans just by analyzing their recent orders. Obviously, you need to provide Web services that support encryption to keep the contents of the services private. For you to really do this securely, you need a signed digital certificate, registered with some trusted certificate authority. To protect your customers, you should also allow customers to verify themselves with signed certificates, as well. As digital commerce becomes more mainstream, digital signatures will continue to grow in importance. Your digital signature may one day be as important, or even more important, than your handwritten signature. Unfortunately, digital signatures are still rather expensive to maintain. The average user isn't going to pay a fee to a certificate authority just to keep their signature on file. At some point, however, there should be a cheaper way to keep digital signatures on file. It may be a service offered by credit card companies, who have a vested interest in preventing fraud. It is also possible that other signature mechanisms will be available soon.
One of the next new features will be personal digital signatures. Each customer will have their own signature, or set of signatures. By letting a customer digitally sign an order, you protect yourself and your customer. From the customer's standpoint, being able to digitally sign orders means that other people can't place phony orders using the customer's name and credit card number. This gives the customer extra security, knowing that even if someone had their credit card number, they couldn't place an order on your system. This mechanism also protects you from the same kind of fraud. You don't want someone else placing phony orders using the names of your good customers. When you receive a digitally signed order, you know that it came from the person who signed the order. Your customer places an order, digitally signing it to verify that it is their order. The customer then sends both the order and the digital signature to you, as shown in Figure 30.1. Figure 30.1 : A customer sends you a digitally signed order. At this point, you can confirm the customer's identity by verifying it with a certificate authority. If someone were trying to create a fake order, they would not know the customer's digital signature key, so they would not be able to sign the order. In the future, credit card companies might require digital signatures on all electronic transactions. This could cut down on fraud, as long as people keep their private signature keys away from prying eyes. There are a number of signature exchange protocols that may be required for credit card transactions. The credit card company would need your signature on a receipt, as well as the customer's. After a customer sends you a digitally signed order, you create an electronic credit card receipt and digitally sign it, then pass it to the customer, as shown in Figure 30.2. Figure 30.2 : You send the customer an electronic, digitally signed credit card receipt. Figure 30.3 illustrates the next step in the sequence. The customer verifies your signature on the receipt, then digitally signs the receipt and sends it back. Figure 30.3 : The customer digitally signs the receipt and sends it back. Now, you have a signed receipt to send to the credit card company which shows that the customer agrees to the transaction. The credit card company can verify the customer's signature. You can also use digital signatures to prevent the transmission of credit card numbers over the Internet. In this case, you need some way to get the customer's credit card number up front, as well as the public key for their digital signature. You keep their credit card number in a secure database, along with their signature key and the customer account number. Now, when a customer places an order, they give you only their customer number, which you use to look up their credit card number. Since you also require that the order is digitally signed, someone else couldn't use that customer's account number. It is conceivable that you could perform unencrypted transactions this way, however, you still need to use a secure download method to download the Java applets that will perform the transaction. Otherwise, you have the potential for someone to create a phony applet, as discussed in Chapter 27, "Encrypting Data."
At the moment, the only encryption mechanism that is readily accessible to Java applets is through the SSL protocol built into the Web browsers. Unfortunately, not all Java-enabled browsers support SSL. Future releases of Java will include a security library with many encryption routines. This will eventually allow remote-object systems, like RMI and CORBA, to support encrypted sessions. This is extremely important. You don't want to be tied to the restrictions of http communications, as you are when using https URLs (SSL-enabled URLs).
Listing 30.1 Source Code for ServerHello.java import netscape.server.applet.*; import java.io.PrintStream; public class ServerHello extends HttpApplet { ServerHello() {} public void run() throws Exception { if (returnNormalResponse("text/html")) { PrintStream out = getOutputStream(); out.println("<HTML><HEAD>"); out.println("<TITLE>Hello World!</TITLE>"); out.println("</HEAD>"); out.println("<BODY>"); out.println("<H1>Hello World!</H1>"); out.println("</BODY></HTML>"); } } }
Figure 30.4 shows the very simple output from this applet on a Web browser. Figure 30.4 : Server-side Java applets may generate HTML output.
Once you install a server-side Java applet, you can run it using an URL of this form: http://host_name/server-java/applet_name For example, if you were running the ServerHello applet on a host called pandora.contessa.com, you would use the following URL: http://pandora.contessa.com/server-java/ServerHello
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch30.htm (4 of 19) [8/14/02 10:56:53 PM]
Listing 30.2 Source Code for FormDemo.java import import import import netscape.server.applet.*; java.io.PrintStream; java.util.Hashtable; java.util.Enumeration;
// This is a Netscape server-side applet that generates a // form which posts information back to this same applet. public class FormDemo extends HttpApplet { FormDemo() {} public void run() throws Exception { if (returnNormalResponse("text/html")) { // If this applet was retrieved with a GET, send the input form if (getMethod().equals("GET")) { sendInputForm(); } else { // Otherwise, this must have been a post, so retrieve the posted data processForm(); } } } protected void sendInputForm() throws Exception { PrintStream out = getOutputStream(); // Send the header out.println("<HTML><HEAD>"); out.println("<TITLE>Java-Five Needs Input!</TITLE>"); out.println("</HEAD>"); out.println("<BODY>");
out.println("<H1>Give Me Some Input!</H1>"); // Send the input form out.println("<FORM action=\"/server-java/FormDemo\" "+ "method=POST>"); // Input field titled "First Name" out.println("First Name: "); out.println("<INPUT type=\"text\" name=\"First Name\">"); out.println("<P>"); // Input field titled "Last Name" out.println("Last Name: "); out.println("<INPUT type=\"text\" name=\"Last Name\">"); out.println("<P>"); // Button to submit the form out.println("<INPUT type=submit><P>"); out.println("</FORM>"); out.println("</BODY></HTML>"); } protected void processForm() throws Exception { PrintStream out = getOutputStream(); // Send the initial part of the response out.println("<HTML><HEAD>"); out.println("<TITLE>OOOOH!! Input!! MMM!!</TITLE>"); out.println("</HEAD>"); out.println("<BODY>"); out.println("<H1>Thanks for the input!</H1>"); out.println("Just for the record, here's what you sent:<P>"); // Get the fields from the form Hashtable formData = getFormData(); // For each field on the form, print the value Enumeration keys = formData.keys(); while (keys.hasMoreElements()) { String key = (String) keys.nextElement(); out.println(key+": "+formData.get(key)+"<P>"); } out.println("</BODY></HTML>"); } }
Figure 30.5 shows the initial input form from this applet, while Figure 30.6 shows the results from the submission of the form.
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch30.htm (6 of 19) [8/14/02 10:56:53 PM]
Figure 30.5 : The FormDemo applet displays a simple input form. Figure 30.6 : The FormDemo applet processes its own input form.
Listing 30.3 Source Code for ShowPicture.java import netscape.server.applet.*; import java.io.File; public class ShowPicture extends HttpApplet { ShowPicture() {} public void run() throws Exception { returnFile(new File("\\pictures\\kaitlynn.jpg")); } }
Figure 30.7 shows the picture returned by this applet. Notice that there is no surrounding text, only an image. Figure 30.7 : A server-side applet can return images, movies, and audio files.
instead of sending a single response to the client, you send multiple responses. This allows you to send a series of images as an animation, or display a page of information that occasionally updates itself. The returnMultipartResponse method in the HttpApplet class allows your server-side Java programs to return multi-part responses: public boolean returnMultipartResponse(String subtype) throws IOException The subtype value should either be "mixed" or "x-mixed-replace." Once you send the multi-part response header, you send responses normally, using the returnNormalResponse method. When you generate a multi-part response, you keep the connection to the client open until you tell the client that you are through sending responses. In a server-side applet, you tell the client you are through sending responses by calling the endMultipartResponse method: public void endMultipartResponse() throws IOException Listing 30.4 shows a server-side applet that displays the current time using a multi-part response. It updates the time every five seconds.
Listing 30.4 Source Code for Multipart.java import netscape.server.applet.*; import java.io.PrintStream; import java.util.Date; // This applet sends a multi-part response that displays the // current time every 5 seconds. public class Multipart extends HttpApplet { Multipart() {} public void run() throws Exception { // Tell the server we are sending a multi-part response if (returnMultipartResponse("x-mixed-replace")) { PrintStream out = getOutputStream(); while (true) { // Send the next part of the response returnNormalResponse("text/html"); out.println("<HTML><HEAD>"); out.println("<TITLE>Web Clock</TITLE>"); out.println("</HEAD>"); out.println("<BODY>"); out.println("<H1>Current Time</H1>"); out.println(new Date());
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch30.htm (8 of 19) [8/14/02 10:56:53 PM]
Listing 30.5 Source Code for PersistentInfo.java import java.util.Vector; // // // // // This is a singleton class that maintains a vector of strings. Since the constructor is protected, the only way you can get an instance of this class is by calling the instance method, which returns the single shared copy of the class.
public class PersistentInfo { // singleInstance is the lone instance of this class protected static PersistentInfo singleInstance; // info is the vector of strings protected Vector info; // instance returns the lone instance of this class, and creates one // if there isn't one already. public synchronized static PersistentInfo instance() { if (singleInstance == null) { singleInstance = new PersistentInfo(); } return singleInstance; } protected PersistentInfo()
{ info = new Vector(); } // getInfo returns an array of the strings stored in the info vector public synchronized String[] getInfo() { String[] strings = new String[info.size()]; info.copyInto(strings); return strings; } // addInfo adds another string to the info list public synchronized void addInfo(String newInfo) { info.addElement(newInfo); } }
The PersistentInfo class stays active while the Java VM is running. This means that any instances of HttpApplet can access the info in PersistentInfo at any time. You can use this method to keep database connections open, or keep open a session with a host that may take a long time to set up. Tip If you use singleton classes with your applets, be especially careful about using synchronization. While you may not be explicitly creating threads, each HttpApplet object runs in its own thread.
Listing 30.6 shows a server-side applet that uses the PersistentInfo object. The applet generates a form with a single text field. Any information in the text field is added to the PersistentInfo object.
Listing 30.6 Source Code for PersistDemo.java import import import import // // // // // netscape.server.applet.*; java.io.PrintStream; java.util.Hashtable; java.util.Enumeration;
This is a Netscape server-side applet that generates a form which posts information back to this same applet. It uses a second class called PersistentInfo. The PersistentInfo class sticks around, so information is preserved for future instances of this class.
public class PersistDemo extends HttpApplet { PersistDemo() {} public void run() throws Exception { if (returnNormalResponse("text/html")) { // If this applet was retrieved with a GET, send the input form if (getMethod().equals("GET")) { sendInputForm(); } else { // Otherwise, this must have been a post, so retrieve the posted data processForm(); } } } protected void sendInputForm() throws Exception { PrintStream out = getOutputStream(); // Send the header out.println("<HTML><HEAD>"); out.println("<TITLE>Persistent Information Demo</TITLE>"); out.println("</HEAD>"); out.println("<BODY>"); out.println("<H1>Current information:</H1>"); // Print out the strings currently stored in the PersistenceInfo class String strings[] = PersistentInfo.instance().getInfo(); for (int i=0; i < strings.length; i++) { out.println(strings[i]+"<P>"); } out.println("<H1>Please enter some new information</H1>"); // Send the input form out.println("<FORM action=\"/server-java/PersistDemo\" "+ "method=POST>"); // Input field titled "First Name" out.println("Information: "); out.println("<INPUT type=\"text\" name=\"Information\">"); out.println("<P>"); // Button to submit the form out.println("<INPUT type=submit><P>"); out.println("</FORM>"); out.println("</BODY></HTML>");
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch30.htm (11 of 19) [8/14/02 10:56:53 PM]
} protected void processForm() throws Exception { // Get the fields from the form Hashtable formData = getFormData(); // If there's any information, add it to the PersistentInfo class String newInfo = (String) formData.get("Information"); if (newInfo != null) { PersistentInfo.instance().addInfo(newInfo); } // Put up another form so we can get more input sendInputForm(); } }
Figure 30.8 shows the output from this server-side applet. Figure 30.8 : A singleton object can preserve information for multiple HttpApplet instances.
This will certainly not be the case forever, though. As security becomes integrated into the Java environment, more Web servers, and other systems, will support encryption and digital signatures. The trick, as always, is to design and build your applications so you can run them now, but keep them flexible enough to embrace other technologies as they become available. Listing 30.7 shows a framework for a simple ordering system. Notice that the system doesn't have any knowledge of applets or servlets-just ordering.
Listing 30.7 Source Code for TheStore.java // A dummy storefront that has some items for sale. // The store doesn't record its transactions, however. public class TheStore { // Make sure there's only one instance of the store protected static TheStore singleInstance; protected StoreItem items[]; public static TheStore instance() { if (singleInstance == null) { singleInstance = new TheStore(); } return singleInstance; } // Create a store with some items protected TheStore() { items = new StoreItem[3]; items[0] = new StoreItem("Dongle", 199); items[1] = new StoreItem("Widget", 599); items[2] = new StoreItem("Tweaker", 799); } // Return a list of available items public StoreItem[] getItems() throws StoreException { try { return (StoreItem[]) items.clone(); } catch (Exception e) { return null; } } // A dummy routine for purchasing products
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch30.htm (13 of 19) [8/14/02 10:56:53 PM]
public void purchase(String customerId, String itemList[]) throws StoreException { // Normally you would do something here to record the purchase } }
This class also uses some auxiliary classes, which are shown in Listings 30.8 and 30.9.
Listing 30.8 Source Code for StoreException.java public class StoreException extends Exception { public StoreException(String why) { super(why); } }
Listing 30.9 Source Code for StoreItem.java public class StoreItem { public String itemName; public int price; public StoreItem(String itemName, int price) { this.itemName = itemName; this.price = price; } }
Now that the first step has been taken, and there is an application defined, you can start creating different interfaces into the application. Since you already have some examples of server-side applets that present HTML forms and parse them, it is fairly simple to create a similar applet that places orders with this application. Listing 30.10 shows an HTML front end for this application.
// This class implements an HTML interface for the TheStore class. public class HTMLStoreFront extends HttpApplet { HTMLStoreFront() {} public void run() throws Exception { if (returnNormalResponse("text/html")) { // If this applet was retrieved with a GET, send the input form if (getMethod().equals("GET")) { sendInputForm(); } else { // Otherwise, this must have been a post, so retrieve the posted data processForm(); } } } protected void sendInputForm() throws Exception { PrintStream out = getOutputStream(); // Send the header out.println("<HTML><HEAD>"); out.println("<TITLE>The Store</TITLE>"); out.println("</HEAD>"); out.println("<BODY>"); out.println("<H1>What would you like to order?</H1>"); // Send the input form out.println("<FORM action=\"/server-java/HTMLStoreFront\" "+ "method=POST>"); out.println("<P>Customer ID #: "); out.println("<INPUT type=text name=\"customerID\">"); out.println("<P>"); // List the items on sale from the store, and their prices StoreItem[] items = TheStore.instance().getItems(); for (int i=0; i < items.length; i++) { out.println("<INPUT type=checkbox name=\""+ items[i].itemName+"\" value=off>"); out.println(items[i].itemName+" "+ items[i].price+"<P>"); }
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch30.htm (15 of 19) [8/14/02 10:56:53 PM]
// Button to submit the form out.println("<INPUT type=submit><P>"); out.println("</FORM>"); out.println("</BODY></HTML>"); } protected void processForm() throws Exception { PrintStream out = getOutputStream(); // Get the fields from the form Hashtable formData = getFormData(); Vector partOrder = new Vector(); String customerID = null; // For each field on the form, see if the checkbox is on Enumeration keys = formData.keys(); while (keys.hasMoreElements()) { String key = (String) keys.nextElement(); // If we got the customerID field, save it and go on if (key.equals("customerID")) { customerID = (String) formData.get(key); continue; } // If the checkbox was on for this part, add it to the parts vector partOrder.addElement(key); } String[] orderItems = new String[partOrder.size()]; partOrder.copyInto(orderItems); try { TheStore.instance().purchase(customerID, orderItems); // Send a success response out.println("<HTML><HEAD>"); out.println("<TITLE>The Store - Order Completed"); out.println("</TITLE>"); out.println("</HEAD>"); out.println("<BODY>"); out.println("<H1>Order Complete. "); out.println("Thanks for the business!</H1>"); out.println("<P>Items on your order:<P>"); for (int i=0; i < orderItems.length; i++) { out.println(orderItems[i]+"<P>");
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch30.htm (16 of 19) [8/14/02 10:56:53 PM]
} } catch (Exception e) { // If we got an error ordering, print the reason out.println("<HTML><HEAD>"); out.println("<TITLE>The Store - Order Aborted!"); out.println("</TITLE>"); out.println("</HEAD>"); out.println("<BODY>"); out.println("<H1>Order Aborted!</H1>"); out.println("Here's why:<P>"); out.println(e+"<P>"); } out.println("</BODY></HTML>"); } }
Plain old HTML forms are pretty boring. You really want to liven up your ordering system by running a Java applet on the client side, right? You can do this, and still keep everything secure! You can use the technique outlined in Chapter 6 "Communicating with a Web Server," for posting to a URL. This will allow you to send secure data to a server-side applet, which can then interpret the posted data and send a response. Rather than making your applet parse the form returned by the HTMLStoreFront class, you can create a similar class that is friendly to applets. Instead of sending HTML data, it sends plain text in a format that the applet can easily read. Listing 30.11 shows such a class.
Listing 30.11 Source Code for AppletStoreFront.java import import import import import netscape.server.applet.*; java.io.PrintStream; java.util.Hashtable; java.util.Vector; java.util.Enumeration;
// This class implements a secure interface for the TheStore class. // This interface is used by client-side applets to store and // retrieve data securely. public class AppletStoreFront extends HttpApplet { AppletStoreFront() {} public void run() throws Exception { if (returnNormalResponse("text/plain")) { // If this applet was retrieved with a GET, send the input form
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch30.htm (17 of 19) [8/14/02 10:56:53 PM]
if (getMethod().equals("GET")) { sendItemList(); } else { // Otherwise, this must have been a post, so retrieve the posted data processOrder(); } } } protected void sendItemList() throws Exception { PrintStream out = getOutputStream(); StoreItem[] items = TheStore.instance().getItems(); for (int i=0; i < items.length; i++) { out.println(items[i].itemName); out.println(items[i].price); } } protected void processOrder() throws Exception { PrintStream out = getOutputStream(); // Get the fields from the form Hashtable formData = getFormData(); Vector partOrder = new Vector(); String customerID = null; // For each field on the form, see if the checkbox is on Enumeration keys = formData.keys(); while (keys.hasMoreElements()) { String key = (String) keys.nextElement(); // If we got the customerID field, save it and go on if (key.equals("customerID")) { customerID = (String) formData.get(key); continue; } // add the part to the parts vector partOrder.addElement(key); } String[] orderItems = new String[partOrder.size()]; partOrder.copyInto(orderItems); try {
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch30.htm (18 of 19) [8/14/02 10:56:53 PM]
Once again, you haven't had to change a single line of code in the original application class. All you do is add new user interfaces for it. You could follow this same track and create an RMI interface and a CORBA interface to this application. Of course, if you can't get a secure version of RMI or CORBA yet, you can't do secure transactions.
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f30-1.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f30-2.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f30-3.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f30-4.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f30-5.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f30-6.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f30-7.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f30-8.gif
CONTENTS
G G G G G
G G G G
Focusing on Function, not Form Providing Access to New Systems Using CORBA to Open Up a Closed System Encapsulating a TCP/IP System Encapsulating with Native Method Calls H Wrapping Java Around a Native Interface H Writing Native Methods in C Encapsulating by Emulating a User Getting Assistance from the Legacy System Presenting a Different Interface Combining Multiple Systems H Handling Deletions Originating in the Legacy System H Using a Two-Phase Commit Protocol H Implementing a Two-Phase Commit Some Real-World Examples H An Example Legacy System H Creating a New Application for the Existing Terminal Base H Creating a New Interface for an Existing Application H Clearing a Path for Migration off the Legacy System
Keeping up with technology is one of the constant problems that most businesses face. You buy a top-of-the-line database system and a year later, after you've spent considerable time converting your organization over to the new database, the database is obsolete. If you try to keep your systems on the leading edge all the time, you'll probably spend more time and money changing systems than you do using them. More likely, you'll keep the old system for a long, long time. Most of these older systems are referred to as "legacy" systems, and while they may not be on the forefront of technology, they are still the lifeblood of many businesses. By the time you decide to switch systems, all your applications are so heavily tied to the legacy system that you have to rewrite all your applications, making it even more expensive to switch. You have to factor these costs into your decision to switch. You don't switch just to use new technology. You switch because a new system will save you money. If you are more heavily tied to the legacy system, you are more willing to keep using it, because the cost of switching is more than the cost of maintaining the existing system. It is obvious that you could reduce the cost of upgrading and changing systems if you could design your applications so they weren't as dependent on specific products.
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch32.htm (1 of 20) [8/14/02 10:57:10 PM]
As you design new applications, you don't want your design to be constrained by the limits of the current system. Obviously, the implementation will have these constraints, but you want to leave room to grow. Rather than attacking this problem on a per-application basis, take a step back and look at the systems you have today, and try to put a prettier face on them. You can use a technique called encapsulation to make a legacy system look like a newer system. There is no magic here, since you can't really make the old system work just like a newer one. It won't run any faster, or magically perform some new functions. What encapsulation does is break your dependence on the exact interfaces of the legacy system. For example, suppose you have an MVS system from IBM, and if you are at a large corporation, you probably do. While MVS is expanding its accessibility, you still have some limitations. Suppose, for instance, that you don't have an MVS TCP/IP gateway available and you must use IBM's proprietary SNA protocol to access MVS. How are you going to write an applet to access MVS data? You aren't going to find a copy of the Netscape Navigator that comes with SNA built in. What you can do, however, is write a program that sits in between MVS and the applet and does the necessary translation. Figure 32.1 illustrates an example configuration. Figure 32.1 : An encapsulation program puts a more friendly face on a legacy system.
START TRANSACTION <customer ID> Begins the ordering process for a particular customer. LIST PARTS Gets a list of available parts. ORDER <part number> <quantity> Adds a part to the list of parts being ordered. REMOVE <part number> Removes a part number from the current list. END TRANSACTION Completes the order. ABORT TRANSACTION Cancels the order.
As you can see by this set of commands, the ordering system requires a high degree of interaction. This set of commands is the "form" of the legacy system. Its function is that it creates orders of parts for your customers. That may seem like a subtle point, but it is very important. One of the reasons we spend so much money re-engineering old applications is that they were too heavily tied to the form of the legacy system. If you got a new ordering system, the likelihood of the commands being different is fairly high. Most systems use their own set of commands. The function should still be the same for a new system, however. You'll still be using a system to create orders. If you focus on the function of the system and don't worry as much about the actual commands, your applications will adapt to new systems more readily. Of course, there is a trade-off here. You have to do a little extra work to translate from the legacy system to the application. That's where the encapsulation comes in. You could, for example, create an encapsulation that placed an entire order at once. It would have methods to list the available parts and place an order. The encapsulation program would translate the order placement into the series of commands expected by the legacy system. Figure 32.2 illustrates how this might take place.
Figure 32.2 : An encapsulation can present an interface different from the legacy system. This form of encapsulation is actually a design pattern known as a "facade."
You design your application the way you want, then adapt the legacy systems to fit your application, not vice versa. For example, suppose you design a new e-mail system using Java for both the client and server. Figure 32.4 illustrates a possible configuration. Figure 32.4 : A Java e-mail system. Now that you have your application designed, you concentrate on encapsulating the legacy system to fit into the new application structure. In the case of the e-mail system, you may want to allow the old legacy terminals to access the system. You create an object or a server that looks like a legacy host to the terminals, but looks like a client to the e-mail system. This object would translate the new-user interface into something the legacy terminals understand. Figure 32.5 shows an example of this. Figure 32.5 : You can translate a new-user interface into something a legacy terminal understands.
You could take a different approach with the new application. Suppose you want to use the existing e-mail application on the legacy system. Maybe it is too expensive or difficult to get the legacy terminals to access the new e-mail application. You could create an object that translates the old e-mail interface into something that looks like the new application, as far as the clients are concerned. Figure 32.6 illustrates this configuration. Figure 32.6 : You can translate a legacy interface into something newer clients can deal with. Note In both of the previous examples, the encapsulation was based on the clear separation between the application and user interface. Once you separate them, you can translate different application interfaces and user interfaces into something that fits your design.
Figure 32.8 illustrates a typical native method encapsulation. Figure 32.8 : You often need native methods to access a legacy system.
Listing 32.1 Source Code for Ordering.java package ordering; // this class provides an interface to an ordering system. It makes // calls to native C methods which take care of accessing the real // system. public class Ordering extends Object { // transactionID is the transaction id returned by the C interface // to the ordering system. Each call to the ordering system must // be accompanied by the transaction ID. int transactionID; // Create an instance of an Ordering object, which begins an ordering // transaction. public Ordering(String customerId) throws OrderingException { transactionID = startTransaction(customerId); } // Add a part to the current order public void orderPart(String partNumber, int quantity) throws OrderingException { orderPart(transactionID, partNumber, quantity); } // remove a part from the current order public void removePart(String partNumber) throws OrderingException
{ removePart(transactionID, partNumber); } // finish the order public void endTransaction() throws OrderingException { endTransaction(transactionID); } // abort the order public void abortTransaction() throws OrderingException { abortTransaction(transactionID); } // These methods are implemented in a local DLL protected native int startTransaction(String customerId) throws OrderingException; public native static String[] listParts(); protected native void orderPart(int txnID, String partNumber, int quantity) throws OrderingException; protected native void removePart(int txnID, String partNumber) throws OrderingException; protected native void endTransaction(int txnID) throws OrderingException; protected native void abortTransaction(int txnID) throws OrderingException; }
Note
The methods in the Ordering class are not quite a straight pass-through to the real ordering system. Rather, it provides a slightly higher level of abstraction. It considers an Ordering object to be a transaction, and hides the transaction ID from the users of the Java class. You will find that many C libraries return information that you must use for future function calls. You don't actually use the information. Instead, since the library has no way of grouping method calls and data together, like an object does, the library makes you handle the data and forces you to pass it to whatever functions need it. When you design a Java interface to a native library, keep information like this hidden within the Java class. Don't let the public methods pass back anything that isn't useful.
/* This is an absolutely skeletal implementation of the native methods for the Ordering class. The only method that does anything is the listParts method, which returns an array of strings. The rest of the methods are dummies. */ int nextId = 1; /* For returning different transaction ID's */
/* parts is a list of values that will be returned in listParts */ char *parts[] = { "12345 Widget", "23456 Deluxe Widget", "55534 Thing", "30038 Zippy" }; struct Hjava_lang_String; long ordering_Ordering_startTransaction(struct Hordering_Ordering *thisPtr, struct Hjava_lang_String *customerId) { return nextId++; } HArrayOfString *ordering_Ordering_listParts(struct Hordering_Ordering *thisPtr) { HArrayOfString *retval; ClassArrayOfString *strs; int i; /* Create an array of strings that will contain 4 strings */ retval = (HArrayOfString *) ArrayAlloc(T_CLASS, 4); /* If we couldn't allocate the memory, throw a Java exception */ if (retval == NULL) { SignalError(EE(), "java/lang/OutOfMemoryException", NULL); return NULL; } /* ArrayAlloc allocated an array of objects, this call makes it an array of strings */ unhand(retval)->body[4] = (HString *) FindClass(EE(), "java/lang/String", TRUE); /* Get a pointer to the array of strings */ strs = unhand(retval); /* Fill the array of strings with Java strings */ for (i=0; i < 4; i++) { strs->body[i] = makeJavaString(parts[i], strlen(parts[i]));
} return retval; } void ordering_Ordering_orderPart(struct Hordering_Ordering *thisPtr, long txnID, struct Hjava_lang_String *part, long quantity) { } void ordering_Ordering_removePart(struct Hordering_Ordering *thisPtr, long txnID, struct Hjava_lang_String *part) { } void ordering_Ordering_endTransaction(struct Hordering_Ordering *thisPtr, long txnID) { } void ordering_Ordering_abortTransaction(struct Hordering_Ordering *thisPtr, long txnID) { }
The skeletal implementation in Listing 32.2 doesn't interface with a real ordering system. Chances are, a real system wouldn't have exactly these methods. For one thing, there are too many things left out. However, for the purposes of illustration, assume that there really is a system that uses the above methods. Once you can access a legacy system, you can change the way it is accessed, as you will see later in this chapter in the section titled "Presenting a Different Interface."
Figure 32.10: Sometimes, the only way to get data from a legacy system is via a modem or serial connection.
If you are encapsulating a legacy system, you have the ability to change the interface into the system to some degree. If the legacy system does something in a strange way, use the encapsulation to hide it. Take the ordering system shown previously, for example. When you create an order, you have to begin a transaction, add parts, then either end or abort the transaction. Maybe you don't want to work that way anymore. You might be better off building an order at the client side, then have the client send the entire order, which is then fed into the system. This would be more in line with the way Web services work. You try to do everything in one message, since there is no concept of a session on a Web server. Of course, designing your system to work only as a session-less system may also be a bad thing. Do what makes sense for your application. Tip You can use this technique when writing an application to use an interface that has not yet been completely specified or is changing. You decide on the interface your application will use, and write your application. When the real interface is completed, write an object that translates from one interface to the other.
The ordering system is a good candidate for a different interface. You can easily create an object that lets you place orders in a single, session-less method call, rather than the group of calls in the current interface. Listing 32.3 shows a simple class with a static method that places an order using the Ordering class.
Listing 32.3 Source Code for Orders.java package ordering; public class Orders extends Object { public static void placeOrder(String customerID, PartOrder[] parts) throws OrderingException { Ordering ordering = new Ordering(customerID); for (int i=0; i < parts.length; i++) { ordering.orderPart(parts[i].part, parts[i].quantity); } ordering.endTransaction(); } }
Listing 32.4 shows the PartOrder class, which encapsulates the information for a single entry in the ordering system.
Listing 32.4 Source Code for PartOrder.java package ordering; public class PartOrder extends Object { public String part; public int quantity; public PartOrder() { } public PartOrder(String part, int quantity) { this.part = part; this.quantity = quantity; } }
As you can see, the placeOrders method is much easier to use than the regular Ordering object. In a typical ordering system, this simplified interface may be a disadvantage. If this were an airline reservations system, you would have to book all your seats at once and hope that no one got a seat you wanted before you did. When you have a session-based system, you
reserve the seats (or the parts) as your transaction progresses. If you abort the transaction, the things you have reserved get freed up for someone else. You will have to decide if you are willing to give up this functionality. It may or may not be worth the cost of a simplified interface.
Figure 32.11 shows an encapsulation that stores data in two places. Figure 32.11: An encapsulation can make multiple databases look like one. There can be some serious drawbacks to this approach, so you need to be very careful if you decide to try it.
If you are willing to accept a small possibility of discrepancies, you can remove the speed blockage caused by the legacy system. You could run a synchronization program that verified that a particular customer was in both databases, and removed any customers from the other database when they disappear from the legacy database. For a large database, this may be unfeasible. Some legacy systems keep a transaction log, so you could run a synchronization program that looked for deletions in the transaction log and perform the deletion on the other database. This is a very reasonable solution. You don't have to perform a large number of queries to synchronize the database. If you are only accessing the databases through the encapsulation, this problem doesn't occur, because the encapsulation knows whenever data is deleted. You may still have some problems if one database is down. You may need a two-phase commit protocol.
When a database system performs a two-phase commit, it writes the transaction to a log, then performs the transaction without committing it. If the transaction is aborted, it undoes the changes. The log is used in case the database fails during the transaction. Once the database has responded in the first phase, it must be able to either commit or abort the transaction. Generally, you implement a two-phase commit by saving enough information to be able to restore things to the way they were before the transaction, then performing the transaction. If the transaction is aborted, you put things back the way they
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch32.htm (13 of 20) [8/14/02 10:57:10 PM]
were. If you have a locking mechanism on the legacy system, you should use it while waiting for a commit or abort in the second phase. If you have performed a change in the first phase and another transaction is able to come in and use the changed version of the data, you may not be able to back out of the change. If you are able to lock the data against changes, this can't happen.
Since you are probably not familiar with the kind of system normally found at an airline, here are thumbnail sketches of the mainframe, the WAN, and the terminals.
G
The mainframes run an operating system from IBM called Transaction Processing Facility (TPF). It is commonly used by large airlines and financial institutions. For this particular site, the only connectivity to the mainframes is via thick cables called "channel cables." The channel is like an overgrown two-way parallel port. The applications running on the mainframe are very tightly intertwined, and generally difficult to maintain. The terminals are very old, rugged terminals that run a proprietary airline data protocol that is completely incompatible with modern data terminals. These legacy terminals can display only uppercase letters, digits, and a small set of other characters. In addition, they operate in block mode, as opposed to single-character mode. That is, when someone enters data at the terminal, nothing is sent until the person presses the "enter" or "send" key. The idea of "press any key to continue" is unheard of on these terminals. Some users in the corporate offices have modern PCs, using special gateways to talk to the legacy mainframe. The wide-area network is a fairly robust packet-switched network that speaks the IBM channel protocol and the proprietary terminal protocol. In addition, it supports a number of other protocols, including the X.25 standard. This network is very modular and programmable.
You could write the e-mail system on the existing mainframes. This would entail great cost and would not provide any neat GUI features for the more modern terminals. You could buy an off-the-shelf e-mail system and put PCs out in the field to run it. This is probably the first thing many people think of. Unfortunately, it may be even more costly than implementing the e-mail system in the mainframe. After all, you not only have to buy millions of dollars worth of PCs, you also have to put in a distributed LAN infrastructure, and possibly a new wide-area network to support it. You could write a custom e-mail system on a modern server, UNIX or NT, for example, provide full GUI access to the modern terminals, and provide an encapsulation for the legacy terminals so they can send and receive mail, too.
Assume that you want to save the company millions of dollars and create a modern e-mail system while encapsulating the legacy terminals. In keeping with the philosophy of not letting the legacy system drag down your design, you design an e-mail system that allows you to send full ASCII text messages, as well as attach binary files to the messages. Note Hopefully, you considered buying such a product off-the-shelf. There is far too much custom software that needs to be written for you to spend your time writing things that have already been done a million times.
Armed with your new design, you try to determine how to fit the legacy terminals into the plan. Normally, you have only the mainframe and the terminals to deal with. In this case, however, you have a third element-the network. You could place an encapsulation in two different places. Figure 32.13 shows a scheme where you encapsulate the mail system by making it look like the wide-area network to the terminals. The terminals send information to your encapsulation thinking it is the wide-area network. Figure 32.13: You can encapsulate the mail system by emulating the netwrok. Your other option is to make your mail application hook into the network and behave like the mainframe. Figure 32.14 illustrates this configuration. Figure 32.14: You can also encapsulate the mail system by emulating a mainframe. Note There is a third option, which is using the mainframe as the front-end for the application, and passing data from the mainframe to the application. In this particular solution, however, you want to avoid writing code on the mainframe.
Each choice brings its own problems. If you try to emulate the wide-area network, you will probably experience some headaches because you'll have to install code wherever the network connects to the terminals. In general, you want to hook in at as few places as possible, preferably one. If you try to emulate the mainframe, you want to avoid having to set up your own IBM channel and performing the channel protocol. If you get desperate, you can operate that way. In this case, however, there is a better option.
Recall that the network is highly programmable and supports X.25. Also remember that you may often reap huge benefits by adding a little code to the legacy system to open it up to your encapsulation. By adding some code to the network, you can use X.25 to emulate a mainframe rather than using the channel protocol. Note The notion of emulating a legacy host is like a reverse screen-scraping. Rather than pulling data off screens generated by a legacy system, you're generating the screens.
Figure 32.15 illustrates this configuration. Figure 32.15: A little interface code in the network makes for a simple encapsulation. Now that you have a way to emulate a host via X.25, you must decide how you will use the X.25 protocol. You will need to use a native method call of some sort, or, as an alternative, you can create a program in C or C++ that accesses the X.25 line and communicates with users of the X.25 line via TCP/IP. In other words, you create an X.25 server. Figure 32.16 shows how a Java program could access an X.25 network via this X.25 server. Figure 32.16: A Java program uses a TCP/IP server to access the X.25 network. Tip This technique of creating small servers to handle specific interfaces or devices is a reasonable choice for data streams. This is much more efficient than CORBA, since you are just passing large blocks of data without interpreting it. CORBA is much better for message-based communications and remote method invocations.
At this point, you have the connectivity problem solved. You can communicate with the network from your Java program, and you will be able to exchange whatever information you need in order to emulate a mainframe. Now, you actually implement the encapsulation. The job of the encapsulation is to take a command from a legacy terminal and turn it into a method invocation in the e-mail system, then gather the results and display them in a form that the terminal will understand. The encapsulation acts like a client to the e-mail system and like a server to the legacy terminal population. Figure 32.17 shows the e-mail system with PC-based clients and legacy terminal clients. Figure 32.17: The e-mail system interacts with both legacy terminals and PC-based terminals. As you can see, the e-mail system is not limited by the constraints of the legacy terminal population, but it is still able to service those terminals. Your e-mail system is able to work in the world of distributed clients, where its users may be reading their mail from hand-held terminals and other devices.
front-end on an airline reservation system. Your first task, before you even think about connectivity, is designing your reservation system. Remember, your system doesn't have to work exactly the same way that the existing system does. You'll probably want to simplify the interface and require as little typing as possible. This is especially important if your goal is to allow agents at the airport to walk around with small hand-held terminals and provide assistance to passengers from outside the confines of a desk. This allows for more personalized service, and this is where you are really going to realize the benefits of Java. Again, for the sake of simplicity, assume that you have come up with a good, user-friendly design. You aren't ready to move the airline's reservation off the mainframe, however. So, if anyone is going to use your new application, you're going to have to encapsulate the existing reservations system and make it look like your new application. Now it's time to look at connectivity. Again, you have the mainframe, the network, and the legacy terminals. Start with a simple screen-scraping and proceed from there. In other words, the encapsulation is going to emulate a terminal. You again have choices for where you put your encapsulation:
G
You can create an encapsulation that looks like the wide-area network, making the mainframe think it is talking to the network when it is really talking to your encapsulation. This would let your encapsulation emulate whole groups of terminals at once. You can hook your encapsulation into the network as a terminal, or cluster of terminals, using the network's X.25 interface. You can write software to emulate an actual legacy terminal, which would be both a huge hassle and a huge waste of time.
The X.25 interface looks like the best candidate here because it's cheap and non-proprietary. Plus, you could use the same X.25 server from the e-mail system to access X.25 from Java. Figure 32.18 shows the relationship between the encapsulation, the X.25 server, and the mainframe. Figure 32.18: The network's X.25 interface permits easy emulation of a legacy terminal. In a screen-scraping setup, your encapsulation logs on to the host system with the same commands that a user would normally type. If you are screen-scraping an IBM 3270 application, there's good news and bad news. The bad news is that 3270 screens are no fun to emulate. The good news is that there are some libraries to help you. In the airline example, however, there are no libraries to assist you. After all, it's not like there would be a big customer base for such a library. You must figure out what commands you would have to enter on the legacy system to perform each task that your new application needs to perform. This may involve single commands to the legacy system, or it may involve a whole series of commands. Tip If at all possible, try to minimize the number of extra commands your library has to perform each time. For instance, you don't want it to go through a complete login sequence every time it needs to do commands. Do the minimum number of commands that will ensure that your terminal is set up the way you expect.
Once you have your screen-scraping code written, the other side of your encapsulation acts like the server for your new application. In other words, to the regular clients, it looks like the new system. To the legacy system, it looks like a terminal or a group of terminals. Figure 32.19 shows this screen-scraping encapsulation. Figure 32.19: A screen-scraping encapsulation is sometimes your only choice. The screen-scraping method of encapsulation is often useful when adding new code to the legacy system is impossible, or cost-prohibitive. Screen-scraping is well-suited for rapid prototyping, however. You can throw a functioning model together fairly quickly, without having to add code to the legacy system. As a mechanism for a production system, however, screen-scraping has some serious drawbacks:
G
G G
If the output for a particular command changes, it could throw the screen-scraping code off and give you incorrect results. If the screen-scraper gets out of sync with the host, you can get very strange results. Screen-scraping can be very slow, and often performs more commands than it needs to.
The nice thing here, however, is that once you have a functioning system, you can work on improving it. If you can get raw data out of the legacy system, you will be much happier. The trick with some systems, like the airline reservation system, is that it wasn't built to give out data in a raw form, so you must add custom code. When you want to receive raw data from the legacy system, you must decide how the legacy system should deliver the data. The channel interface is a very fast interface, but it takes more work. If you are in a hurry, you may be better off finding an easier solution. Note This reluctance to directly hook up to an IBM channel is specific to the airline reservation system. If you are connecting to a regular SNA host, this is a very viable solution, and there are many software/hardware packages available that do this.
You also have the option of making some kind of binary data stream to a terminal, which would most likely require a lot of cooperation from both the legacy system and the network. The network makes a nice option here. With a little code on the reservation system and on the network, you can get data over the X.25 interface. The legacy system passes data to the network over the channel, and the network hands it to you via the X.25 interface. Yet again, you have the X.25 server program to keep your Java encapsulation from needing any native methods to access X.25. Unlike screen-scraping, you don't have to perform a series of commands. You can work out a set of messages to be passed between the reservation system and your encapsulation. You won't have to go through the pain of extracting information from text output stream. Instead, the information you need will be laid out in specific places in the messages sent from the reservation system. Figure 32.20 shows how a Java encapsulation program can use X.25 to exchange binary data with the legacy host. Figure 32.20: Your encapsulation can exchange binary data with the legacy system.
Assuming you have the time and the resources, a direct channel connection to the mainframe is the best option in terms of bandwidth. When you need such an interface, you may be better off finding a firm that specializes in channel communications and getting them to write the channel interface code. Assuming you have a way to speak your system's channel code, you should create something similar to the X.25 server. It should speak channel protocol on one side and TCP/IP on the other. Again, this means that you can connect a Java application to the channel gateway with a simple socket instead of creating non-portable native methods. As with the X.25 solution, when passing information over the channel, you need to figure out what data your encapsulation needs to send and receive, and then add code on the legacy system to handle the requests. Figure 32.21 shows a channelbased encapsulation configuration. Figure 32.21: A direct channel connection to a mainframe gives you high-speed access to data.
When you decide to migrate your terminal population first, you must create an encapsulation for the legacy system. You encapsulate the part you aren't changing. As shown in Figure 32.22, you first encapsulate the legacy system, creating what looks like a server for your new application design. Figure 32.22: When migrating legacy terminals, you first encapsulate the legacy system. Next, you begin to migrate your terminal population over to the new application design. Since the old terminals and the new terminals are still accessing the same legacy system, you don't run into any coordination problems. Figure 32.23 shows how your terminal population might look halfway through the migration. Figure 32.23: New workstations use the encapsulation to access the legacy system.
Finally, when all the terminals have been migrated over to the new application design, you can create a new server to replace the legacy system, as shown in Figure 32.24. Figure 32.24: Once the legacy terminals are gone, you can replace the legacy system. When you migrate the legacy system first, you encapsulate the terminal population first. In other words, you create an encapsulation that translates commands from the legacy terminals into commands understood by the new application design. Figure 32.25 illustrates this encapsulation. Figure 32.25: A terminal encapsulation allows legacy terminals to access the new system. Once the new system is in place, you can migrate your terminals over to the newer terminals. Again, since the new terminals access the same system as the old system, you don't have any coordination problems-there is only one system running. Figure 32.26 illustrates this migration in the process of changing the terminals over. Figure 32.26: Legacy terminals and new workstations access the new system. Finally, when you have migrated the last terminal over to the new application design, you are done. Legacy system is a lot harder than it sounds. The techniques presented in this chapter represent your battle strategy. You use them to attack each new legacy system in a different way. Even though you have a good strategy, you still have to fight the battle-or write the code, in this case. That's where the real fun begins. Each system has its own challenges. You may not enjoy it while you're doing it, but sometime down the road, you'll enjoy recounting all the peculiarities of each system you worked with. They all have their own personalities, brought about by years of tweaks and changes. Legacy system migration is crucial to bringing more companies onto the Internet and really advancing into the information age. Very few companies want to create a new system from scratch just so they can be on the Net.
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f32-1.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f32-2.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f32-3.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f32-4.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f32-5.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f32-6.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f32-7.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f32-8.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f32-9.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f32-10.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f32-11.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f32-12.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f32-13.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f32-14.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f32-15.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f32-16.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f32-17.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f32-18.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f32-19.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f32-20.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f32-21.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f32-22.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f32-23.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f32-24.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f32-25.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f32-26.gif
CONTENTS
G
Using Encapsulations to Access Legacy Data H Aiming for Session-Less Transactions H Storing Session Information in the Web Page H Using HTTP Cookies to Preserve Session Information H Choosing a Good Session Identifier H Clearing Out Old Sessions Accessing Legacy Data from Servlets
Many companies get their feet wet on the Web by creating a static display-only page. This may be nice from a marketing perspective, but Web surfers find little use for these pages. The real power of the Web is that you can provide a way for customers to interact with your business. When customers can do business with your company over the Web, they can conduct business at any time of day, from anywhere in the world. They don't have to wait for a salesman, they aren't stuck on hold waiting for an operator-you can give them all the information they need and the power to place orders right at their fingertips. One of the reasons more companies aren't operating this way is that so many companies have older systems that aren't very Web-friendly. IBM has begun to address this problem for many of their customers, providing Web server software for the AS/400 line, and gateway software to access CICS systems. Chapter 34, "Interfacing with CICS Systems," shows you ways to access a CICS system. If your company has a legacy system that needs to be accessed from the Web, you can use the encapsulation techniques from Chapter 32, "Encapsulating Legacy Systems," to help you.
Using the encapsulation techniques you learned in Chapter 32, you create an object that both interacts with the legacy system and presents a nice Java front-end to it.
From the time a client first initiates a session, the sequence would go like this:
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch33.htm (2 of 13) [8/14/02 10:57:34 PM]
1. The client accesses the URL for the Web service. 2. The Web service creates a session with the legacy system, then creates a unique session identifier and stores the session information in a hash table using the session identifier as a key. 3. The Web service returns an HTML page to the client that contains fields for entering data for the session, along with a hidden field containing the session identifier. 4. The client enters some data for the session and clicks the submit button. 5. The Web service reads in the fields from the client's page, including the session identifier. It uses this identifier to locate the session with the legacy system. 6. The Web service updates the current transaction with whatever information was sent by the client, and returns another HTML page containing the same session identifier. 7. When the client signals that it wants to finish the transaction, the server uses the session identifier again to locate the session with the legacy system. It then asks the legacy system to finish the transaction and close down the session. 8. The server removes the session information from the hash table, because the session has been completed. Listing 33.1 shows a servlet that uses a simple sequence number for a session ID. It uses a text field to build up a string of text. Anytime text is entered in the field, it appends the text to the current session text. It also provides a checkbox to signal that the transaction is complete.
Listing 33.1 Source Code for SessionServlet.java import import import import // // // // // java.io.*; java.util.*; java.servlet.*; sun.server.html.*;
This servlet uses hidden variables to keep track of a session. When it starts a new session, it sets a hidden sessionId variable in the web page. Whenever the 'submit' button is clicked, the hidden sessionId is transmitted to the servlet.
public class SessionServlet extends Servlet { // The sessions table maps session id's to session information. For // this example, the session information is just a string containing // the text added during the session. protected Hashtable sessions; // Just for demonstration purposes, we use a simple integer // variable for generating session id's. protected int nextSessionNumber;
public void init() { nextSessionNumber = 0; sessions = new Hashtable(); } // Start new session is called when a request comes in that // does not contain a session id. This routine should generate // a new session id and perform the necessary session setup. protected String startNewSession() { // Generate a new session id String sessionId = ""+nextSessionNumber++; // Set up the session information in the session table (start // with an empty string). setSessionInfo(sessionId, ""); return sessionId; } // getSessionInfo returns the information associated with // a session - in this case, the information string. protected String getSessionInfo(String sessionId) { return (String) sessions.get(sessionId); } // setSessionInfo stores session-related information in the // sessions table. protected void setSessionInfo(String sessionId, String info) { sessions.put(sessionId, info); } // finishSession is called to complete a transaction and close // down the session. protected void finishSession(String sessionId) { sessions.remove(sessionId); } public void service(ServletRequest req, ServletResponse resp) { boolean isNewSession = false;
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch33.htm (4 of 13) [8/14/02 10:57:34 PM]
boolean isExpired = false; boolean isFinished = false; // get a table of variables from the web page Hashtable params = req.getQueryParameters(); // get the session id from the page String sessionId = (String) params.get("sessionId"); // if there was no session id, start up a new session if (sessionId == null) { sessionId = startNewSession(); isNewSession = true; } // get the session info for this session String sessionInfo = getSessionInfo(sessionId); // if there was no session info, it must be an old/invalid session id if (sessionInfo == null) { isExpired = true; } // get the text item to be added to the session information String newStuff = (String) params.get("item"); // if there is text in the "item" field and the session id // isn't expired, add the item to the session info and save it. if (!isExpired && (newStuff != null)) { sessionInfo += newStuff + "\n"; setSessionInfo(sessionId, sessionInfo); } // See if the "finished" flag was checked String finishedFlag = (String) params.get("finished"); isFinished = (finishedFlag != null) && finishedFlag.equals("on"); // Start generating the response resp.setContentType("text/html"); try { resp.writeHeaders(); } catch (IOException e) { e.printStackTrace(); return; }
// Create a response page HtmlPage page = new HtmlPage("Session-Oriented Service"); // We always respond with a form of some kind, the action page.addText("<FORM action=\"/servlet/SessionServlet\">"); // // // // If the transaction isn't finished, and the session id hasn't expired, generate a form for entering text, including the hidden session id, and a checkbox for signalling the last entry in the transaction. if (!isFinished && !isExpired) { // Add the hidden sessionId field page.addText("<INPUT type=\"hidden\""+ " name=\"sessionId\""+ " value=\""+sessionId+"\">"); // Print out the current session info page.addText("<P>Current transaction text:<P>"); page.addText(sessionInfo+"<P>"); // Create the field for adding new text page.addText("<P>New text to add: "); page.addText("<INPUT type=\"text\" name=\"item\">"); // Create the checkbox for signalling the end of the transaction page.addText("<P>Check here when finished: "); page.addText("<INPUT type=\"checkbox\""+ " name=\"finished\">"); // Create the submit buttin page.addText("<P>"); page.addText("<INPUT type=\"submit\">"); // If this is a new session, let the user know if (isNewSession) { page.addText("<P>Congratulations, you've started a "+ "new session!<P>"); } // If the "finished" box was checked, finish the session and // create a simple form to start another transaction } else if (isFinished) { // Close down the session finishSession(sessionId); page.addText("<P>Your transaction is completed.<P>");
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch33.htm (6 of 13) [8/14/02 10:57:34 PM]
page.addText("<P>Press 'Submit' to start another.<P>"); page.addText("<INPUT type=\"submit\">"); // If the session id had expired, print a message about it and create // a button to start another transaction. } else if (isExpired) { page.addText("<P>Uh oh! Your session has expired!"); page.addText("<P>Press 'Submit' to start another.<P>"); page.addText("<INPUT type=\"submit\">"); } // Finish off the form page.addText("</FORM>"); // Write the HTML page to the response output stream try { page.write(resp.getOutputStream()); } catch (IOException e) { e.printStackTrace(); } } }
Figure 33.1 shows the output from the SessionServlet servlet. Figure 33.1 : You can maintain a session ID using a hidden variable.
order to use cookies instead of hidden variables, is change the way you read and write the information. For instance, in the previous example, you read the session ID by saying: Hashtable params = req.getQueryParameters(); String sessionId = (String) params.get("sessionId"); To use a cookie instead, you have to use a few more lines. You must first get the cookie string from the header: // get the cookie value(s) String cookieStr = req.getHeader("Cookie"); Then, if there were a cookie string, you must extract the sessionId cookie: Cookie cookie = null; String sessionId = null; // If there was a cookie string, convert it to a cookie object // and then fetch the sessionId value if (cookieStr != null) { cookie = new Cookie(cookieStr); sessionId = cookie.getValues("sessionId"); // Netscape doesn't seem to like to erase a cookie while it's still // running, so we hack around it by creating a session id of "erased" if ((sessionId != null) && sessionId.equals("erased")) { sessionId = null; } } Unlike the hidden variable example, you still have to write out cookie information when the session ID has expired or you have finished the transaction. As you now know, you can't simply erase a cookie, so we have a special kludge to handle erasing the cookie: // If we need to write out a session id, create a cookie and // write it out. if (!isFinished && !isExpired) { cookie = new Cookie("sessionId", sessionId); cookie.setPath("/servlet/SessionCookie"); resp.setHeader("Set-Cookie", cookie.toString()); } else { // If expired or finished, set the sessionId to "erased"
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch33.htm (8 of 13) [8/14/02 10:57:34 PM]
Listing 33.2 Source Code for NativeServlet.java import import import import java.io.*; java.util.*; java.servlet.*; sun.server.html.*;
// This servlet displays a list of parts from the ordering // system. It shows that a servlet can invoke native methods. public class NativeServlet extends Servlet { public void init() { // Load the native library System.load("ORDERING.dll"); } public void service(ServletRequest req, ServletResponse resp)
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch33.htm (10 of 13) [8/14/02 10:57:34 PM]
{ // Start generating the response resp.setContentType("text/html"); try { resp.writeHeaders(); } catch (IOException e) { e.printStackTrace(); return; } // Create a response page HtmlPage page = new HtmlPage("Native Encapsulation Service"); try { // Get the list of parts from the "legacy system" String[] parts = ordering.Ordering.listParts(); // Display the parts list for (int i=0; i < parts.length; i++) { page.addText(parts[i]+"<P>"); } } catch (Exception e) { page.addText(e.toString()); } // Write the HTML page to the response output stream try { page.write(resp.getOutputStream()); } catch (IOException e) { e.printStackTrace(); } } }
You don't have to restrict the servlet from running in the same Java environment as the legacy system encapsulation, however. You can use CORBA, RMI, or even TCP/IP to link the servlet to the encapsulation. Figure 33.4 shows an example configuration using RMI. Figure 33.4 : A servlet can use RMI to access a legacy system encapsulation. Listing 33.3 shows a banking servlet that retrieves an account balance from the banking service in Chapter 16,
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch33.htm (11 of 13) [8/14/02 10:57:34 PM]
Listing 33.3 Source Code for BankBalance.java import import import import java.io.*; java.util.*; java.servlet.*; sun.server.html.*;
import java.rmi.server.StubSecurityManager; import java.rmi.Naming; import banking.*; // This servlet displays a bank balance using RMI to invoke methods // on the banking service. public class BankBalance extends Servlet { public void init() { // Set the security manager for RMI System.setSecurityManager(new StubSecurityManager()); } public void service(ServletRequest req, ServletResponse resp) { // Start generating the response resp.setContentType("text/html"); try { resp.writeHeaders(); } catch (IOException e) { e.printStackTrace(); return; } HtmlPage page = new HtmlPage("Banking Service"); Account myAccount = new Account( "AA1234", "1017", Account.CHECKING); try { // Get a stub for the BankingImpl object (the stub implements the
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch33.htm (12 of 13) [8/14/02 10:57:34 PM]
// Banking interface). Banking bank = (Banking)Naming.lookup("NetBank"); // Check the initial balance page.addText("<P>My balance is: "+ bank.getBalance(myAccount)+"<P>"); } catch (Exception e) { page.addText(e.toString()); } // Create a response page // Write the HTML page to the response output stream try { page.write(resp.getOutputStream()); } catch (IOException e) { e.printStackTrace(); } } }
Servlets are a big advantage over CGI when you need to do session-oriented transactions with a legacy system. Servlets stick around between requests, so the sessions with the legacy system are preserved for the next time. You already have a framework for tracking these sessions; all you need to do is put it together with your legacy system encapsulation and you're ready to hit the Web.
CONTENTS
G G G G G G
A Thumbnail Sketch of CICS The CICS External Call Interface The Java-CICS Gateway API Creating Multiple-Call LUWs Creating Web Interfaces to CICS Providing a CORBA Interface to CICS H Creating a CORBA-CICS Gateway H Creating CORBA Interfaces to CICS Programs
IBM's Customer Information Control System (CICS) is an extremely popular transaction processing system. Sites all over the world use CICS for ordering, inventory, accounting, and almost any other business application you can think of. While CICS itself is still a very active operating system, it can be considered a "legacy" system with respect to the Internet. IBM, realizing the potential of the Internet, has taken huge strides in opening up their products to the Internet. They have created gateways for the AS/400 line of computers and have been pushing them as Web servers. There is also a TCP/IP gateway available for MVS systems, which has been a boon to many MVS shops. In the area of CICS, IBM has created a CICS Web gateway that allows you to create CGI scripts that make calls into CICS. Rather than just stick with that, however, IBM took a major step forward and created the Java-CICS gateway. The Java-CICS gateway allows a Java applet or application to send requests to a CICS system and receive responses. This gateway sits between the CICS system and your Java programs and translates TCP/IP requests from the Java side into SNA or TCP/IP requests on the CICS side. Figure 34.1 shows the relationship between the Java-CICS gateway, a CICS system, and your Java program.
Figure 34.1 : The Java-CICS gateway connects Java programs to CICS systems. Tip The Java-CICS gateway is an excellent example of a non-Java encapsulation program that is Java-friendly.
The External Call Interface (ECI) is a mechanism used by CICS to allow non-CICS clients to invoke programs under CICS. CICS has a remote procedure call mechanism called DPL (Distributed Program Link) which allows a CICS program to start up a CICS program on another CICS host. ECI works exactly the same way as DPL, except that the program that originates the call is not running under CICS. CICS applications are built as a series of programs, where one program runs another program, which is why you can do almost anything you need with ECI. To run a CICS program using ECI, you must supply CICS with a user name and password, as well as the name of the program you want to run. You pass data to the program via a block of data called the COMMAREA block. The contents of the block vary from program to program. There is no fixed format to the block. Typically, however, you pass text information in the block. If you have to pass a numeric value, you pass numbers as a text string. You must also pass information about the LUW for your transaction. A unit of work can span multiple ECI calls, if it needs to. An LUW is similar to a transaction in a relational database. As you make changes over the course of an LUW, the changes are not visible to the rest of the system until you commit the LUW, that is, until you save it. If there is a failure of some kind, CICS will undo the changes made so far in the current LUW, but not the previous LUWs. When you make an ECI call, you pass an LUW token, which is like a session ID for the LUW. When you start a new LUW, you pass a token called ECU_LUW_NEW. CICS will then generate an LUW token value for subsequent ECI calls for this LUW. You must also pass an extend mode parameter for the LUW. You pass either a value of ECI_EXTENDED or ECI_NO_EXTEND. It's very easy to determine when to use ECI_EXTENDED and when to use ECI_NO_EXTEND. You use ECI_NO_EXTEND on the last ECI call in an LUW, and ECI_EXTENDED for all others. If you have only one call in the LUW, it's both the first and last call, so it is sent with the ECI_NO_EXTEND parameter. Sometimes, you realize you want to back out the LUW (undo the changes) or commit it without actually calling another program. You can use an extend mode of ECI_COMMIT to commit the current LUW, or ECI_BACKOUT to back out the changes in the current LUW. In addition to the preceding parameters, which are part of the regular ECI interface, you must pass an additional parameter to the Java-CICS gateway. You must supply the name of the CICS server you want to talk to, because the gateway can talk to many different servers.
The JGateConnection class represents a connection to a Java-CICS gateway server. You can create multiple connections to a single server, or to different servers. You can perform only one ECI call at a time over a single connection. You can perform as many ECI calls as you like over one connection, just one at a time. The constructor for the JGateConnection class takes two parameters-the name and port number of the gateway server you are connecting to. For example: JGateConnection cicsGateway = new JGateConnection( "gateway.myplace.com", 4321); Once you have created the connection, you use the flow method to send ECI requests to the gateway. The flow method takes an ECIRequest object as its only parameter. Any information returned from the ECI call is stored in the Commarea array in the ECIRequest. You create an ECIRequest by passing its constructor the name of the CICS server, the user name, password, Commarea, extend mode, and LUW token. Suppose you had a CICS program called "BOOKSEAT" that booked a seat on an airline flight. Furthermore, assume that the name of the CICS server is "RES," and that the user name and password are airjava and whee, respectively. Also, assume that you must store a 4-digit flight number, a 5-digit date in the form DDMMM, and a 3-letter seat number in the Commarea. The following code segment would create the ECIRequest to book seat 35B on flight 5050 on January 12. Also, this is a one-shot LUW, so use an extend mode of ECI_NO_EXTEND, and an LUW token of ECI_LUW_NEW. byte[] commarea = new byte [12]; // A quickie to copy a string into a byte array "505012JAN35B".getBytes(0, 12, commarea, 0); ECIRequest request = new ECIRequest("RES", "airjava", "whee", "BOOKSEAT", commarea, ECI_NO_EXTEND, ECI_LUW_NEW); To send this request to CICS, you use the flow method in the JGateConnection object, like this: cicsGateway.flow(request); // send the ECI request
Once you send the request, you can check the CICS return code, which is stored in the Cics_Rc variable in the ECIRequest object. If the request completed successfully, the Cics_Rc value should be 0.
you. When you do a multiple-call LUW, there are a few extra things you have to deal with:
G
G G
On the first ECI call, you must sent an LUW token of ECI_LUW_NEW, and the extend mode must be ECI_EXTENDED. After the first ECI call completes, you must save the LUW token returned by the CICS system, which is stored in the Luw_Token variable in the ECIRequest object. You must pass this Luw_Token variable as the LUW token for subsequent ECI requests for this LUW. On the last ECI call, you must set the extend mode to ECI_NO_EXTEND. If you want to commit or back out the current LUW, you set the extend mode to ECI_COMMIT or ECI_BACKOUT. Tip If you reuse the same ECIRequest object for each ECI call in an LUW, the Luw_Token variable will already contain the correct LUW token value. You need to change only the token value when you start a new LUW.
Suppose you want to book a round-trip flight using the BOOKSEAT program illustrated previously. Because you want this to be a single LUW, that is, you want to book both seats as a single transaction, you call BOOKSEAT twice as part of a single LUW. The following code books seat 35B on flight 5050 on January 12 and seat 12A on flight 1313 on January 13 as a single LUW: byte[] commarea = new byte [12]; // A quickie to copy a string into a byte array "505012JAN35B".getBytes(0, 12, commarea, 0); // Book the first flight leg ECIRequest request = new ECIRequest("RES", "airjava", "whee", "BOOKSEAT", commarea, ECI_EXTENDED, ECI_LUW_NEW); cicsGateway.flow(request); // send the ECI request // Now copy in the return flight leg "131313JAN12A".getBytes(0, 12, commarea); // Change the extend mode on the ECI request to ECI_NO_EXTEND request.Extend_Mode = ECI_NO_EXTEND; // Book the second flight leg cicsGateway.flow(request); // send the ECI request
Caution
Be careful when creating multiple-call LUWs. If there is a significant amount of time between calls, you could keep system resources locked unnecessarily. Remember that CICS keeps certain locks on data while an LUW is in progress.
Figure 34.4 : A CORBA-CICS gateway uses the Java-CICS gateway to make ECI calls. The reason you probably want to avoid setting things up this way is that you don't want to carry the CICSspecific features into your future versions. In other words, an interface like this is one of those cases where the remnants of a legacy system can stay embedded in your application long after the legacy system has departed. For example, suppose you write all your applications to use this ECI interface over CORBA. You're now passing all sorts of different requests using the same method call. The only difference between the requests is the program name and the contents of the Commarea. Now, suppose you replace your CICS system with a newer one. If you don't want to replace all your existing applications, you have to create an encapsulation that maps the program name and Commarea information to whatever the new system needs. Someone looking at an application cannot determine what a particular ECI call does without knowing what the CICS program did.
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f34-1.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f34-2.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f34-3.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f34-4.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch35.htm
CONTENTS
G
Writing a Protocol Handler H Step One: Decide Upon a Package Name H Step Two: Create the Directories H Step Three: Set Your CLASSPATH H Step Four: Implement the Protocol H Step Five: Create the Handler Class H Step Six: Compile the Sources Using Protocol Handlers with HotJava H Step One: Update the properties File H Step Two: Run HotJava Using Protocol Handlers with Your Own Applications H The main() Method: Starting FetchWhois H The FetchWhois Constructor: Where the Work Gets Done H The whoisUSHFactory Class: Registering the Protocol Handler H Running FetchWhois More on URLStreamHandlerFactory
Anessential feature that Java attempts to impart to computing is extensibility. Over time, newer and better means of communicating will be created. Application protocols will evolve and programs must be able to adapt. Support for new protocols can be added to Java and the HotJava browser through protocol handlers. Protocol handlers are a key tool for maintaining an adaptable environment with Java. They extend support to new URL schemes, which can access new application protocols, or present existing ones in new ways.
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch35.htm
All URLs are classified into schemes. The scheme is the label prior to the first colon. Common schemes are http, ftp, and gopher. A scheme generally identifies an application protocol, and thus Java references often describe the scheme as being the protocol portion of the URL. However, a scheme need not identify a particular protocol. The distinction is important when you implement more complex protocol handlers that implement new URL schemes built upon existing protocols. For instance, a useful protocol handler would be one that created a search scheme, which connected to various Web search engines and returned a unified hit list.
HotJava can make use of new protocol handlers; this is part of the vision guiding HotJava's ongoing development. The intention is that programs like HotJava should have very little core intelligence, providing only a framework for extensibility. When new protocols are invented, HotJava will transparently download a new handler and the user can then access the resource. You can also utilize protocol handlers in your own applications. By installing a new protocol handler and using a registration facility termed a factory, URL objects can use these handlers just as the standard protocols are used. Protocol handlers are not a factor new to your Java programming. In fact, any time an URL object was used to obtain a resource in the previous chapters, a handler inherent in the JDK was used. With the steps that follow, you can create your own handlers to build on this existing set. The examples within this chapter implement the NICNAME/WHOIS protocol, defined in RFC 954. WHOIS is used by Internic, the domain name registration and directory project, to enable individuals to query their database. WHOIS can be used at rs.internic.net, but other sites utilize this service. By opening a TCP socket to the standard port 43, a query can be entered and the response read. Tip More information on this protocol is available at http://www.cis.ohio-state.edu/htbin/rfc/rfc954.html
Caution Java is case-sensitive. Even if your system doesn't treat upper and lower case characters within directory names differently, use the case of the letters as shown within these instructions.
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch35.htm
Lising 35.1 Creating the Directories Under Windows NT and Windows 95 %HOMEDRIVE% cd %HOMEPATH% mkdir classes mkdir classes\ORG mkdir classes\ORG\netspace mkdir classes\ORG\netspace\dwb mkdir classes\ORG\netspace\dwb\protocol mkdir classes\ORG\netspace\dwb\protocol\whois
The process is similar under the UNIX operating system, as shown in Listing 35.2.
Listing 35.2 Creating the Directories Under UNIX cd ~ mkdir mkdir mkdir mkdir mkdir mkdir
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch35.htm
setenv CLASSPATH .:${HOME}:/usr/java/lib If you are on a UNIX system using the Korn or a POSIX-compliant shell, add the following line to whatever file your ENV environment variable points. If ENV is unset, then you could add the line to your ~/.profile file: CLASSPATH=.:${HOME}:/usr/java/lib export CLASSPATH
Listing 35.3 whoisURLConnection.java // This is the package identified for this protocol handler. package ORG.netspace.dwb.protocol.whois; import java.io.*; import java.net.*; // Import the package names used.
/** * This class implements a connection to the new "whois" * URL scheme. * @author David W. Baker * @version 1.0 */
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch35.htm
class whoisURLConnection extends URLConnection { // Some defaults for the WHOIS protocol implementation: // site defaults to rs.internic.net // port defaults to 43 // query defaults to QUIT private static final String DEF_SITE = "rs.internic.net"; private static final int DEF_PORT = 43; private static final String DEF_QUERY = "QUIT"; private static final String CONT_TYPE = "text/html"; static final int URL_BASE = 16; InputStream fromHandler; // Input from the handler Socket whoisSocket; // Socket for communication boolean gotQuery = false; // Did we get the data? /** * Given a URL will instantiate a whoisURLConnection * object. * @param getURL The URL to contact. */ whoisURLConnection(URL getURL) { super(getURL); // Call superclass with the URL. } /** * Connect to the WHOIS server, obtain, and format the * data. * @exception java.io.IOException Indicates a problem * connecting to URL. */ public void connect() throws IOException { String whoisSite; int whoisPort; String whoisQuery; PrintStream toApp; // Send data to app using handler. DataInputStream fromWhois = null; PrintStream toWhois = null; String dataLine; // Set up piped streams for communication between // this handler and the application using it. PipedOutputStream pipe = new PipedOutputStream(); toApp = new PrintStream(pipe); fromHandler = new PipedInputStream(pipe); // Get host from the URL, using default if omitted. if (url.getHost().length() == 0) { whoisSite = DEF_SITE; } else { whoisSite = url.getHost(); } // Get port from the URL, using default if omitted. if (url.getPort() < 1) { whoisPort = DEF_PORT; } else { whoisPort = url.getPort(); } // Get file from the URL, using default is "/" if (url.getFile().equals("/")) { whoisQuery = DEF_QUERY;
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch35.htm (5 of 15) [8/14/02 10:57:46 PM]
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch35.htm
} else { whoisQuery = url.getFile().substring(1); } // Decode the query from the URL. whoisQuery = decodeURL(whoisQuery); // Open a socket to the whois server. whoisSocket = new Socket(whoisSite,whoisPort); // Open streams to communicate with the whois server. fromWhois = new DataInputStream(whoisSocket.getInputStream()); toWhois = new PrintStream(whoisSocket.getOutputStream()); // Send the query to the server. toWhois.println(whoisQuery); // Print out some HTML. toApp.println("<!DOCTYPE HTML PUBLIC \"-//IETF//DTD " + "HTML//EN\">"); toApp.println("<HTML>"); toApp.println("<HEAD>"); toApp.println("<TITLE>Whois Query for: " + url + "</TITLE>"); toApp.println("</HEAD>"); toApp.println("<BODY>"); toApp.println("<H1>Whois Query for : " + url + "</H1>"); toApp.println("<PRE>"); // Loop through the data from the whois server, // printing it all out within the preformatted // text section. while ((dataLine = fromWhois.readLine()) != null) { toApp.println(dataLine); } // Some last HTML. toApp.println("</PRE>"); toApp.println("</BODY>"); toApp.println("</HTML>"); toApp.flush(); // Flush the pipe. toWhois.close(); // Close the streams. fromWhois.close(); whoisSocket.close(); // Close the socket. toApp.close(); // Close one end of the pipe. gotQuery = true; // Data has been obtained. } /** * Determine the content type of the data returned. * @return The content type. */ public String getContentType() { return CONT_TYPE; } /** * Obtain a stream to get data from this protocol handler. * @return The stream to read data. */ public synchronized InputStream getInputStream() throws IOException { // If where has not been obtained, connect()
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch35.htm (6 of 15) [8/14/02 10:57:46 PM]
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch35.htm
if (!gotQuery) { connect(); } // Return the stream. return fromHandler; } /** * This method decodes the URL encoded format. * i.e. %XX -> char and + to space * @param decode The String to decode. * @return The decoded String. */ protected String decodeURL(String decode) { StringBuffer decoded = new StringBuffer(); char nextChar; String encString; Integer encInteger; // Go through the String character by character. for(int index=0; index < decode.length(); index++) { // Get the next character in the String. nextChar = decode.charAt(index); // If the character is +, then convert it to // a space. if (nextChar == '+') { decoded.append(" "); } // If the character is a %, then the next two // characters store the value of the encoded // character. else if (nextChar == '%') { // Create an Integer object containing the // integer value of the next two characters // in the string, assuming a base 16 notation. encInteger = Integer.valueOf( decode.substring(index+1,index+3),URL_BASE); // Increment our counter by 2 - we just read // two characters. index += 2; // Return the int value within the Integer // and then cast that into a char type. nextChar = (char)encInteger.intValue(); // Add the coded character. decoded.append(nextChar); } // Otherwise, just add the character. else { decoded.append(nextChar); } } // Return the decoded string. return decoded.toString(); } }
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch35.htm
The whoisURLConnection constructor merely makes the appropriate call to the URLConnection superclass, passing it the URL object. The connect() method is where all of the work is done. First, it creates a number of streams for reading in data and passing it back to your Java application. Next, it uses various URL class methods to parse the URL, supporting the formats identified in Table 35.1. The default values of the host (for example, "rs.internic.net") and port (i.e., "35") are stored within static final variables. Then, the connect() method opens a TCP socket connection to the remote system, sends the query, and prepares to read the response. It reads in the query response, embedding it within HTML. Finally, connect() closes the streams and TCP socket. The getContentType() method is used to indicate that the returned data is an HTML document. getInputStream() returns the PipedOutputStream, into which this protocol handler has pushed the formatted data. The Java application uses this stream to access the data obtained by the new protocol handler. The whoisURLConnection has an additional protected method called decodeURL(). Certain characters, such as spaces and other special characters, cannot be represented literally within URLs, and these characters are encoded with a special format. For example, a space is encoded as %20. Since your WHOIS query may need to use such characters, the WHOIS protocol handler must have a method to decode this data. decodeURL() is called from the connect() method, and examines a string byte-by-byte. When it encounters a percent-sign, it uses the next two characters as the Unicode value to the encoded char-acter.
Listing 35.4 Handler.java // This is the package identified for this protocol handler. package ORG.netspace.dwb.protocol.whois; import java.net.*; // Import the package names used.
/** * This class is a subclass of URLStreamHandler and provides * an implementation of the abstract openConnection() method * to support the "whois" scheme. * @author David W. Baker * @version 1.0 */ public class Handler extends URLStreamHandler { /** * Given a URL return an appropriate URLConnection. * @param requestedURL The URL instance to contact. * @return The connection to the resource. */ public synchronized URLConnection openConnection(URL requestedURL) { return new whoisURLConnection(requestedURL); } }
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch35.htm
Once a protocol handler has been installed, the following additional steps are necessary to use it within your HotJava browser. Note JavaSoft makes the HotJava browser and instructions for its installation available at <URL:http://www.javasoft.com/java.sun.com/HotJava/CurrentRelease/installation.html>.
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch35.htm
Listing 35.5 FetchWhois.java import java.net.*; import java.io.*; // Import the package names used.
/** * This is an application which uses our new whois * protocol handler to obtain information. * @author David W. Baker * @version 1.1 */ public class FetchWhois { /** * The method launches the application. * @param args Arguments which are the query string. */ public static void main (String args[]) { if (args.length < 1) { System.err.println( "usage: java FetchWhois query string"); System.exit(1); } FetchWhois app = new FetchWhois(args); } /** * This constructor does all of the work of obtaining * the data from the server. * @param args The tokens of the query string. */ public FetchWhois(String args[]) { String encodedString; // Hold the URL encoded query. String nextLine; // Line from the handler. URL whoisURL; // URL to whois resource URLConnection whoisAgent; // Connection to whois. DataInputStream input; // Stream from whois. // Create a buffer to place in all of the query // string tokens. StringBuffer buffer = new StringBuffer(); // Append all of the tokens to the buffer. for(int index = 0; index < args.length; index++) { buffer.append(args[index]); if (index < args.length-1) {
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch35.htm
buffer.append(" "); } } // URL encode the query buffer. encodedString = URLEncoder.encode(buffer.toString()); // Set the factory to register the whois handler. URL.setURLStreamHandlerFactory(new whoisUSHFactory()); try { // Create the whois URL object. whoisURL = new URL("whois:" + encodedString); // Open the connection. whoisAgent = whoisURL.openConnection(); // Get an input stream from the whois server. input = new DataInputStream(whoisAgent.getInputStream()); // Print out the data line-by-line. while((nextLine = input.readLine()) != null) { System.out.println(nextLine); } input.close(); // Close the stream. } catch(MalformedURLException excpt) { System.err.println("Mailformed URL: " + excpt); } catch(IOException excpt) { System.err.println("Failed I/O: " + excpt); } } } /** * This class implements the URLStreamHandlerFactory * interface to register the whois protocol handler. * @see java.net.URLStreamHandlerFactory */ class whoisUSHFactory implements URLStreamHandlerFactory { /** * This method returns the protocol handler to the * calling object. * @param scheme The URL scheme to be obtained. * @return The protocol handler. * @see java.net.URLStreamHandlerFactory#createURLStreamHandler */ public URLStreamHandler createURLStreamHandler(String scheme) { // Make sure that this is for a whois URL if (scheme.equalsIgnoreCase("whois")) { // If so, create the handler and return it. return new ORG.netspace.dwb.protocol.whois.Handler(); // Otherwise print an error message and return null. } else { System.err.println("Unknown protocol: " + scheme); return null; } } }
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch35.htm
Running FetchWhois
Compile FetchWhois with javac and then try running it by using the following command. The program should print an HTML document that contains the same data as when you used the whois URL within HotJava. java FetchWhois internic.net HotJava will soon support dynamically downloaded protocol handlers, enabling a much more flexible use of this feature. Manually installing these extensions will continue to be a useful alternative.
More on URLStreamHandlerFactory
The URLStreamHandlerFactory class is a means of passing out specific protocol handlers for each supported protocol handler. In Listing 35.5, you used a custom factory called whoisUSHFactory. This factory supported only one scheme, whois. However, factories can be much more general, and can support both custom protocol handlers and those provided within the JDK. Listing 35.6 shows another application, ParseURL, which uses its own URLStreamHandlerFactory implementation. This application will take a series of URLs, including new URL schemes, as command line arguments and then print out how the URL class parses them. This demonstrates how the URL class deals with different forms of URLs.
This simple application demonstrates how a custom URLStreamHandlerFactory can be used to provide customwritten protocol handlers as well as those within the JDK. This program uses a dummy protocol handler so that it can construct a URL object to unknown protocols and
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch35.htm
* parse out that URL. * @author David W. Baker * @version 1.0 */ public class ParseURL { /** * This method allows ParseURL to be executed as a * stand-alone application. It takes a series of URLs * as arguments and then prints out the various portions * of those URLs. * @param args The command-line arguments. */ public static void main(String[] args) { URL urlToParse; // We use this to parse each URL. // First, we must use the custom URLStreamHandlerFactory // which is included below. URL.setURLStreamHandlerFactory(new ParseURLFactory()); // Go through each command-line argument. for(int index = 0; index < args.length; index++) { try { // Create the URL object. urlToParse = new URL(args[index]); // Print the URL to be parsed. System.out.println("Parsing URL: " + args[index]); // Print out the various portions of the URL. System.out.println("\tScheme:\t" + urlToParse.getProtocol()); System.out.println("\tHost:\t" + urlToParse.getHost()); System.out.println("\tPort:\t" + urlToParse.getPort()); System.out.println("\tFile:\t" + urlToParse.getFile()); System.out.println("\tRef:\t" + urlToParse.getRef()); System.out.println("=============================="); } catch(MalformedURLException excpt) { // We should catch this exception, but our custom // URLStreamHandlerFactory will ensure such is // never thrown. System.err.println("Malformed URL: " + args[index]); System.err.println(excpt); } } } } /** * This custom URLStreamHandlerFactory allows us to create * URL objects that will use the JDK protocol handlers for * known schemes. If the scheme is not one of those within * the JDK, we create an instance of our DummyHandler and * return that. If this factory returned null instead, * a MalformedURLException would be thrown for unknown * schemes. */ class ParseURLFactory implements URLStreamHandlerFactory { public URLStreamHandler createURLStreamHandler(String
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch35.htm (13 of 15) [8/14/02 10:57:46 PM]
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch35.htm
// // // if
scheme) { First, go through the JDK's protocol handlers for known URL schemes. All of these protocol handlers reside within sun.net.www.protocol.<scheme>. (scheme.equalsIgnoreCase("http")) { return new sun.net.www.protocol.http.Handler();
} if (scheme.equalsIgnoreCase("ftp")) { return new sun.net.www.protocol.ftp.Handler(); } if (scheme.equalsIgnoreCase("gopher")) { return new sun.net.www.protocol.gopher.Handler(); } if (scheme.equalsIgnoreCase("file")) { return new sun.net.www.protocol.file.Handler(); } if (scheme.equalsIgnoreCase("mailto")) { return new sun.net.www.protocol.mailto.Handler(); } // If it is none of the above, create an instance of our // dummy URLStreamHandler. return new DummyHandler(); } } /** * This is a dummy protocol handler. This handler will allow * us to create a URL object for this scheme, but we won't * be able to retrieve any data through it. */ class DummyHandler extends URLStreamHandler { /** * This method will only return null. Data cannot be * obtained from this handler. * @param u The URL to obtain data from. * @return A null URLConnection. */ public URLConnection openConnection(URL u) { return null; } }
ParseURL also elucidates how the MalformedURLException is created for unknown schemes. If the factory being used with your application doesn't return a protocol handler for a scheme, any attempt to create an URL instance with such a scheme will throw a MalformedURLException. In ParseURL, however, you prevent this from occurring so that you can see how new URLs are parsed. ParseURL has three classes, ParseURL, ParseURLFactory, and DummyHandler. The first is the only public class and is used to invoke the application. The main() method within the ParseURL class sets the URLStreamHandlerFactory to your custom one, ParseURLFactory. It then iterates through the command line arguments, creating URL instances for each and then printing out the portions of each URL. The ParseURLFactory is your custom factory. It inspects the scheme of the URL instance to be created. If the scheme happens to be one of the standard schemes supported within the JDK, it returns the corresponding protocol handler. All of these protocol handlers within the JDK are contained within the package sun.net.www.protocol.scheme. If the scheme is not one of the standard ones, ParseURLFactory returns an instance of DummyHandler, rather than null. If null were returned, a MalformedURLException would be thrown if you attempted to create any URLs with non-standard schemes.
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch35.htm (14 of 15) [8/14/02 10:57:46 PM]
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch35.htm
The DummyHandler class does very little, as its name suggests. This class allows you to create URL instances for non-standard schemes. You can then parse such URLs with methods like getHost() and getFile() without having to create a protocol handler for each new scheme. However, DummyHandler does not implement any connection mechanism. Thus, you will not be able to obtain any data through non-standard URL instances that use the DummyHandler. To test your ParseURL application, first compile it with the Java compiler. Then execute it with the Java interpreter with one or more URLs as arguments. For instance, invoking it with: java ParseURL http://www.yahoo.com/ newscheme:newstuff returns the following information: Parsing URL: http://www.yahoo.com/ Scheme: http Host: www.yahoo.com Port: -1 File: / Ref: null ============================== Parsing URL: newscheme:newstuff Scheme: newscheme Host: Port: -1 File: /newstuff Ref: null ==============================
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f35-1.gif
CONTENTS
G
Writing Content Handlers H Step One: Decide upon a Package Name H Step Two: Create the Directories H Step Three: Set Your CLASSPATH H Step Four: Write the Content Handler H Step Five: Compile the Source Using Content Handlers with HotJava H Step One: Disable Special MIME Handling H Step Two: Update the PROPERTIES File H Step Three: Run HotJava Using Content Handlers with Your Own Applications H Start FetchFuddify H The ContentHandlerFactory Implementation H Running the Application
Files on the Internet come in various formats, each of which is used to convey specific information. Thereare different image file formats, sound clips, video information, and HTML pages. When these documents are transmitted on the Web with the HTTP protocol, a particular MIME content type is used in order to identify how that file should be interpreted. New document formats are constantly being introduced to the World Wide Web. However, before you can use these new formats, your browser or other applications must understand how to interpret them. Extensibility is part of the nature of the Java execution environment and the HotJava browser. To manage new MIME types, Java and HotJava can be extended through content handlers. Content handlers are Java's way of dealing with various data formats, such as text files, images, and sounds. By creating new content handlers, additional data types can be processed and rendered. They empower you to add new functionality to your Web browser and quickly develop applications to utilize new file formats.
formats. The process of creating new content handlers is quite similar to creating protocol handlers. If you have read the previous chapter, some of these instructions will seem quite familiar. As an example, this chapter demonstrates a content handler that processes plain text documents, overriding the existing handling. Note The example in this chapter provides the somewhat frivolous task of making incoming text files appear as though spoken by a famous bald cartoon character inclined towards hunting rabbits.
The process of creating protocol handlers is very similar to that of content handlers, and is described in the section "Writing a Procotol Handler," in Chapter 35, "Adding Additional Protocols to HotJava."
The content handler class must be placed into a directory that corresponds to the package name. Such directories usually reside within a directory called classes in your home directory. For Windows NT and Windows 95 users, the following sequence of commands accomplishes this at the command prompt: Note If you have previously installed other content handlers, protocol handlers, or personal Java classes, you may have already created some of the following directories.
mkdir classes\ORG\netspace\dwb mkdir classes\ORG\netspace\dwb\content mkdir classes\ORG\netspace\dwb\content\text For UNIX users, the analogous commands are: cd~ mkdir mkdir mkdir mkdir mkdir mkdir
The content handler must be a class that extends java.net.ContentHandler. It must also have the same name as the subtype of the MIME content-type it processes. That is, for image/gif, the class should be called gif, while my example that overrides the normal plain/text handler should be named text. The class must have a getContent() method that takes a URLConnection as an argument and returns a generic Object. For now, HotJava supports the following returned Object instances:
G G G G
A String object that appears as plain text within the HotJava window An instance of sun.awt.image.InputStreamImageSource, allowing HotJava to load the image An InputStream object that opens the Save to Disk dialog box A Thread instance that launches an external helper application
The code for the example used in this chapter is shown in listing 36.1. This content handler has only one method-getContent(). It obtains an InputStream from the URLConnection object and then enters an infinite loop. Within the loop, it reads the incoming characters and makes a number of substitutions, altering the text to appear as though spoken by our cartoon friend. The filtered characters are placed into a StringBuffer() object. Once the last character is read, the read() method returns -1, and the content handler breaks from the loop. It closes the InputStream and then returns a String object. Note If there is an exception, the method returns a String providing information about the problem.
Listing 36.1 plain.java // This is the package identified for this content handler. package ORG.netspace.dwb.content.text; import java.lang.*; import java.net.*; import java.io.*; // Import the package names used.
/** * This is a text/plain content handler which "fuddifies" * the text it receives. * @author David W. Baker * @version 1.1 * @see sun.net.ContentHandler */ public class plain extends ContentHandler { // Stream to receive text/plain file from. private InputStream input; // Some standard replacement strings. private static final String QUIET = "(be vewy quiet, "; private static final String HEH = ", eheheheh."; private static final String SCREWY = "? Awe you scwewy?"; private static final String RASCAL = ", you wascal!"; private static final String MISCREANT = ", you miscweant:";
/** * This method returns an Object containing the * processed content from the given URLConnection. * @param contentConn Connection used to obtain the content. * @return The content. * @see sun.net.ContentHandler#getContent */ public Object getContent(URLConnection contentConn) { // Create a buffer to store the filtered data. StringBuffer fuddBuff = new StringBuffer(); int intChar; // A int representation of a char. char nextChar; // A char. try { // Get the input. input = contentConn.getInputStream(); // Loop infinitely. filter: while(true) { // Read in next character. intChar = input.read(); // Make sure we aren't at the end. if (intChar == -1) { break filter; // Break if end. } // Convert it to a char. nextChar = (char)intChar; // Substitute "(" for QUIET if (nextChar == '(') fuddBuff.append(QUIET); // Substitute "W" for "L" else if (nextChar == 'L') fuddBuff.append('W'); // Substitute "w" for "l" else if (nextChar == 'l') fuddBuff.append('w'); // Substitute "R" for "W" else if (nextChar == 'R') fuddBuff.append('W'); // Substitute "r" for "w" else if (nextChar == 'r') fuddBuff.append('w'); // For periods at the end of the file or periods // followed by whitspace, substitute HEH. else if (nextChar == '.') { intChar = input.read(); if (intChar == -1) { fuddBuff.append(HEH); break filter; // Break if end. } nextChar = (char)intChar; if (nextChar == ' ') fuddBuff.append(HEH + " "); else fuddBuff.append("." + nextChar); } // For ? the end of the file or ? // followed by whitspace, substitute SCREWY. else if (nextChar == '?') { intChar = input.read(); if (intChar == -1) { fuddBuff.append(SCREWY);
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch36.htm (5 of 11) [8/14/02 10:57:52 PM]
break filter;
// Break if end.
} nextChar = (char)intChar; if (nextChar == ' ') fuddBuff.append(SCREWY + " "); else fuddBuff.append("?" + nextChar); } // For ! at the end of the file or ! // followed by whitspace, substitute RASCAL. else if (nextChar == '!') { intChar = input.read(); if (intChar == -1) { fuddBuff.append(RASCAL); break filter; // Break if end. } nextChar = (char)intChar; if (nextChar == ' ') fuddBuff.append(RASCAL + " "); else fuddBuff.append("!" + nextChar); } // For : at the end of the file or : // followed by whitspace, substitute MISCREANT. else if (nextChar == ':') { intChar = input.read(); if (intChar == -1) { fuddBuff.append(MISCREANT); break filter; // Break if end. } nextChar = (char)intChar; if (nextChar == ' ') fuddBuff.append(MISCREANT + " "); else fuddBuff.append(":" + nextChar); } else fuddBuff.append(nextChar); } input.close(); } catch(IOException excpt) { return "Unable to load document: " + contentConn.getURL(); } return fuddBuff.toString(); } }
Be sure to leave the .class file within the bottom "text" directory. Tip If you choose to create the plain.java file somewhere else, you use the -d option to the Java compiler in order to automatically place the .class file into the proper place. For example: javac -d classes/ORG/netspace/dwb/content/text plain.java
java.content.handler.pkgs=COM.company.content|ORG.netspace.dwb.content
Note When editing the HotJava properties file, be sure to use a text editor or, if you are using a word processor, save the file as text.
Listing 36.2 FetchFuddify.java import java.net.*; import java.io.*; // Import package names used.
/** * This is an application which utilizes the new * text/plain content handler which "fuddifies" * the text. * @author David W. Baker * @version 1.1 */ public class FetchFuddify { /** * This method starts the application. * @param args The program arguments - should be URL. */ public static void main (String args[]) { // Check the arguments. if (args.length != 1) { System.err.println("usage: " + "java FetchFuddify <url of Fudd document>"); System.exit(1); }
// Create an instance of FetchFuddify to do its stuff. FetchFuddify app = new FetchFuddify(args[0]); } /** * This constructor does all of the work of obtaining * the data with the appropriate content handler and * sending it to standard output. * @param url The URL to obtain. */ public FetchFuddify(String url) { URL fuddURL; // URL object to resource. URLConnection fuddConn; // Connection to resource. Object fuddObject; // Object returned. // Register the content handler with our ow // factory. URLConnection.setContentHandlerFactory( new fuddifyCHFactory()); try { // Create the URL object with the command line // argument used. fuddURL = new URL(url); // Open the connection. fuddConn = fuddURL.openConnection(); // Get the content. fuddObject = fuddConn.getContent(); // Convert the content to a String and print it. System.out.println(fuddObject.toString()); } catch(MalformedURLException excpt) { System.err.println("Mailformed URL: " + excpt); } catch(IOException excpt) { System.err.println("Failed I/O: " + excpt); } } } /** * This class implements the ContentHandlerFactory * interface to register our own content handler. * @see java.net.ContentHandlerFactory */ class fuddifyCHFactory implements ContentHandlerFactory { /** * This method returns our own customer content * handler when given a "text/plain" content type. * @param contenttype MIME type - should be "text/plain". * @return The content handler to use. * @see java.net.ContentHandlerFactory#createContentHandler */ public ContentHandler createContentHandler(String contenttype) { // Ensure the content type is "text/plain". if (contenttype.equalsIgnoreCase("text/plain")) { // Create an instance of our content handler.
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch36.htm (9 of 11) [8/14/02 10:57:52 PM]
return new ORG.netspace.dwb.content.text.plain(); } // Otherwise, print an error message and return null. System.err.println("Unknown data type: " + contenttype); return null; } }
Start FetchFuddify
The main() method checks to see that the program was invoked with a single argument, which corresponds to the URL of a text file to filter. Then it creates a FetchFuddify object, passing it the String command line argument. The constructor performs the essential task in using a new content handler: invoking the static method of the URLConnection class, setContentHandlerFactory(). Factories should be a familiar concept, this time allowing the URLConnection class to choose an appropriate content handler. The setContentHandlerFactory takes an object that implements the java.net.ContentHandlerFactory interface. This example's implementation, fuddifyCHFactory, is described next in "The ContentHandlerFactory Implementation." The constructor then creates an URL object and opens a connection to the resource. It calls the getContent() method of the URLConnection class, which causes the code of the content handler to be invoked. getContent() returns an Object, which the constructor converts to a String with the toString() method and prints to standard output.
- Any appwication that expowts wemote objects must be awwowed by the SecuwityManagew to use SewvewSockets to wisten fow and accept incoming socket connections, eheheheh. - Appwets may not expowt wemote objects since theSecuwityManagew pwevents using SewvewSocket, eheheheh. This wiww be suppowted in a futuwe update. It is promised that with the 1.0 release version of HotJava, dynamically downloaded content handlers will be supported. Once realized, this will allow HotJava to be extended on demand with little effort from the enduser. When you encounter a new document type, HotJava will automatically download and install the new content handler necessary to render the data.
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f36-1.gif
CONTENTS
G G
G G
Designing Multi-User Applications Adding Socket-Based Access to Multi-User Applications H Creating a Socket-Based Server H Sending Messages over Sockets Other Issues When Dealing with Sockets Adding RMI Access to Multi-User Applications
One thing that attracts thousands of people to the Internet is its interactive nature. The popularity of multi-user chat programs like IRC and various multi-user games like MUDs (Multi-User Domain/Dungeon/Dimension) illustrates that fact very clearly. In the beginning, multi-user programs were all text-based. There are many early multi-user programs that predate the Internet. Many multi-user programs are still text-based, but they are beginning to get graphical front ends (another form of encapsulation!). Other programs have grown out of single-user versions. Game manufacturers, for instance, have begun to support Internet connections. This allows game users to play against each other over the Internet. Java adds something that these off-the-shelf games don't really have. You can download a Java game and play it on any Java-enabled platform immediately. You can even create a game server that manages the connections between players. Whenever you add new games to the server, the players download new Java applets that present the user interface for the new games. The multi-user paradigm isn't restricted to games, of course. You can set up various kinds of collaborative applications, so people can solve problems and complete tasks from separate parts of the world.
information flows only from the client to the server and back. Figure 37.1 illustrates this difference. Figure 37.1 : Information f. between users a multi-user application. When you design a multi-user application, you should try to ignore the network if possible. You can't fully discount the network, of course. You have to remember that there is a high amount of overhead between the client and the server. You want to minimize the number of interactions between the client and server. Tip If your server needs to invoke methods in the client, define the client as an interface. This allows you to implement the client side in different ways, while not tying the server to a particular set of client implementations.
When you create your application, you first create the server, and a client interface if needed. Next, you create encapsulations for the various network protocols and remote object systems you want to support. Figure 37.2 shows an example configuration, where the server can be accessed through TCP sockets and RMI. Figure 37.2 : Through encapsulation, your application can support multiple protocols. Tip As far as the server is concerned, the networking protocol is the user interface for the server. You are really just following the principle of separating the application from the user interface.
Listing 37.1 shows a server for a simple chat system. The server relays chat messages to the other users, and notifies the users whenever a new client enters the system or an existing client leaves.
Listing 37.1 Source Code for ChatServer.java package chat.server; import java.util.Vector; import java.util.Hashtable; import java.util.Enumeration; // This is a simple chat application. It allows clients to enroll // under a particular name, and send messages to each other. // Messages are sent to the client via the ChatClient interface. public class ChatServer
{ // clients is a table that maps a client name to a ChatClient // interface protected Hashtable clients; public ChatServer() { clients = new Hashtable(); } // Add client adds a new client to the system and tells the other // clients about the new client. public synchronized void addClient(String name, ChatClient client) { // If the client picks a name that is already here, // disconnect the new client, let the old one keep its name. if (clients.get(name) != null) { client.disconnect(); return; } // Add the new client to the table clients.put(name, client); // Tell the other clients about this new client sendEnterMessage(name); } public synchronized void removeClient(String name) { ChatClient client = (ChatClient) clients.get(name); if (client != null) { clients.remove(name); sendLeaveMessage(name); } } // removeClient removes a client from the chat system and tells // the other clients about it. public synchronized void removeClient(ChatClient client) { // We remove by ChatClient, not by name. We have to enumerate through // all the clients to find out the name of this client.
Enumeration e = clients.keys(); while (e.hasMoreElements()) { String key = (String) e.nextElement(); // If we found the right name for this client, remove them and // tell everyone about it. if (clients.get(key) == client) { clients.remove(key); sendLeaveMessage(key); } } } // sendChat is called by a client to send a message to the // other clients public synchronized void sendChat(String name, String message) { Enumeration e = clients.elements(); // Enumerate through all the clients and send them the chat message // Note that this will send a message back to the original // sender, too. while (e.hasMoreElements()) { ChatClient client = (ChatClient) e.nextElement(); client.incomingChat(name, message); } } // sendEnterMessage tells all the clients when a new client // has arrived public synchronized void sendEnterMessage(String name) { Enumeration e = clients.elements(); // Enumerate through all the clients and tell them about // the new client while (e.hasMoreElements()) { ChatClient client = (ChatClient) e.nextElement(); client.userHasEntered(name); } }
// sendLeaveMessage tells all the clients that a client has left public synchronized void sendLeaveMessage(String name) { Enumeration e = clients.elements(); // Enumerate through all the clients and tell them who left while (e.hasMoreElements()) { ChatClient client = (ChatClient) e.nextElement(); client.userHasLeft(name); } } // getUserList returns a list of all the users on the system public synchronized String[] getUserList() { Enumeration e = clients.keys(); // Create an array to hold the user names String[] nameList = new String[clients.size()]; // Copy the user names into the nameList array int i = 0; while (e.hasMoreElements()) { nameList[i++] = (String) e.nextElement(); } // Return the name list return nameList; } }
Since this server needs to invoke methods on the client, it defines a ChatClient interface that all clients to this system must implement. Listing 37.2 shows this ChatClient interface.
Listing 37.2 Source Code for ChatClient.java package chat.server; public interface ChatClient { public void incomingChat(String who, String chat);
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch37.htm (5 of 29) [8/14/02 10:58:01 PM]
public void userHasEntered(String who); public void userHasLeft(String who); public void disconnect(); }
Again, it is important to note that there is no mention of a specific networking protocol. These two classes represent the core application. If you design all your applications this way, you will have no trouble adding other ways to access your application.
Listing 37.3 Source Code for TCPChatServer.java package chat.tcp.server; import java.net.*; import java.io.*; import chat.server.*; // This class implements a simple TCP server that listens // for incoming connections. It creates a TCPChatClient object // to handle the actual connection. public class TCPChatServer extends Object implements Runnable { // serverSocket is the socket we are listening on protected ServerSocket serverSocket; // server is a reference to the application object, which we // pass to the TCPChatClients protected ChatServer server; protected Thread myThread; public TCPChatServer(ChatServer server, int port) throws IOException { serverSocket = new ServerSocket(port); this.server = server; } public void run() { while (true) { try { // Accept a new connection Socket newConn = serverSocket.accept(); // Create a client to handle the connection TCPChatClient newClient = new TCPChatClient( server, newConn); // Start the client (it's runnable) newClient.start(); } catch (Exception e) {
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch37.htm (7 of 29) [8/14/02 10:58:01 PM]
} } } public void start() { myThread = new Thread(this); myThread.start(); } public void stop() { myThread.stop(); myThread = null; } }
Listing 37.4 Source Code for TCPChatClient.java package chat.tcp.server; import java.io.*; import java.net.*; import chat.server.*; import chat.tcp.common.TCPChatMessageTypes; // // // // This class acts like a client of the ChatServer application. It translates messages from a TCP socket into requests for the chat server, and translates method invocations from the server into TCP messages.
public class TCPChatClient extends Object implements ChatClient, Runnable { // server is the ChatServer application we are a client of protected ChatServer server; // clientSock is the socket connection to the user protected Socket clientSock; // inStream and outStream are Data streams for the socket. This allows // us to send information in forms other than an array of bytes protected DataInputStream inStream; protected DataOutputStream outStream; // clientName is the name the user wants to be known by protected String clientName; protected Thread myThread; public TCPChatClient(ChatServer server, Socket clientSock) throws IOException { this.server = server; this.clientSock = clientSock; // get data streams to the socket inStream = new DataInputStream( clientSock.getInputStream()); outStream = new DataOutputStream(
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch37.htm (9 of 29) [8/14/02 10:58:01 PM]
clientSock.getOutputStream()); // The first thing that the user sends us is // the name they want to use clientName = inStream.readUTF(); // Add ourself to the server application server.addClient(clientName, this); } // // // // The next few methods implement a really simple messaging protocol: 4 byte Integer message type 4 byte message length <message length> bytes of data
// userHasEntered is called by the server whenever there's a new user // the data part of the message is just the name of the user who has // entered. public void userHasEntered(String who) { try { // Write the message type outStream.writeInt(TCPChatMessageTypes.ENTER); // Write the message length outStream.writeInt(who.length()); // Write the user's name outStream.writeBytes(who); } catch (Exception e) { server.removeClient(this); } } // userHasLeft is called by the server whenever there's a new user // the data part of the message is just the name of the user who has // left. public void userHasLeft(String who) { try { outStream.writeInt(TCPChatMessageTypes.LEAVE); outStream.writeInt(who.length()); outStream.writeBytes(who); } catch (Exception e) { server.removeClient(this); } } // incomingChat is called by the server whenever someone sends a message. // The data part of the message has three parts:
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch37.htm (10 of 29) [8/14/02 10:58:01 PM]
// // // //
the length of the name of the person sending the message (the length value itself is a 4-byte integer) the name of the person sending the message the chat message public void incomingChat(String who, String chat) { try { outStream.writeInt(TCPChatMessageTypes.CHAT); outStream.writeInt(who.length() + chat.length() + 4); outStream.writeInt(who.length()); outStream.writeBytes(who); outStream.writeBytes(chat); } catch (Exception e) { server.removeClient(this); } }
// disconnect is called by the server when the client has // been disconnected from the server. We just close down the // socket and stop this thread. public void disconnect() { try { clientSock.close(); } catch (Exception e) { } stop(); }
The rest of the TCPChatClient class deals with messages coming in from the client. The run method reads in an integer message type as the first part of the message. It then calls an appropriate method to handle the rest of the message. The handleChatMessage method reads an incoming chat message and passes it on to the server to be distributed to the rest of the clients. Because this protocol is extremely simple, there are no other message types defined. Because you may want to add protocol types at some point, the server should be able to receive messages it does not understand without completely dying. In this case, because the length of the message is always sent after the message type, the skipMessage method can read in and then ignore any message that the server doesn't understand. You should always provide some sort of safety mechanism like this. Someone may take this server and really expand it and then write a nice client for it. If that client then accesses an original version of the server, it should still be able to safely use the original version without the server dying. If you decide to change the contents of a particular message, you should assign that message a new message type and continue to support the old type. If you added a date field to the incoming chat message, you can't expect all the
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch37.htm (11 of 29) [8/14/02 10:58:01 PM]
clients to suddenly support the new field. You should be able to handle incoming chat messages with or without the date field. One of the best ways to handle this is by adding a second message type. Version numbers are another common device used for handling multiple formats for a particular message. When the client connects to the server, it tells the server which version of the messaging protocol it uses. If it uses version 2, for instance, it will be sending a date field in every chat message, while version 1 clients don't send the date field (see Listing 37.5).
Listing 37.5 Source Code for TCPChatClient.java (continued) // handleChatMessage reads an incoming chat message from the user and // sends it to the server. The data part of the message is just the // chat message itself. public void handleChatMessage() throws IOException { // Get the message length int length = inStream.readInt(); byte[] chatChars = new byte[length]; // Read the chat message inStream.readFully(chatChars); String message = new String(chatChars, 0); // Send the chat message to the server server.sendChat(clientName, message); } // If we get a message we don't understand, skip over it. That's // why we have the message length as part of the protocol. public void skipMessage() throws IOException { int length = inStream.readInt(); inStream.skipBytes(length); } public void run() { while (true) { try { // Read the type of the next message
int messageType = inStream.readInt(); switch (messageType) { // If it's a chat message, read it case TCPChatMessageTypes.CHAT: handleChatMessage(); break; // For any messages whose type we don't understand, skip the message default: skipMessage(); return; } } catch (Exception e) { server.removeClient(clientName); return; } } } public void start() { myThread = new Thread(this); myThread.start(); } public void stop() { myThread.stop(); myThread = null; } }
The TCPChatClient class uses message types defined in an interface called TCPChatMessageTypes, which is shown in Listing 37.6.
Listing 37.6 Source Code for TCPChatMessageTypes.java package chat.tcp.common; public interface TCPChatMessageTypes { public static final int CHAT = 1; public static final int ENTER = 2;
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch37.htm (13 of 29) [8/14/02 10:58:01 PM]
The user-side client program is pretty simple to write. It needs to connect to the TCPChatClient and pass chat messages to it. It must also read any messages sent by the server. Since the user-side client is reading from two different places, it needs at least two threads. The RunTCPClient class, shown in Listing 37.7, uses a second class called TCPChatReader to read messages coming from the TCPChatServer. The TCPChatReader class calls methods in RunTCPClient to actually display the results of a message from the server. In this simple example, the RunTCPClient class just prints the messages to System.out. If you were making a chat applet, however, you would display incoming messages differently. You could still use the TCPChatReader with a chat applet.
Listing 37.7 Source Code for RunTCPClient.java import java.net.*; import java.io.*; import chat.server.*; import chat.tcp.common.TCPChatMessageTypes; import chat.tcp.client.*; // Class is a client for the TCPChatServer object. It reads chat // messages from System.in and relays them to the chat server. // It displays any information coming back from the chat server. public class RunTCPClient extends Object implements ChatClient { public RunTCPClient() { } // Display a message when there's a new user public void userHasEntered(String who) { System.out.println("--- "+who+" has just entered ---"); } // Display a message when someone exits public void userHasLeft(String who) { System.out.println("--- "+who+" has just left ---"); } // Display a chat message
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch37.htm (14 of 29) [8/14/02 10:58:01 PM]
public void incomingChat(String who, String chat) { System.out.println("<"+who+"> "+chat); } public void disconnect() { System.out.println("Chat server connection closed."); System.exit(0); } public static void main(String args[]) { int port = 4321; // Allow the port to be set from the command line (-Dport=4567) String portStr = System.getProperty("port"); if (portStr != null) { try { port = Integer.parseInt(portStr); } catch (Exception ignore) { } } // Allow the server's host name to be specified on the command // line (-Dhost=myhost.com) String hostName = System.getProperty("host"); if (hostName == null) hostName = "localhost"; Listing 37.7 Continued try { // Connect to the TCPChatServer program Socket clientSocket = new Socket(hostName, port); DataOutputStream chatOutputStream = new DataOutputStream( clientSocket.getOutputStream()); DataInputStream chatInputStream = new DataInputStream( clientSocket.getInputStream()); DataInputStream userInputStream = new DataInputStream(System.in);
System.out.println("Connected to chat server!"); // Prompt the user for a name System.out.print("What name do you want to use? "); System.out.flush(); String myName = userInputStream.readLine(); // Send the name to the server chatOutputStream.writeUTF(myName); RunTCPClient thisClient = new RunTCPClient(); // Start up a reader thread that reads messages from the server TCPChatReader reader = new TCPChatReader( thisClient, chatInputStream); reader.start(); // Read input from System.in while (true) { String chatLine = userInputStream.readLine(); sendChat(chatOutputStream, chatLine); } } catch (Exception e) { System.out.println("Got exception:"); e.printStackTrace(); System.exit(1); } } // sendChat sends a chat message to the TCPChatServer program public static void sendChat(DataOutputStream outStream, String line) throws IOException { outStream.writeInt(TCPChatMessageTypes.CHAT); outStream.writeInt(line.length()); outStream.writeBytes(line); } }
The TCPChatReader class reads messages from the chat server. Rather than display the messages itself, it
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch37.htm (16 of 29) [8/14/02 10:58:01 PM]
invokes methods in another object. This enables you to customize the display of information without changing the TCPChatReader class. Listing 37.8 shows the TCPChatReader class.
Listing 37.8 Source Code for TCPChatReader.java package chat.tcp.client; import java.io.*; import chat.server.*; import chat.tcp.common.TCPChatMessageTypes; // This class sets up a thread that reads messages from the // TCPChatServer and then invokes methods in an object // implementing the ChatClient interface. public class TCPChatReader extends Object implements Runnable { protected ChatClient client; protected DataInputStream inStream; protected Thread myThread; public TCPChatReader(ChatClient client, DataInputStream inStream) { this.client = client; this.inStream = inStream; } public void run() { while (true) { try { int messageType = inStream.readInt(); // Look at the message type and call the appropriate method to // read the message. switch (messageType) { case TCPChatMessageTypes.CHAT: readChat(); break; case TCPChatMessageTypes.ENTER: readEnter(); break;
case TCPChatMessageTypes.LEAVE: readLeave(); break; default: skipMessage(); break; } } catch (Exception e) { client.disconnect(); } } } public void start() { myThread = new Thread(this); myThread.start(); } public void stop() { myThread.stop(); myThread = null; } // // // // // // readChat has the toughest job in reading the message, and it's not really that tough. The message length is the total length of the bytes sent. It is followed by the length of the name of the person sending the chat, and then the name itself. This method has to compute the length of the chat string by subtracting the length of the name, and 4 bytes for the name length.
public void readChat() throws IOException { // Get the total message length int length = inStream.readInt(); // Get the length of the name of the person sending the chat int whoLength = inStream.readInt(); // Compute the length of the chat, subtract the length of the name, // and 4 bytes for the length that was sent. int chatLength = length - whoLength - 4; // Read in the name of the person sending the chat byte[] whoBytes = new byte[whoLength];
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch37.htm (18 of 29) [8/14/02 10:58:01 PM]
inStream.readFully(whoBytes); String whoString = new String(whoBytes, 0); // Read in the chat byte[] chatBytes = new byte[chatLength]; inStream.readFully(chatBytes); String chatString = new String(chatBytes, 0); // Pass the chat to the object that will display it client.incomingChat(whoString, chatString); } public void readEnter() throws IOException { int length = inStream.readInt(); byte[] whoBytes = new byte[length]; inStream.readFully(whoBytes); String whoString = new String(whoBytes, 0); client.userHasEntered(whoString); } public void readLeave() throws IOException { int length = inStream.readInt(); byte[] whoBytes = new byte[length]; inStream.readFully(whoBytes); String whoString = new String(whoBytes, 0); client.userHasLeft(whoString); } public void skipMessage() throws IOException { int length = inStream.readInt(); inStream.skipBytes(length); } }
When you write socket-based servers, you have to take care of all the problems that RMI and CORBA normally take care of. For instance, if a client has a very slow network link, you may have threads that start blocking when trying to write to the client. This can cause the server to appear hung for some users. Just as you created a thread to read from a client, you can create a thread to write to a client. You can then create a pipe stream for sending data to the write thread. The write thread would read data from the pipe and write it to the client's socket connection. You also have the problem of deciding when a user's connection is hung. Usually when a client disappears, the socket connection closes. Sometimes, however, the network never receives a message to close down the connection. You may be queuing up data for a client that will never read it. One way to solve this problem is to keep track of how long a write thread has been trying to write data to a client. The write thread sets a flag indicating that it is trying to write and stores the current time before calling the write method. You then create a thread that runs in the background checking all the write threads. If it finds a thread that is trying to write and it has been trying to write for a certain time period (maybe 10-15 minutes), it closes down the connection to the client.
Listing 37.9 Source Code for RMIChatEnrol.java package chat.rmi; public interface RMIChatEnrol extends java.rmi.Remote { public RMIChatServer enrol(String name, RMIChatClient client) throws java.rmi.RemoteException; }
Listing 37.10 shows the RMI implementation for this factory. It simply identifies itself to the RMI registry (the RMI naming service) and then creates new RMIChatServerImpl objects in response to an enroll request from a client.
Listing 37.10 Source Code for RMIChatEnrolImpl.java package chat.rmi; import java.rmi.server.UnicastRemoteServer; import java.rmi.server.StubSecurityManager; import chat.server.*; // This class is a factory for RMIChatServerImpl objects. Whenever // a client enrolls, it creates a new RMIChatServerImpl and returns // it to the client. public class RMIChatEnrolImpl extends UnicastRemoteServer implements RMIChatEnrol { ChatServer server; public RMIChatEnrolImpl(ChatServer server) throws Exception { this.server = server; // Find out what name this object should use in the RMI registry String name = System.getProperty("rmiName", "chat"); // Identify this object to the registry java.rmi.Naming.rebind("chat", this); }
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch37.htm (21 of 29) [8/14/02 10:58:01 PM]
public RMIChatServer enrol(String name, RMIChatClient client) throws java.rmi.RemoteException { // Create a new RMIChatServerImpl and return it to the client return new RMIChatServerImpl(server, name, client); } }
Once the connection handler is created, it needs to be able to communicate with the client, and the client needs to communicate back. Under RMI, this requires two more interfaces. Listing 37.11 shows the RMIChatClient interface, which is implemented by the client. The connection handler calls methods in RMIChatClient in response to method calls from the chat application.
Listing 37.11 Source Code for RMIChatClient.java package chat.rmi; public interface RMIChatClient extends java.rmi.Remote { public void incomingChat(String who, String chat) throws java.rmi.RemoteException; public void userHasEntered(String who) throws java.rmi.RemoteException; public void userHasLeft(String who) throws java.rmi.RemoteException; public void disconnect() throws java.rmi.RemoteException; }
The RMIChatServer interface is implemented by the connection handler. The client invokes the sendChat method in this interface to send a chat message to the chat application. Listing 37.12 shows the RMIChatServer interface.
package chat.rmi; public interface RMIChatServer extends java.rmi.Remote { public void sendChat(String chat) throws java.rmi.RemoteException; public void disconnect() throws java.rmi.RemoteException; }
Unlike the complex TCPChatClient class, the RMIChatServerImpl class is extremely straightforward. It doesn't have to cram messages down a socket, and it doesn't have to interpret any data. All it does is invoke methods on the remote client or on the chat application. Listing 37.13 shows the RMIChatServerImpl class.
Listing 37.13 Source Code for RMIChatServerImpl.java package chat.rmi; import java.rmi.server.UnicastRemoteServer; import java.rmi.server.StubSecurityManager; import chat.server.*; // // // // This class is actually an RMI encapsulation for the ChatClient interface. It implements the methods in the ChatClient interface and invokes the corresponding method in the RMIChatClient interface.
// It also handles messages coming from the client. When the // sendChat method is invoked via RMI, it turns around and // invokes sendChat in the chat application. public class RMIChatServerImpl extends UnicastRemoteServer implements RMIChatServer, ChatClient { protected ChatServer server; protected String name; protected RMIChatClient client; public RMIChatServerImpl(ChatServer server, String name, RMIChatClient client) throws java.rmi.RemoteException { this.server = server; this.name = name;
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch37.htm (23 of 29) [8/14/02 10:58:01 PM]
this.client = client; server.addClient(name, this); } public void incomingChat(String who, String chat) { try { client.incomingChat(who, chat); } catch (Exception e) { try { client.disconnect(); } catch (Exception ignore) { } server.removeClient(name); client = null; } } public void userHasEntered(String who) { try { client.userHasEntered(who); } catch (Exception e) { try { client.disconnect(); } catch (Exception ignore) { } server.removeClient(name); client = null; } } public void userHasLeft(String who) { try { client.userHasLeft(who); } catch (Exception e) { try { client.disconnect(); } catch (Exception ignore) { } server.removeClient(name); client = null; } } public void disconnect()
{ try { client.disconnect(); } catch (Exception ignore) { } server.removeClient(name); client = null; } public void sendChat(String chat) throws java.rmi.RemoteException { server.sendChat(name, chat); } }
The actual client program that you run is very simple, too. Unlike the TCP program, it doesn't need to spawn a separate thread, since RMI is running as a separate thread. The program can concentrate on reading input from the user. Listing 37.14 shows the RMIChatClientImpl object, which is the actual application that a user would run.
Listing 37.14 Source Code for RMIChatClientImpl.java import java.net.*; import java.io.*; import java.rmi.server.UnicastRemoteServer; import java.rmi.server.StubSecurityManager; import chat.server.*; import chat.rmi.*; // This class is an RMI client for the chat application public class RMIChatClientImpl extends UnicastRemoteServer implements RMIChatClient { public RMIChatClientImpl() throws java.rmi.RemoteException { } // The following 4 methods are callbacks from the // RMIChatServerImpl class.
public void userHasEntered(String who) throws java.rmi.RemoteException { System.out.println("--- "+who+" has just entered ---"); } public void userHasLeft(String who) throws java.rmi.RemoteException { System.out.println("--- "+who+" has just left ---"); } public void incomingChat(String who, String chat) throws java.rmi.RemoteException { System.out.println("<"+who+"> "+chat); } public void disconnect() throws java.rmi.RemoteException { System.out.println("Chat server connection closed."); System.exit(0); } public static void main(String args[]) { // Get the name of the enroll factory String chatName = System.getProperty("rmiName", "chat"); // Must have a stub security manager! System.setSecurityManager(new StubSecurityManager()); try { // Get the name the user wants to use System.out.print("What name do you want to use? "); System.out.flush(); DataInputStream userInputStream = new DataInputStream(System.in); String myName = userInputStream.readLine(); // Create an instance of this object to receive callbacks RMIChatClient thisClient = new RMIChatClientImpl(); // Locate the RMIChatEnrol object
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch37.htm (26 of 29) [8/14/02 10:58:01 PM]
RMIChatEnrol enrol = (RMIChatEnrol) java.rmi.Naming.lookup(chatName); // Enrol to the chat system RMIChatServer server = enrol.enrol(myName, thisClient); // Free up the enrol object, we don't need it any more enrol = null; // Read lines from the user and pass them to the server while (true) { String chatLine = userInputStream.readLine(); server.sendChat(chatLine); } } catch (Exception e) { System.out.println("Got exception:"); e.printStackTrace(); System.exit(1); } } }
All you need now is a class to start up the chat application and set up the TCP and RMI front ends for the application. Because the application implementation is separate from the networking protocols, you can run both TCP and RMI interfaces to a single chat application. This means that RMI users and TCP users can talk together. Listing 37.15 shows the RunServer class that starts up everything.
Listing 37.15 Source Code for RunServer.java import chat.tcp.server.TCPChatServer; import chat.server.ChatServer; import chat.rmi.*; import java.rmi.server.StubSecurityManager; // This class starts up the chat application and the TCP and RMI // front ends. public class RunServer {
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch37.htm (27 of 29) [8/14/02 10:58:01 PM]
public static void main(String[] args) { try { // Start the chat application ChatServer server = new ChatServer(); int port = 4321; String portStr = System.getProperty("port"); if (portStr != null) { try { port = Integer.parseInt(portStr); } catch (Exception ignore) { } } System.setSecurityManager(new StubSecurityManager()); // Start the RMI server RMIChatEnrol rmiEnrol = new RMIChatEnrolImpl( server); // Start the TCP server TCPChatServer tcpServer = new TCPChatServer( server, port); tcpServer.start(); } catch (Exception e) { System.out.println("Got exception starting up:"); e.printStackTrace(); } } }
You should be able to use these classes as a starting point for any multi-user application you want to write. Always remember, however, to keep the application separated from the network protocols.
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f37-1.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f37-2.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f37-3.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f37-4.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f37-5.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f37-6.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f37-7.gif
CONTENTS
G G
G G
G G
Java's Suitability for On-Demand Applications Using the On-Demand Audio Applet H Logging In H Playing Audio Clips Adding Sound to Applets On-Demand Music Applet Code Review H Applet Architecture H Initialization and Registration H Song Selection H Playing the Songs Java Shortcomings New Features
The World Wide Web has served as an agent of change for many industries. Even old, stable technologies like telephone service are swaying to the rhythm of the Net. Very shortly, the content we now call multimedia will be available as an on-demand stream, first to your desktop, then to your set top, and finally to your palmtop. The promise is that you will receive the content of the exact type you want, as much as you want, anywhere you want. Java will probably play an important role in this media explosion. If ever there was a right programming language in the right place at the right time, this surely looks like it. Pay-Per-View and Content OnDemand are terms that you are probably already familiar with. Java holds the promise of creating high quality products for these new application areas.
Why is Java particularly suited for on-demand access of multimedia over the network? Take a look at the process of doing an on-demand multimedia application using C++. The following shows your list of concerns if developing a multimedia application using C++:
G
Develop a client and server that can communicate over the Internet.
G G G
Server listens for a connection on a given port. Once a connection is made, handshaking occurs. Client makes its request. Server interprets the request and processes it. Server needs to understand the digital media file format. (If the format is unknown, you have problems.) Server reads file and sends it over the network. Communications are monitored for loss of connectivity, interrupts, and socket transfer rates. When the transaction completes, the communication line is terminated.
Client initializes itself and presents a UI to the user and waits for input. User selects clip; client contacts the server and establishes a connection. Client requests the clip and waits for data. Data is read from the socket and is sent to the output device. Data stream is complete; communication socket is closed and the output device is closed.
The server application can be fairly platform-independent; however, it has platform-specific modules controlling the file system and other operating system-specific aspects. It also needs basic knowledge of the underlying media if it is to transfer the data efficiently (can the headers be left out, what data packing is most appropriate?). The client software is very platform-specific. At best, the user interface uses a standard library that can be linked on different platforms. More likely, sections of it need to be rewritten for the various platforms. The client also needs to control the output device and understand the translation, if any, between the file data format and the required hardware data format. The client needs intimate knowledge of network communications. Finally, the client needs to be distributed to customers; this is a major marketing problem. The customer wants the audio or video "on-demand" (read now). Can you imagine an on-demand system where users need to have the foresight to order an application a couple of days before they have the desire to watch and listen? There are some libraries available for C++ that help alleviate some of the problems-a commercial implementation of CORBA for the communications issues, various UI libraries for the user interface-but
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch38.htm (2 of 12) [8/14/02 10:58:10 PM]
none deal comprehensively with the problem areas of network multimedia. Java addresses many of these problems. The underlying steps remain, but the Java execution environment removes many of the implementation details from the developer. For example, instead of developing a server from scratch (or even modifying and reusing an existing server), the product can be developed as an applet. The communication issues are completely removed from the process. There is no need to develop a server at all. Any off-the-shelf Web server works. Any application (or applet) developed with Java is platform-independent. Therefore, creating graphical elements, displaying images, playing audio, and processing events are not only done with the same source code, but the same compiled binary code. The Java built-in media API removes many file format- and output hardware-dependent issues. For example, audio clips can be streamed over the network and played on any system without regard to the various output devices or streamed data formats. Because the majority of the underlying functionality is supplied by the Web server/browser system, the distributed Java executable is very small. The example developed for this chapter is approximately 9K. Due to its extremely compact size, it can be downloaded over the Web in less time than most images. This solves the problem of distribution and provides instant access. To review, Java is suitable for multimedia because:
G G G G G
It is portable. It is compact. It can handle streaming data. It is distributed in a client/server topology. It has built-in multimedia classes.
Logging In
To login to the On-Demand Audio Applet: 1. Start your favorite Java-capable browser. 2. Open the file OnDemand.html with your browser. The text string Applet OnDemand running is displayed on your browser's status line when the
applet is fully loaded. The login dialog appears as in Figure 38.1. 3. Click in the Username text field. 4. Enter the text string anonymous in lowercase letters. 5. Click the Login button. Figure 38.1 : Enter anonymous in the Username text field.
Tip
Getting the audio clip this way is usually very slow the first time it is done in an applet. This is due to dynamic class loading. When you are running these applets over the Internet, each instantiation of an AudioClip is extremely slow. This is because the AudioClip object does not truly stream the audio data. It downloads and buffers the data at the time of instantiation. You need to give users some feedback so they don't cancel the operation. Try a call to showStatus that states something like, "Initializing audio. Please wait". Changing the cursor to a wait state would be ideal, but is not possible in applets.
getAudioClip returns a reference to an AudioClip object. You can then use this object to play the audio clip when you are ready. To do this, simply call the AudioClip.play method rockinTune.play(); You can also stop the playback using AudioClip.stop method and play back continuously using the AudioClip.loop method. Caution The supported list of audio file formats is limited. Currently, you can use only 8-bit, 8000Hz, single-channel (mono) Next/Sun AU files with G711 -law compression. Note that files with a higher sample rate may produce hissing on playback.
Registration screen where a user signs in (later used for billing). Browse the system for songs of interest. Play a song. Register the transaction on the server (for billing).
The first three elements are addressed here. The fourth element involves writing to files on the server and is addressed in Chapter 8, "Reading and Writing Files from an Applet." The sample code does have comments showing where to implement the transaction registration.
Applet Architecture
To keep the system as flexible as possible, all of the applet options are controlled by configuration files and parameters in the applet HTML. There are four parameters. Each parameter is the name of a configuration file. They are:
G G G G
userfile-contains a list of accepted users. imagefile-the start-up image. artistfile-the configuration file containing a list of artists. transactionfile-the name of the file to record the transactions to (this final parameter is not used).
Specifying the file names as parameters and designing the applet to be based on configuration files allows a single, simple applet to be used by a wide variety of users without any code modification by the programmer. All flexible values are specified in the files-the image file, the acceptable user names, the list of artists, the names of the files containing the songs for each artist, the names of the audio files associated with each song, and the detailed information for each song. These can be easily modified and don't even require the user to reload the applet. Due to the simplicity of this applet, there is no need to generate a host of new classes. The sample includes a total of two new classes: one subclass of applet, and a utility class that aids in file input and string parsing (FileParser). If the applet were expanded, separate classes would probably be desirable for each element listed earlier.
<APPLET code="OnDemand.class" width=600 height=300> <PARAM name="userfile" value="user.txt"> <PARAM name="imagefile" value="image.gif"> <PARAM name="artistsfile" value="art.txt"> <PARAM name="transactionfile" value="tran.txt"> </APPLET> ...
First, the initialization parameters are set, then an image is read, as shown in Listing 38.2.
Listing 38.2 Reading the Initialization Parameters and Creating the Image Object in the Java Applet // get the input parameters userFileName = getParameter("userfile"); imageFileName = getParameter("imagefile"); artistFileName = getParameter("artistsfile"); transactionFileName = getParameter("transactionfile"); ... // get the startup image startUpImage = getImage(getDocumentBase(),imageFileName); ...
When the Login button is pressed, the method OnDemand.confirmLogin is called. It retrieves the requested user name from the text field, reads the contents of the user file (specified in the parameter list), and compares the selected name to the retrieved list (see Listing 38.3). If the name matches, the interface is cleared and OnDemand.startUI is called to begin the main program. If the name does not match one of the names in the configuration file, the text field is cleared and the login form remains on the screen. Tip To improve performance, user configuration files could be read asynchronously at startup.
Note This applet is not intended as a commercial application; a more sophisticated applet uses encryption or digital signatures to secure its communications (see Chapters 26-28 in Part VI, "Java Security," for more details). It also provides a dialog box if the login fails, and a password is then required. To implement a password text field, see TextField.setEchoCharacter.
Listing 38.3 Confirming the Username // get the specified UID userName = userNameField.getText(); ... // read the file contents and pack the lines of text into a // vector of strings userFileContents = FileParser.parseFile(getDocumentBase(), userFileName); ... // loop over the acceptable usernames and compare the specified UID int i; int total = userFileContents.size(); for(i=0; i<total; i++) { try { String tempString = (String)userFileContents.elementAt(i); // compare the strings if( userName.compareTo(tempString) == 0) { // the strings are the same, let them log in startUI(); return; } } catch (ArrayIndexOutOfBoundsException e) { // do nothing since we are sure not to be out of the bounds
} } ...
Song Selection
Once the user is logged in, two text lists are displayed. The first is a list of artists; the second is a list of songs by that artist. The first list is read from the artist file (one of the initialization parameters). When an artist is selected, OnDemand.selectArtist is called and the artist's line from the configuration file is parsed and the song list filename is retrieved. Then, the song list is read (see Listing 38.4).
Listing 38.4 Reading the Song List in Response to an Artist Being Selected ... // // get the selected artist // int index = artistList.getSelectedIndex(); ... // get the selected artist configuration line String temp = (String)artistFileContents.elementAt(index); // // read the contents of the artist's song file. the name of the // song file is the second argument in the artists file. // songFileContents = FileParser.parseFile(getDocumentBase(), FileParser.parseField(temp,2)); ...
When a song is selected, OnDemand.selectSong is called, the song's configuration line is parsed, and the song filename and informational message is retrieved. Then, the song information is displayed using the song filename retrieved from the configuration files; the AudioClip object is created (see Listing 38.5).
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch38.htm (9 of 12) [8/14/02 10:58:10 PM]
Listing 38.5 Parsing the Song Configuration Line // // get the selected song from the list // int index = songList.getSelectedIndex(); // notify the user of wait condition showStatus("Initializing audio. Please wait..."); ... // get the song info string the song's configuration line String line = (String)songFileContents.elementAt(index); // // create the audio clip // sound = getAudioClip(getDocumentBase(), FileParser.parseField(line,2)); // // populate the text area // FileParser.populateTextArea(songInfo, FileParser.parseField(line,3), songInfo.getColumns()-10); ...
Caution Double-clicking is used in this example to select items from the list. This can be confusing since one click highlights a new item. If the user is not paying attention, there may be confusion between which clip is selected and which is highlighted. To alleviate this problem, subclass List and override the handleEvent operation. Look for the event IDs of Event.LIST_SELECT and Event.LIST_DESELECT with the target a list region. See the Java API documentation for details.
Java Shortcomings
Although Java is a very powerful language, there are a number of things that can be improved:
G
G G
Media streaming is available only via applets that are executed in the environment of a robust Web browser (see Chapter 13, "Running Applets as Applications," for details and work-arounds). It is not possible to use the appletviewer to run or debug these applets. The current debug method is a slow process of loading your applet into a Web browser, checking it out, and then editing your Java source. This in itself is not so bad, but some browsers buffer your classes (even if you try to force a reload). You must relaunch your browser each time you want to test another feature. Also, it is difficult to get any debug feedback via System.out because some Web browsers do not supply a standard out. Currently the AudioClip is limited to a very specific file format and even a specific range of parameters within that format (Sun/Next audio file with one channel, -law compression, and 8KHz sample rate). This limitation should be removed. Java needs to include video streaming. True data streaming should be implemented to reduce initialization delay times in the instantiation of objects. Currently, the AudioClip objects download and buffer the audio data upon initialization. There may be some room for compromise using built-in asynchronous transfers. Ultimately, the streams need to have more user feedback control. For example, the user should be able to pause and scrub through an audio clip. Visual media types (images, and in the future, video) should be subclasses of components or have containers that can accept them as components. The current Javasoft API documentation is fairly weak when it comes to educating a new developer. Although it is excellent for reference, a Programmer's Guide with detailed information on difficult topics like the Layout Managers would be beneficial.
New Features
Java is evolving; look for the following changes that will affect the development of multimedia applications:
G
Video implementation
Improvements will include both latency-sensitive and -insensitive video and will accommodate both streaming and stored video sources. Enhanced Audio Enhancements will include support for sampled and synthesized audio as well as 3-D spatial audio sources. Streaming and stored audio sources will be supported. MIDI support Support will include timed-event streams, loadable synthesizers, and effects. High performance 2-D object animation 2-D support will include sprites with transparency, programmed sprite behaviors, scrolling background images, aggregation and hierarchical compositing, and image- transformation effects. Improvements to 2-D graphics and imaging 2-D graphics support will include affine transformations (translate, rotate, skew, and so on) on points and paths; compositing, which will allow overlays, blending, and transparency; image filters, including features such as table lookup, convolve, and sharpen; and paint enhancements, including gradients, and patterns.
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f38-1.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f38-2.gif
CONTENTS
G
G G G
G G
Java's Suitability for Multimedia Applications H Java Is Portable H Java Is Compact H Java Can Handle Streaming Data H Java Is Based on the Client/Server Model H Java Supports PDAs Easily Using the Multimedia Encyclopedia Adding Images and Sound to Applets The On-Line Multimedia Encyclopedia In-Depth H Applet Architecture H Index Window H Topic Window Shortcomings New Features
Knowledge is dynamic. Science and industry march on heedless of the headaches that this evolving picture of the universe gives publishers and authors around the world. Even information that we think of as static does not rest peacefully. Discoveries are made every day that change the way that we look upon historical events. The Dead Sea Scrolls are unearthed; a meteorite from Mars with tantalizing clues of life is discovered; the Titanic is raised from the ocean's floor. Not only is knowledge dynamic but so is language, teaching methodology, and culture. There are methods of displaying information today that were not possible several years ago. Now, look at our feeble attempts to deal with dynamic information. Reprinted books? CD-ROMs? Something is wrong. How do you update the copy of Encarta that you've had on your machine for three years? The possibility of solutions is only now starting to emerge with increased use of the Internet, graphical browsers, and portable, small, powerful software applets written in Java.
Java Is Portable
Java already runs on every popular computing platform. The same code runs on PCs, Macintoshes, and UNIX workstations.
Java Is Compact
The Multimedia Encyclopedia applet in this chapter is only 9K in size. This is critically important when dealing with the small bandwidth of phone lines (or an even smaller bandwidth for some mobile applications).
and increasing performance. Because this application is written in Java, tourists can take this same tour at home on their PCs, either before or after their visit, simply by accessing the museum's Web site.
Tip Getting the audio clip this way is usually very slow the first time it is done in an applet. This is due to dynamic class loading. When you are running these applets over the Internet, each instantiation of an AudioClip is extremely slow. This is because the AudioClip object does not truly stream the audio data. It downloads and buffers the data at the time of instantiation. You need to give users some feedback so they don't cancel the operation. Try a call to showStatus that states something like, "Initializing audio. Please wait". Changing the cursor to a wait state would be ideal, but is not possible in applets.
This sample creates an object that references the sound file, sound.au, in the same location as the applet's URL. To play the sound clip, simply invoke the AudioClip.play method. You can also stop and loop (continually play back) the audio clip. The following line shows how to play a sound: sound.play();
Tip Using the loop functionality can be very helpful for background sounds. Instead of downloading long sound files, you can loop short sound files. Looping a sound file will play it over and over without pause. This will shorten download time without sacrificing quality.
Caution The supported list of audio file formats is extremely limited. Currently, you can use only 8-bit, 8000Hz, single-channel (mono) NEXT/Sun AU files with G711 -law compression. Note that files with a higher sample rate may produce hissing on playback.
Adding images to your applet is very similar to adding audio. The primary difference is that the image does
not have a draw method to display itself, whereas the audio clip contains its own play method. To attach to a particular image, call the Applet.getImage method. The URL (or a base URL and relative path) can be specified just like the AudioClip, as follows: Image thePicture = myApplet.getImage(getDocumentBase(), "image.gif"); An image object can reference either JPEG (JFIF) or GIF images. To display the image, add a call to Graphics.drawImage. This is usually done in the applet's (or component's) overridden paint method. The following example explains how to display an image object. Note that the call to myApplet.getGraphics is made because only a Graphics object can display an Image: Graphics g = myApplet.getGraphics(); g.drawImage(thePicture, 20, 40, myApplet); The method Graphics.drawImage is overloaded and has four implementations. They vary based on whether or not a default background or output resolution is specified. If an output resolution is specified, the image is scaled as it is rendered to the screen. Images can also be created or altered by your applet, and they can be rendered on or off screen. For more information, see Chapter 4, "Displaying Images." Also, refer to the ImageProducer class and the java.awt.image package. Tip Unfortunately, images are not components in the Java libraries. This means that they cannot be added directly to a layout. If you want to add images to a display that contains components, you probably need to subclass Canvas. Your subclass must at least override the Component.paint method. Add the Graphics.drawImage method to the paint method. This is illustrated in the sample code ImageCanvas.java. To develop a more robust ImageCanvas, you should also override the Component.preferredSize, Component.minimumSize, and Component.size methods to return the actual image dimensions. This makes the resulting component the correct dimensions.
Applet Architecture
To make the applet as flexible as possible, all of the display options are specified as either applet parameters or in configuration files. The encyclopedia cover page (image file name) and the index file are specified in the applet parameter list. The topic configuration filename is a field in the index file (associated with the index listing), which references a file that contains fields specifying an image filename, a sound filename, and a textual description of the topic. Thus, the encyclopedia can be easily modified without changing the underlying Java source code. Due to the simplicity of this example, there is no need to create a large set of new classes. However, due to their nature, some new classes are straightforward, obvious, and well- behaved. These classes are ImageCanvas and FileParser. The FileParser class was not actually created for this sample. It was originally developed for the sample in Chapter 38, "Creating On-Demand Multimedia Services." Follow good object practice here and reuse it. FileParser aids in configuration file input, file and field parsing, and text region population (loading the fields from the file into specified text regions). ImageCanvas is a component that displays an image within its body. The ImageCanvas is required in order to include an image in a display that also contains buttons and other widgets. The Canvas allows an image to be displayed as part of a component. If the image is not a component, no space is reserved for it by the Layout Managers. Listing 39.1 is an example of a basic ImageCanvas.
Listing 39.1 Implementing a Basic ImageCanvas public class ImageCanvas extends Canvas { private Image theImage; // image canvas constructor public ImageCanvas(Image that) { theImage = that; } // draw the canvas public void paint(Graphics g) { // draw the image g.drawImage(theImage, 0, 0, this); } }
Index Window
The encyclopedia consists of two screens. The first is the index screen; it contains a list of indexes and the encyclopedia cover page (image). Double-clicking a list topic takes you to the topic screen. Listing 39.2 shows the major steps required to display an image in a Canvas on the interface.
Listing 39.2 Creating the Cover Page Image Object and Placing It in an ImageCanvas ... // get the startup image Image startUpImage = getImage(getDocumentBase(),imageFileName); // create the intro image canvas component introImage = new ImageCanvas(startUpImage); // // Create the GUI. Notice that the image canvas is a component and // can be handled by the Layout Managers like any other component. // setLayout(new GridLayout(1,2)); add(indexPanel); add(introImage); ...
Note There is no need to explicitly draw the image, because its parent component ImageCanvas draws the image in its paint method.
Topic Window
The topic window displays the topic's image, a text region containing the topic information, a Listen button to play the sound clip associated with the image, and an Index button that takes you back to the original index screen. Again, the ImageCanvas is employed to place and display the image component. The AudioClip object is created and tied to the Listen button. Listing 39.3 shows the major steps required to create Image and
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch39.htm (7 of 10) [8/14/02 10:58:20 PM]
Audio objects. Listing 39.4 is an example of binding sound playback to a button action.
Listing 39.3 Creating Media Objects and Adding Them to the Applet ... // // Create the image, image canvas, and audioclip objects. // Sometimes the audio initialization takes some time, so post a // message to the user. // showStatus("Initializing the audio. Please wait..."); sound = getAudioClip(getDocumentBase(), audioName); Image theImage = getImage(getDocumentBase(), imageName); ImageCanvas theCanvas = new ImageCanvas(theImage); showStatus(""); // add the items to the applet setLayout(new GridLayout(1,2)); add(theCanvas); add(textPanel); ...
Listing 39.4 Play the Sound in Reaction to the Listen Button // // watch for button events // public boolean action(Event event, Object arg) { // the listen button was pressed if(event.target == listenButton) sound.play(); // didn't find anything of interest. see if my parent wants it. else return super.action(event,arg); return true; }
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch39.htm (8 of 10) [8/14/02 10:58:20 PM]
Shortcomings
Following are some shortcomings of Java for developing this multimedia encyclopedia:
G
Visual media types (images and video) should be subclasses of components or should have containers that can accept them as components. Tip Until this happens, try subclassing Canvas and overriding the paint method. Also, override the minimumSize, preferredSize, and size method to make the panel conform to the image size.
G G
Currently, the AudioClip is limited to a very specific file format and even a specific range of parameters within that format (Sun/NEXT audio file with one channel, -law compression, and 8KHz sample rate). This limitation should be removed. Media streaming is available only via applets that are executed in the environment of a robust Web browser (see Chapter 13, "Running Applets as Applications," for details and work-arounds). It is not possible to use the appletviewer to run or debug these applets. The current debug method is a slow process of loading your applet into a Web browser, checking it out, and then editing your Java source. This in itself is not so bad, but some browsers buffer your classes (even if you try to force a reload). You must relaunch your browser each time you want to test another feature. Also, it is difficult to get any debug feedback via System.out, because some Web browsers do not supply a standard out. Java needs to include video streaming. Java should include 3-D world streaming for support of these potentially important types for a multimedia encyclopedia. Ultimately, the streams need to have more user feedback control. For example, the user should be able to pause and scrub video and audio clips. True data streaming should be implemented to reduce initialization delay times in the instantiation of objects. There may be some room for compromise using built- in asynchronous transfers. There is no way to synchronize graphical events with audio playback. There should be a time-based synchronization method. This will need to be easily integrated with video streaming.
New Features
Java is evolving; look for the following changes that will affect the development of multimedia applications:
G
Video implementation
Improvements will include both latency-sensitive and insensitive video and will accommodate both streaming and stored video sources. Enhanced audio Enhancements will include support for sampled and synthesized audio, as well as 3-D-spatial audio sources. Streaming and stored audio sources will be supported. MIDI support Support will include timed event streams, loadable synthesizers, and effects. High-performance 2-D object animation 2-D support will include sprites with transparency, programmed sprite behaviors, scrolling background images, aggregation and hierarchical compositing, and image transformation effects. Improvements to 2-D graphics and imaging 2-D graphics support will include affine transformations (translate, rotate, skew, and so on) on points and paths; compositing, which will allow overlays, blending, and transparency; image filters including features such as table lookup, convolve, and sharpen; paint enhancements, including gradients and patterns. Addition of 3-D geometry and behavior 3-D geometry features will include immediate, retained, and compiled-retained 3-D graphics; highlevel specification of behavior and control of 3-D objects; a generalized morphing engine; and highresolution coordinate anchors. A new Java object-the servlet Servlets are Java objects that can be downloaded from the server at runtime. These servlets provide increased capabilities for communicating with the server. In a simple case, they can be used to replace the rather awkward CGI scripts that are currently being used.
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f39-1.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f39-2.gif
CONTENTS
G G G
Characteristics of Non-Traditional Devices The New Computing Model Designing Applications to Support Non-Traditional Devices H Separating the User Interface from the Application H Avoiding Large, Monolithic Applications H Sticking to Standard Libraries H Avoiding Long, Complex Transactions Designing User Interfaces for Small Devices H Creating Obvious, Self-Documenting Interfaces H Avoiding Extraneous Pictures or Information H Keeping Everything Readable H Supporting Multiple Sources of Input Creating Reusable Components for Small Devices H Using the CardLayout Layout Manager as a Stack H Creating a Keyboard/Keypad Input Filter H Creating a Pop-Up Keypad for Pen and Touch-Screen Users
One of the most interesting aspects of Java is that it was originally designed to work on a small hand-held device, making it very friendly toward these devices. Java is also a self-contained environment; that is, it is more than just a programming language. You can run Java programs wherever you can implement the Java runtime environment. Java promises to allow you to run programs in places you never expected. For example, you can have a mobile terminal mounted in your car, maybe as an extension to your cellular phone. Within the next year, you may see television sets that can also access the Internet and run Java programs. You will soon see Java showing up in cellular phones and Personal Digital Assistants (PDAs). These devices come with their own set of advantages and limitations. If you get your systems ready for these devices, you can get a jump on your competition.
soon have a need for your own home network. Figure 40.3 illustrates a typical home network configuration. Figure 40.3 : With so many network-aware devices at home, you will need your own home network. As more and more of the applications you use every day become Java-enabled, you will be able to run them in the different devices on your home network. For instance, you may have an address book program that you might need to access from your phone, your TV, or your desktop computer (if you still have one). Applications for these new devices will consist of one or more applets implementing the user interface and communicating with a server application, as shown in Figure 40.4. Figure 40.4 : The Java-enabled devices in your home will often just implement a user interface for an application. It is entirely possible that your desktop computer, as you know it, will change. Right now, you probably have a keyboard, a monitor, a CPU, a modem, and a printer. Figure 40.5 illustrates the typical home computer of today. Figure 40.5 : A typical home computer configuration. In your future home network, you still need these devices, of course, but some of these components need to be shared with other devices in your house. Your CPU becomes the computing server for your house. All your applications run on it. Your printer either is connected to your CPU or plugs directly into the network. Your monitor and keyboard are replaced by a network computer or a Web TV. You probably can buy a cheap box that turns your current monitor and keyboard into a network computer. Your modem is either attached to the network or replaced by whatever device gives you connectivity to the outside world-maybe a cable or an ISDN terminal adaptor. Figure 40.6 shows how the components of your home computer fit into a home network. Figure 40.6 : You still need the same computing components, just rearranged differently. You may eventually be able to control your lights and thermostat from a home network. After you have a network running throughout your home, the possibilities for new devices are endless. Figure 40.7 illustrates some of the possible devices that can be on your home network. Figure 40.7 : You can attach numerous devices to your home network. This brings up a sticky situation, however. The more things that are accessible on your home network, the more critical it is to prevent people from accessing your home network from the outside. Unfortunately, you can't simply block out any incoming requests, because you may want to access information on your home server from your car, which may also have its own network. You need a home firewall system that prevents malicious network attacks, and enables you to control who can access information in your home and what information they can access. It is possible that, some day, you will no longer need your CPU. You will be able to buy computing services like you buy phone or cable services. Your computing services provider supplies the applications you use over the Internet. This provider also maintains the applications, making sure that the most recent versions are available. In addition, the provider would perform the often-ignored task of backing up your data. There are advantages and disadvantages to giving up the administration of your computing server. On the positive side, you no longer have to keep up with the current versions of software. You don't have to go out and install new packages;
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch40.htm (3 of 22) [8/14/02 10:58:32 PM]
your provider should have whole application suites ready for immediate use. You don't have to make backups, either. In the simplest case, you can subscribe to such a service, then go out and buy a Java-enabled TV and a network printer, plug them into the wall, and go.
Separate the user interface from the application logic This allows you to create various interfaces for the same application. Avoid large, monolithic applications Try to break up the functionality into smaller classes that can be downloaded on demand. Use standard libraries whenever possible Smaller devices will probably have enough local code to communicate on the network, hopefully using RMI or CORBA. Some, if not all, will include JDBC, because it is one of the core Java libraries. The less you have to download to the device, the better off you are. Avoid complex, prolonged transactions Most of the time, remote devices need to get small pieces of information quickly. Obviously there are exceptions, but something like an address book should be small and quick.
There's a tradeoff involved here. It is extremely costly for your server to generate images for every client whenever the client needs to zoom the image. You would be much better off if the client were doing that processing. You must balance that against the fact that you have to put the zooming code onto the client, which may be a burden to the client. Tip Design involves a series of tradeoffs that result in different decisions for different applications.
It is often very easy to let pieces of the user interface leak into the application. For instance, suppose you create an ordering system that presents the user with a list of parts. The user can select any number of parts on the list, and press the Order button to order the parts. You might be tempted to have the application keep track of which parts the user has selected. In other words, the interface between the application and the user interface might include methods to select and deselect parts, and then place the order, like this: public void selectPart(String partName); public void deselectPart(String partName); public void placeOrder(); This is not a good separation, however. The user interface has leaked over into the application. This often happens because you have taken too granular a view of what is going on. The application is responsible for ordering parts. It is not responsible for identifying which parts should be ordered. Tip If the interface between an application and a user interface closely reflects the various components of the user interface, you may not have separated them very well.
When you look at the selectPart-deselectPart-placeOrder interface, you can almost see what the designer was thinking-a multiple-selection scrolling list of parts and an Order button. The interface should really just contain a method to place an order, like this: public void placeOrder(String[] partNames); Now the application doesn't have any notion of the interaction between the user and the user interface, as it did when it had methods for selecting and deselecting parts.
A word processor not only serves as a good example of a typically monolithic application, it also illustrates one of the compromises you have to make when separating the user interface from the application. Conceptually, the document you are editing belongs to the application side. If you were to send every little editing command over to the application, you may never get anything done, unless you happen to have a very fast network. You have to create a balance, where you bring over portions of the text for editing, and occasionally mirror the text back to the application.
Different features of your application have their own special interfaces. Rather than downloading these to the client at startup time, you can use Java's dynamic loading mechanism to postpone the loading of these special interfaces. You can also cut down on the interdependence between various interface components if you create each interface component as an AWT panel, or better yet, as an applet. That's really where the term applet comes from. They aren't whole applications, they are pieces of applications. Because the special interfaces are panels or applets (which are panels themselves), you can create a special section of your display for the special interfaces, or use a card layout manager to switch between different panels. The other parts of your user interface don't have to know anything else about the interface. The Java component interface, known as Beans, promises to provide additional support for this, allowing your special interfaces to plug themselves into the current application in a seamless way.
The important thing here is that you don't write anything that is already available in the standard Java libraries. If you have your own special remote method invocation system, consider replacing it with RMI, at least for Java client-server communications. You save a tremendous amount of time, because the RMI code is part of the standard Java libraries (as of Java 1.1). Tip Keep in mind that non-Java components, such as ActiveX, are probably not going to be available on these smaller clients. You should be very wary of these components when designing user interfaces that may run on small clients.
The interface should be obvious and self-documenting You should be able to figure out how to use the interface just by looking at it or by playing around with it for a minute or two. You can't rely on online help. No extraneous pictures or information Screen space is precious, as is download time. Don't put anything on the screen that isn't used to perform a task. Keep everything readable Don't just shrink down an interface from the desktop and expect someone to use it. If you are searching for smaller fonts so you can fit more text on the screen, you're doing something wrong. Support multiple sources of input If your user interface must run on a cellular phone, you had better be able to use it with only a phone keypad.
This doesn't mean that you should avoid pictures altogether. You may find that a few simple icons describe the use of the device just as well as text. The advantage to the icons is that they are not language-dependent. Anyone can use an iconbased user interface no matter what language they speak. When you create icons for your interface, however, keep them simple. If you need arrows pointing in certain directions, don't download some ray-traced image of a marble arrow. Either download a simple arrow image, or use the Graphics class to paint the arrow yourself.
Small keyboard with arrows and most ASCII characters, but no function keys Numeric telephone keypad with a few extra keys A pen that functions like a mouse A touch-screen, which is like a pen, but with a big, fat pointer device called a finger A voice-recognition system
The fact that some devices may not even have separate input sources poses a huge problem. If you have to support a pen or touch-screen interface, you have to leave room on the screen for the items the user can select.
Because you can't always count on letters being present when you have a keypad, you have to support the limited set of buttons on a telephone keypad. These are serious issues that may be addressed by the manufacturers of these devices as they become more prevalent. Unfortunately, this technology is too new for anyone to have considered the problem. In the past, you had to write custom programs for each hand-held device, so you could make safe assumptions about what kind of input source might have been present. With the advent of Java, you can no longer make that assumption, so you must adjust. The following are some strategies you can take when approaching this problem:
G
Create a loader applet that figures out what kind of input source is present and then loads an applet specialized for that kind of input source. Provide multiple means of performing an operation. For instance, allow the 2 key on a phone keypad to work like the up arrow on a regular keyboard. Provide small touchable icons. If someone has a touch screen or a pen, this may be the only way to enter input. You may also create a special icon that brings up a small touchable keyboard or keypad. Force your users to run different applets depending on what kind of input source they have. This is not a very good solution, but sometimes you don't have much choice.
Listing 40.1 Source Code for StackLayout.java import java.awt.CardLayout; // This class is used as a layout manager for use with the PushablePanel // class. It works exactly like the CardLayout, but the PushablePanel // looks for a StackLayout explicitly, so you can safely use CardLayouts
// in your panels without pushing a PushablePanel on top of them. public class StackLayout extends CardLayout { public StackLayout() { } public StackLayout(int hgap, int vgap) { super(hgap, vgap); } }
Now, when a panel pushes itself onto a stack layout, it adds itself to the end of the stack and tells the stack layout to display the last element. You may want to make custom user interface components that can pop up panels at any time. This presents a small difficulty, because you always add the panels to the container that uses a stack layout. This means that each user interface component would have to have a reference to that container. If a component is nested several containers deep, this is unacceptable. However, you can make a pushable panel that searches for the correct container. Whenever a component pops up one of these panels, the panel searches for the container that uses a stack layout for a layout manager. It does this by using the getParent method in the current component and continuing to search parents. Listing 40.2 shows the PushablePanel class that works with the StackLayout layout manager.
Listing 40.2 Source Code for PushablePanel.java import java.awt.*; // This class implements a panel that can push itself onto a StackLayout // and pop itself off again. public class PushablePanel extends Panel { protected Container parentContainer; protected StackLayout stackLayout; public PushablePanel() { parentContainer = null; stackLayout = null; } // Push this panel onto the current stack layout. Given a component, find // a container whose layout manager is a stack layout, and push this panel // on top of it. By doing this, any object can push a new panel without // having a reference to the layout manager. public void push(Component comp) { while (comp != null) { // If the current component is a Container, see if it uses a StackLayout
//
//
// //
// // //
if (comp instanceof Container) { Container cont = (Container) comp; LayoutManager layout = cont.getLayout(); If the current container uses a StackLayout, we've found our container if (layout instanceof StackLayout) { parentContainer = cont; stackLayout = (StackLayout)layout; break; } } Try the next component up the line comp = comp.getParent(); } If we found a container with a StackLayout, add this component to the container. if (parentContainer != null) { parentContainer.add(this); stackLayout.last(parentContainer); } } public void pop() { To pop this panel off the stack, move the stack layout to the previous panel (the StackLayout is really a CardLayout) and then remove this panel from the stack. if (parentContainer != null) { stackLayout.previous(parentContainer); parentContainer.remove(this); } }
Listing 40.3 Source Code for SmallDevicePanel.java import java.awt.*; // This class implements a PushablePanel that can filter keystrokes // to implement hot keys for various user interface components. public class SmallDevicePanel extends PushablePanel { // InputFilter maps key codes to components and passes keystroke events // to those components. protected InputFilter filter; // The empty constructor creates an input filter by default public SmallDevicePanel() { filter = new InputFilter(); } // This constructor allows you to create an unfiltered panel, which // makes this object nothing more than a PushablePanel. public SmallDevicePanel(boolean filtered) { if (filtered) { filter = new InputFilter(); } else { filter = null; } } // Add a component and if it can have an input filter, set its filter public Component add(Component comp) { if ((filter != null) && (comp instanceof FilteredComponent)) { ((FilteredComponent)comp).setFilter(filter); } return super.add(comp); } // Add a component and if it can have an input filter, set its filter public Component add(String name, Component comp) { if ((filter != null) && (comp instanceof FilteredComponent)) { ((FilteredComponent)comp).setFilter(filter); } return super.add(name, comp); } // Add a component and if it can have an input filter, set its filter public Component add(Component comp, int position) { if ((filter != null) && (comp instanceof FilteredComponent)) { ((FilteredComponent)comp).setFilter(filter); }
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch40.htm (12 of 22) [8/14/02 10:58:32 PM]
return super.add(comp, position); } // Filter any keypresses and pass them to the input filter class public boolean handleEvent(Event evt) { if (filter == null) return super.handleEvent(evt); if (evt.id == Event.KEY_PRESS) { return filter.filter(evt); } return super.handleEvent(evt); } }
The SmallDevicePanel class requires a helper class called InputFilter that actually maps the keystrokes to a component. The components register the keystrokes they want with the input filter. Listing 40.4 shows the InputFilter class.
Listing 40.4 Source Code for InputFilter.java import java.awt.*; import java.util.*; // This class implements a keystroke filter that allows you to // create hot keys for various components. It uses a hash table // to look up the keystrokes. public class InputFilter extends Object { protected Hashtable filterTable; protected boolean filtering; public InputFilter() { filterTable = new Hashtable(); filtering = false; } // Map a single key value to a component public void add(int ch, Component receiver) { filterTable.put(new Integer(ch), receiver); } // Map a range of key values to a component public void add(int from, int to, Component receiver) { for (int i=from; i <= to; i++) { filterTable.put(new Integer(i), receiver); } } // Unmap a key, but only if it belongs to this receiver.
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch40.htm (13 of 22) [8/14/02 10:58:32 PM]
//
// // // // //
// // //
//
// // //
public void remove(int ch, Component receiver) { Integer key = new Integer(ch); if (filterTable.get(key) == receiver) { filterTable.remove(key); } } Unmap a range of keys, but only if they belong to this receiver public void remove(int from, int to, Component receiver) { for (int i=from; i <= to; i++) { Integer key = new Integer(i); if (filterTable.get(key) == receiver) { filterTable.remove(key); } } } This method actually performs the filtering. It uses a flag to see if it is already filtering an event. This way, if it passes the event to a component and the event gets all the way back up to the panel that has the filter, we don't filter it again. Otherwise, we'd have an infinite recursion, and that is a bad thing. public synchronized boolean filter(Event evt) { If we're already filtering an event, go away if (filtering) return false; Now we definitely are filtering an event filtering = true; See if there's a component that wants this keystroke Component comp = (Component) filterTable.get( new Integer(evt.key)); If nobody wanted this keystroke, unset the filtering flag and return if (comp == null) { filtering = false; return false; } Send this event to the component that wants it boolean retval = comp.postEvent(evt); We're through filtering filtering = false; Return the result that came from postEvent return retval; }
An interesting feature of the SmallDevicePanel and the InputFilter is that they allow the components themselves to specify what keystrokes they want to receive. The SmallDevicePanel class checks each component to see whether it implements the FilteredComponent interface, shown in Listing 40.5.
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch40.htm (14 of 22) [8/14/02 10:58:32 PM]
Listing 40.5 Source Code for FilteredComponent.java // This interface is implemented by any component that wants // hot keys controlled by the InputFilter class. public interface FilteredComponent { public void setFilter(InputFilter filter); }
If a component implements the FilteredComponent interface, the SmallDevicePanel class calls the setFilter method in the component. At that time, the component tells the filter what keystrokes it is interested in. You can safely use regular components with the SmallDevicePanel class, because it explicitly checks for the FilteredComponent interface first. It doesn't do anything if a component doesn't implement the interface. Listing 40.6 shows the ShortcutButton class, which allows you to specify a character as a shortcut for the button.
Listing 40.6 Source Code for ShortcutButton.java import java.awt.*; // This class implements a button that has a shortcut character. // It works in conjunction with the SmallDevicePanel and InputFilter classes. public class ShortcutButton extends Button implements FilteredComponent { protected int shortcut; protected InputFilter filter; // Create a button with a specific label and shortcut character public ShortcutButton(String label, int shortcut) { super(label); filter = null; this.shortcut = shortcut; } // Whenever this button becomes enabled, re-register the shortcut key // with the input filter. public synchronized void enable() { if (filter != null) { filter.add(shortcut, this); } super.enable(); } // Whenever this button becomes disabled, unregister the shortcut key
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch40.htm (15 of 22) [8/14/02 10:58:32 PM]
public synchronized void disable() { if (filter != null) { filter.remove(shortcut, this); } super.disable(); } // If we get a keypress event and the key pressed is the shortcut key, // generate an ACTION_EVENT event for this button. public boolean handleEvent(Event evt) { if ((evt.id == Event.KEY_PRESS) && (evt.key == shortcut)) { return postEvent(new Event(this, Event.ACTION_EVENT, getLabel())); } return super.handleEvent(evt); } // setFilter is called by the SmallDevicePanel class when this button // is added to the panel. The button then registers the shortcut key // with the input filter. public void setFilter(InputFilter filter) { this.filter = filter; if ((filter != null) && isEnabled()) { filter.add(shortcut, this); } } }
// This class implements a text field for entering integers. // Because of some of the peculiarities of text fields, it sets // the field to be non-editable and handles the keystroke events // manually. It allows * to be used as a delete key to key out // potential cell-phone users. public class NumericInputField extends TextField implements FilteredComponent { protected int numDigits; protected InputFilter filter; // When you create the field, you give a limit to the number of digits public NumericInputField(int numDigits) { super(numDigits); this.numDigits = numDigits; setEditable(false); } public boolean handleEvent(Event evt) { // If we get a keypress, check to see if the key is a number if (evt.id == Event.KEY_PRESS) { if ((evt.key >= '0') && (evt.key <= '9')) { // We got a number, see if there's room to add another digit if (getText().length() >= numDigits) { return true; } // To add a digit, we create an array of 1 character, turn it into a // string, and then add that to the current digit string char ch[] = new char[1]; ch[0] = (char)evt.key; setText(getText()+new String(ch)); return true; // If we get a '*', remove the last character in the digit string } else if (evt.key == '*') { String currText = getText(); int len = currText.length(); if (len > 0) { setText(currText.substring(0, len-1)); } return true; } return false; // If we get a mouse down event, pop up a keypad for entering a number } else if (evt.id == Event.MOUSE_DOWN) { if (getParent() instanceof NumberPad) return true; doPad(); return true; // If we get an action event, see if it is an action from the number pad. // When you click "OK" on the number pad, it generates an ACTION_EVENT // and will send it to you if you ask. In this case, when we get that // event, we pop the pad back off the stack layout.
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch40.htm (17 of 22) [8/14/02 10:58:32 PM]
} else if (evt.id == Event.ACTION_EVENT) { if (evt.target instanceof NumberPad) { NumberPad pad = (NumberPad)evt.target; setText(""+pad.getValue()); pad.pop(); return true; } else { return false; } } else { return super.handleEvent(evt); } // } doPad creates a number pad and pushes it onto the stack layout public void doPad() { NumberPad pad = new NumberPad(numDigits, this); pad.push(this); } getValue returns the numeric value of the digit string public int getValue() { try { return Integer.parseInt(getText()); } catch (Exception e) { return 0; } } setFilter tells the input filter what characters we are interested in public void setFilter(InputFilter filter) { this.filter = filter; if (isEnabled()) { filter.add('0', '9', this); filter.add('*', this); } } If this component becomes enabled, re-register the keystrokes with the input filter public void enable() { if (filter != null) { filter.add('0', '9', this); filter.add('*', this); } } If this component becomes disabled, unregister the keystrokes with the input filter public void disable() {
//
//
// //
// //
The NumberPad class used by the NumericInputField class is a very simple panel of 12 buttons (0 through 9, *, and #). It passes the digits and the * key on to the NumericInputField class, and uses the # as an OK button, causing the pad to pop off the screen, sending you back to the previous screen. Listing 40.8 shows the NumberPad class.
Listing 40.8 Source Code for NumberPad.java import java.awt.*; // NumberPad creates a pushable panel of buttons // that resembles a telephone keypad. It has the // digits 0-9 and also * and #. It uses the * key // as delete and # as OK. public class NumberPad extends SmallDevicePanel { protected NumericInputField inputField; protected int numDigits; protected Component notifyMe; // Creates a number pad which will generate an ACTION_EVENT to // itself when OK is pressed. public NumberPad(int numDigits) { this.numDigits = numDigits; notifyMe = this; createPad(); } // Creates a number pad that sends the ACTION_EVENT to another // component when OK is pressed. This allows the NumericInputField // class to pop up a number pad and receive an action event when // OK is pressed. public NumberPad(int numDigits, Component notifyMe) { this.numDigits = numDigits; this.notifyMe = notifyMe; createPad(); } // Create the buttons for the pad protected void createPad() { inputField = new NumericInputField(numDigits); setLayout(new BorderLayout());
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch40.htm (19 of 22) [8/14/02 10:58:32 PM]
add("North", inputField); Panel buttonPanel = new Panel(); buttonPanel.setLayout(new GridLayout(4, 0)); buttonPanel.add(new Button("1")); buttonPanel.add(new Button("2")); buttonPanel.add(new Button("3")); buttonPanel.add(new Button("4")); buttonPanel.add(new Button("5")); buttonPanel.add(new Button("6")); buttonPanel.add(new Button("7")); buttonPanel.add(new Button("8")); buttonPanel.add(new Button("9")); buttonPanel.add(new Button("* DEL")); buttonPanel.add(new Button("0")); buttonPanel.add(new Button("# OK")); add("Center", buttonPanel); // } Return the integer value in the number pad public int getValue() { return inputField.getValue(); } This method handles all the button presses for the keypad. The digit that each button represents is conveniently stored as the first digit in the label. public boolean action(Event evt, Object whichAction) { If this event isn't for a button, we don't handle it if (!(evt.target instanceof Button)) { return false; } char ch = ((String)whichAction).charAt(0); If we get any of the characters that the numeric input field might be interested in, pass them along to it. if (((ch >= '0') && (ch <= '9')) || (ch == '*')) { inputField.postEvent( new Event(evt.target, evt.when, Event.KEY_PRESS, evt.x, evt.y, ch, 0)); return true; If we get a '#', post an action event } else if (ch == '#') { return notifyMe.postEvent(new Event(this, Event.ACTION_EVENT, new Boolean(false))); } return super.handleEvent(evt); }
// // //
//
// //
//
Listing 40.9 shows a very simple test program that demonstrates the various components presented in this chapter. It creates a numeric input field and a button with a shortcut key of #. The idea is that it can be used by a client who has only a telephone keypad, or by someone who has only a pointing device. If you have only a pointing device, you can click the numeric input field to bring up a keypad to enter a number.
Listing 40.9 Source Code for TestField.java import java.awt.*; import java.applet.*; // This is a simple test applet for the SmallDevicePanel and // the NumericInputField classes. It creates a numeric field and // a shortcut button. public class TestField extends Applet { NumericInputField inField; Button okButton; public void init() { setLayout(new StackLayout()); SmallDevicePanel startPanel = new SmallDevicePanel(); startPanel.setLayout(new BorderLayout()); inField = new NumericInputField(8); startPanel.add("North", inField); okButton = new ShortcutButton("# OK", '#'); startPanel.add("South", okButton); add(startPanel); } public boolean action(Event evt, Object whichAction) { if (evt.target instanceof Button) { System.out.println("Your number is "+ inField.getValue()); } return false; } }
Figure 40.8 shows the test applet in operation. The applet itself violates one of the design principles in that it is not selfdocumenting. Its purpose is just to demonstrate the numeric input field. Figure 40.8 : Your interfaces should support different input sources. Figure 40.9 shows the number pad that pops up by the numeric input field. You could use this same approach to create a
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch40.htm (21 of 22) [8/14/02 10:58:32 PM]
small pop-up keyboard. Figure 40.9 : Create auxiliary panels to help users with limited input sources. Whereas these classes may help you get going when designing interfaces for small devices, you really need a full development library geared toward these devices. Hopefully, one will be available by the time Java-enabled hand-held devices become prevalent. Otherwise, you'll have to create many components from scratch.
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f40-1.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f40-2.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f40-3.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f40-4.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f40-5.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f40-6.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f40-7.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f40-8.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f40-9.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f33-1.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f33-2.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f33-3.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f33-4.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f26-1.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f26-2.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f26-3.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f26-4.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f26-5.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f26-6.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f26-7.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f26-8.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f26-9.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f26-10.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f26-11.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f26-12.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f26-13.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f23-2.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f23-3.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f23-4.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f23-5.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f13-1.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f13-2.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f13-3.gif
CONTENTS
G G G G G
Protecting Your Code from Unauthorized Use Embedding Copyrights in Your Code Verifying the Origin of the Applet Hiding Information in Your Applet Obfuscating a Working Program H Make All Your Function and Variable Names Meaningless H Perform Occasional Useless Computations or Loops H Hide Small Numbers in Strings H Create Large Methods H Spread Methods Out Among Subclasses H Using a Commercial Obfuscator
If you intend to take legal recourse if your code is stolen or misused, be sure to consult a lawyer before making the software available on the Web. Many firms specialize in copyright and intellectual property laws, and can advise you on the best ways to protect yourself.
You might include a copyright statement in your code this way: char copyright = "Copyright (c) 1996 by Mark Wutka - "+ "Unauthorized distribution is forbidden. For questions "+ "about licensing this code, send mail to wutka@netcom.com."; You should also include a similar copyright on your Web page: <! Copyright (c) 1996 blah blah blah > If you want to ensure that the copyright notice has been retained on the Web page, you can turn the copyright into a parameter and make the applet check for it. For example: <APPLET codebase="." code="VerifyCopyright.class width=200 height=200> <PARAM name="copyright" value="Copyright (c) 1996 by Mark Wutka - All Rights Reserved"> Your applet then checks for the copyright parameter: import java.applet.*; public class VerifyCopyright extends Applet { public String copyright = "Copyright (c) 1996 by Mark Wutka - All Rights Reserved";
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch12.htm (2 of 7) [8/14/02 10:59:05 PM]
public void init() { String copyrightParam = getParameter("copyright"); if ((copyrightParam == null) || !copyrightParam.equals( copyright)) { throw new SecurityException("Invalid Copyright"); } } } This doesn't stop someone who's clever enough to change the copyright statement in your code, however. But it does remind someone to include the copyright on their Web page if they use your applet.
and strange interactions between variables and functions. There is a market for good code obfuscators, because obfuscation by hand frequently leads to bigger, slower programs. An ideal obfuscator would make your program terribly confusing while not increasing the size, the speed, or the correctness of the program. Keep your eye out for such obfuscators if you are worried about someone stealing your code. Note There is no such thing as security by obscurity. Don't think that you can hide a password in your code without someone figuring it out. Just because you can come up with a function that you wouldn't be able to unravel doesn't mean someone else can't figure it out. You would be amazed at the things people can figure out when deciphering code. If you need secure transactions, such as banking funds transfers, don't even think about obfuscation. Use a secure, encrypted protocol and a signed applet. This is discussed later in this book.
static String alert = "Exception in computation."; which looks like an unobfuscated error message. The letter 'E' has an ASCII value of 69, meaning that you can use 'E' - 64 as the number of ships: int ships = alert.charAt(0) - (1 << 6); // ('E' - 64 or 5) Of course, you would obscure the variable name of ships.
There was a flurry of debate on the Internet after the introduction of a very successful Java decompiler called Mocha. It was able to take a .class file and turn it into a very readable Java source file. It didn't always reproduce the original source code exactly, and comments were missing, of course, but the program worked well enough to cause great concern among Java developers. You can try Mocha for yourself. It is available for free at http://web.inter.nl.net/users/H.P.van.Vliet/mocha.htm. Keep in mind, however, that many source code licenses, including those from Sun, prohibit you from decompiling or disassembling any of their code. The purpose of such a restriction is usually to prevent code theft. Many times, a company won't mind if you are doing the disassembly just to find out how something works or even to fix a crucial bug. While Mocha was causing a storm in the Java community, its author was busy working on the counterpart to Mocha, which is called Crema. The Crema program is a code obfuscator, and does a very good job at jumbling up a .class file. Unlike Mocha, Crema is not free, but you can download it for evaluation purposes. The Crema Web page is located at http://web.inter.nl.net/users/H.P.van.Vliet/crema.html. Obfuscating your code may be a very tough thing for you to do. Hopefully you won't have to do it very often. After all, the Internet fosters a spirit of information sharing, not hiding.
CONTENTS
G G G
Sending E-Mail Sending E-Mail Using the SMTP Protocol Accessing Your Mailbox with the POP3 Protocol
Sending E-Mail
One of the problems you may encounter when designing an application is not being able to run server-side applications. Many Web providers have a limited set of features available for processing form data. If you can't post data to a Web server, you may still be able to receive data from a client applet by sending e-mail. This solution is not useful if you need to create a form that provides instant feedback. It is useful for creating things like a guest book or an order form. The idea here is that instead of using an HTTP POST to send data to a server, you e-mail the information instead. This is far less desirable than the post mechanism because you have to write something that goes through your mailbox and extracts the information.
All commands and responses are terminated by CRLF (in Java, \r\n). For each command the client sends, the server responds
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch11.htm (1 of 19) [8/14/02 10:59:11 PM]
with a text response that starts with a 3-digit return code. Return codes in the 200-299 range indicate a successful command. A return code in the 300-399 range indicates that the command was initially successful but that it needs more information to complete. Commands in the 400-499 and 500-599 range indicate errors. An error in the 400-499 range indicates that there was an error on the server (server down, file system full), while errors in the 500-599 range indicate an error on the client side (invalid password, unknown command). This pattern of error codes is used in many Internet protocols. The format of the return codes is like that of the FTP protocol. If a return code is followed immediately by a '-', the response is multiline (the '-' character is a continuation character). The sequence of messages sent by the client to the server is: 1. HELO"-a simple greeting from the client to the server. This can also include the host name the client would like to be known by. For example: "HELO mail-o-matic.com". Many servers ignore the alternative host name if it is not a valid name for your host. 2. MAIL FROM: sender's address"-identifies the user account that is sending this not have to be a valid account on the client machine. In other words, you could say that the mail was from an account halfway around the world. 3. think this would be a neat way to fake messages from someone, remember that the mail header contains a detailed path of where a message came from, so fake . An example "MAIL FROM" line would be: "MAIL FROM: wutka@netcom.com." 4. RCPT TO: recipient address"-identifies the address where this mail will be sent. Many mail servers do not need this address to be on the server itself. This allows you to bounce messages from one server to another. Web server has a mail server but you want to receive mail from your applets at an account onanother host, you should be able to send mail to your alternative account. The format of the address is the same as in the "MAIL FROM" line: "RCPT TO: wutka@netcom.com". "DATA"-tells the mail server that the client is ready to start sending the body of the mail message. The server should respond with a code in the 300-399 range if this command is successful, indicating that it wants more input-namely, the mail message. After the DATA command, the client sends the mail message one line at a time. When it is finished sending the message, the client sends a "." on a line by itself. If any other line in the message starts with a ".", the client adds an extra "." before the start of the line. Note If you want to experiment with the SMTP protocol and see what the responses look like, you can pretend to be a mail server client by using a telnet program and telnetting to port 25 on some mail server. Just enter the SMTP commands manually and hit return after each one. You should see a response. If you want to abort the message, just disconnect the telnet session sometime before you send the "." to end a message.
Listing 11.1 shows a transcript from an SMTP session performed using the telnet command.
220-flamingo Sendmail 8.6.12/8.6.9 ready at Sun, 22 Sep 1996 21:26:28 -0400 220 ESMTP spoken here HELO 250 flamingo Hello contessa [192.0.0.1], pleased to meet you MAIL FROM: elvis 250 elvis... Sender ok RCPT TO: mark 250 mark... Recipient ok DATA 354 Enter mail, end with "." on a line by itself Subject: Well hey there Uh huh huh. Thank yuh. Thank yuh very much. The King .250 VAA07236 Message accepted for delivery
Listing 11.2 shows a class that implements a session with an SMTP server. The sendMessage method actually sends the message. It automatically closes down the con-nection if the message is sent correctly, but you need to close it down manually if you get an exception.
Listing 11.2 Source Code for SMTPSession.java import java.io.*; import java.net.*; import java.util.*; public class SMTPSession extends Object { public String host; // Host name we connect to public int port; // port number we connect to, default=25 public String recipient; public String sender; public String[] message; protected Socket sessionSock; protected DataInputStream inStream; protected DataOutputStream outStream; public SMTPSession() { } public SMTPSession(String host, String recipient, String sender, String[] message) throws IOException {
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch11.htm (3 of 19) [8/14/02 10:59:11 PM]
this.recipient = recipient; this.message = message; this.sender = sender; } public SMTPSession(String host, int port, String recipient, String sender, String[] message) throws IOException { this.host = host; this.port = port; if (this.port <= 0) this.port = 25; this.recipient = recipient; this.message = message; this.sender = sender; } // Close down the session public void close() throws IOException { sessionSock.close(); sessionSock = null; } // Connect to the server protected void connect() throws IOException { sessionSock = new Socket(host, port); inStream = new DataInputStream( sessionSock.getInputStream()); outStream = new DataOutputStream( sessionSock.getOutputStream()); } // Send a command and wait for a response protected String doCommand(String commandString) throws IOException { outStream.writeBytes(commandString+"\n"); String response = getResponse(); return response; }
// Get a response back from the server. Handles multi-line responses // and returns them as part of the string. protected String getResponse() throws IOException { String response = ""; for (;;) { String line = inStream.readLine(); if (line == null) { throw new IOException( "Bad response from server."); } // FTP response lines should at the very least have a 3-digit number if (line.length() < 3) { throw new IOException( "Bad response from server."); } response += line + "\n"; // If there isn't a '-' immediately after the number, we've gotten the // complete response. ('-' is the continuation character for FTP responses) if ((line.length() == 3) || (line.charAt(3) != '-')) return response; } } // Sends a message using the SMTP protocol public void sendMessage() throws IOException { connect(); // After connecting, the SMTP server will send a response string. Make // sure it starts with a '2' (reponses in the 200's are positive // responses. String response = getResponse(); if (response.charAt(0) != '2') { throw new IOException(response); } // Introduce ourselves to the SMTP server with a polite "HELO" response = doCommand("HELO"); if (response.charAt(0) != '2') { throw new IOException(response);
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch11.htm (5 of 19) [8/14/02 10:59:11 PM]
} // Tell the server who this message is from response = doCommand("MAIL FROM: " + sender); if (response.charAt(0) != '2') { throw new IOException(response); } // Now tell the server who we want to send a message to response = doCommand("RCPT TO: " + recipient); if (response.charAt(0) != '2') { throw new IOException(response); } // Okay, now send the mail message response = doCommand("DATA"); // We expect a response beginning with '3' indicating that the server // is ready for data. if (response.charAt(0) != '3') { throw new IOException(response); } // Send each line of the message for (int i=0; i < message.length; i++) { // Check for a blank line if (message[i].length() == 0) { outStream.writeBytes("\n"); continue; } // If the line begins with a ".", put an extra "." in front of it. if (message[i].charAt(0) == '.') { outStream.writeBytes("."+message[i]+"\n"); } else { outStream.writeBytes(message[i]+"\n"); } } // A "." on a line by itself ends a message. response = doCommand("."); if (response.charAt(0) != '2') {
Listing 11.3 shows a sample guest book applet that e-mails its information to a server. As you can see, the bulk of the applet just deals with setting up the screen components. The portion that sends e-mail is fairly small. Notice that the GuestBookApplet class uses a grid bag layout to arrange the input fields. When you are creating a simple form with labeled fields, this form of the grid bag works very well. The form is essentially a set of right-justified labels and leftjustified input fields. You use the GridBagConstraints.EAST anchor value to right-justify a component, and the GridBagConstraings.WEST anchor to left-justify a component. Since the left-justified components (the input fields, in this case) are the last components in their respective rows, they will all line up with each other. If you were to do this form as a series of nested panels, you wouldn't be able to guarantee that the input fields would line up. If you want to set up a completely automated guest book registration, you will also need to write a program to read the guest book e-mail messages and store them somewhere. You could write such a program using the POP3 protocol, which is discussed in the next section. The program could scan through your mail messages looking for those messages with a subject heading that matches the guest book heading (in the case of the GuestBookApplet program, it's "GUESTBOOK REGISTRATION").
Listing 11.3 Source Code for GuestBookApplet.java import java.applet.*; import java.awt.*; // This applet demonsrates the use of the SMTPSession class to // send e-mail information from an applet. It implements a simple // guest book that reads a name and e-mail address. public class GuestBookApplet extends Applet { protected TextField nameField; protected TextField emailField; protected Button submitButton; public void init() { // in order to lay the applet out where there are labels to the // left of the text fields in a reasonable format, we use the // GridBagLayout layout manager. GridBagLayout layout = new GridBagLayout(); GridBagConstraints c = new GridBagConstraints(); setLayout(layout);
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch11.htm (7 of 19) [8/14/02 10:59:11 PM]
c.weightx = 1.0; c.weighty = 1.0; // Set up a label that is right-justified (anchored EAST) and // is the second to the last element in a row (RELATIVE) Label nameLabel = new Label("Name:"); c.anchor = GridBagConstraints.EAST; c.gridwidth = GridBagConstraints.RELATIVE; layout.setConstraints(nameLabel, c); add(nameLabel); // Set up a left-justified text field that is the last element on the row nameField = new TextField(30); c.anchor = GridBagConstraints.WEST; c.gridwidth = GridBagConstraints.REMAINDER; layout.setConstraints(nameField, c); add(nameField); // Again, right-justifies label, second-to last in row Label emailLabel = new Label("E-Mail address:"); c.anchor = GridBagConstraints.EAST; c.gridwidth = GridBagConstraints.RELATIVE; layout.setConstraints(emailLabel, c); add(emailLabel); // Text field, left justified, last in row emailField = new TextField(30); c.anchor = GridBagConstraints.WEST; c.gridwidth = GridBagConstraints.REMAINDER; layout.setConstraints(emailField, c); add(emailField); // Now create a centered Submit button that is the only thing in its row submitButton = new Button("Submit"); c.anchor = GridBagConstraints.CENTER; c.gridwidth = GridBagConstraints.REMAINDER; layout.setConstraints(submitButton, c); add(submitButton); } public boolean action(Event evt, Object whichAction) { // If someone hits the button, send the registration e-mail if (evt.target == submitButton) { sendRegistration(); return true; } return false; }
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch11.htm (8 of 19) [8/14/02 10:59:11 PM]
protected void sendRegistration() { String[] emailMessage = new String[3]; // emailMessage contains the text of the message. You have to // generate your own subject line in SMTP. Use the name and // the email address as the only two lines in the message body. emailMessage[0] = "Subject: GUESTBOOK REGISTRATION"; emailMessage[1] = nameField.getText(); emailMessage[2] = emailField.getText(); try { // We use dummy e-mail names here, the first one is the name of // the recpient, the second is the name of the sender. Fill in // your specific addresses for this applet. SMTPSession mailSession = new SMTPSession( getDocumentBase().getHost(), "recipient@somewhere.xxx", "sender@.someplace.yyy", emailMessage); // Send the mail message mailSession.sendMessage(); // This applet SHOULD display some sort of positive response to // say that the entry has been submitted, but it doesn't. } catch (Exception e) { // This is a REALLY bogus way to flag an error. You should pop up a // dialog box or something. e.printStackTrace(); } } }
Figure 11.1 shows the guest book in action. Figure 11.1 : A guest book applet can register guests by sending e-mail.
Unlike some of the other Internet protocols, the POP3 protocol does not use numeric responses. Instead, its responses start with a plus for a successful command or a minus in the case of an error. Also, there are only a few specific circumstances when POP3 returns multiline responses. These are in the form of multiple lines terminated by a line containing only a period (exactly the same form that is used by the SMTP DATA command). When you connect to a POP3 server, usually at port 110, you send a USER command with the user name of the mailbox you are reading, followed by a PASS command containing the user's password. Caution Some mail servers store mail in a different place when you use POP3 to read your mail. If you read your mail using the POP3 protocol, and then you go back to using your normal mail reader, you may suddenly find that your mailbox is completely empty. Don't panic. Your mail has probably been copied to another file. For example, if you have a Netcom shell account, your mail is normally stored in .mailbox/inbox. If you read your mail with POP3, however, your mail is moved to .mailbox/inbox.pop.
Once you have logged on to the POP3 server, you can do the following:
G G G
G G
Retrieve a message count using the STAT command. Get a list of active message numbers with the LIST command. Examine the beginning of a message with the TOP command (this command is optional according to the standard but should be available on most servers). Read an entire message with the RETR command. Delete a message with the DELE command.
Listing 11.4 shows a POP3 session performed using telnet. It reads the message sent by the telnet session in Listing 11.1.
Listing 11.4 Telnet Log of POP3 Session +OK flamingo POP3 Server (Version 1.004) ready. USER mark +OK please send PASS command PASS Shhh!!!! +OK 1 messages ready for mark in /usr/spool/mail/mark LIST +OK 1 messages; msg# and size (in octets) for undeleted messages: 1 461 . RETR 1 +OK message 1 (461 octets): X-POP3-Rcpt: mark@flamingo Return-Path: elvis Received: from (contessa [192.0.0.1]) by flamingo (8.6.12/8.6.9) with SMTP id VAA07236 for mark; Sun, 22 Sep 1996 21:26:34 -0400
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch11.htm (10 of 19) [8/14/02 10:59:11 PM]
Date: Sun, 22 Sep 1996 21:26:34 -0400 From: elvis@contessa Message-Id: <199609230126.VAA07236@flamingo> Subject: Well hey there Apparently-To: mark@flamingo Uh huh huh. Thank yuh. Thank yuh very much. The King . DELE 1 +OK message 1 marked for deletion LIST +OK 1 messages; msg# and size (in octets) for undeleted messages: . QUIT +OK flamingo POP3 Server (Version 1.004) shutdown.
As you can see, the POP3 protocol has all the ingredients that enable you to make a nicee-mail reader. All you need is a Java class to do the POP3 protocol. Listing 11.3 shows an excerpt from the POP3Session class, which uses all of the POP3 commands mentioned above (there are a few more optional ones that haven't been covered). The complete source to the POP3Session class is available on the CD that comes with this book. The only part that has been omitted is the section that sets up the host name, port number, user name, and password for the session. The first part of the POP3Session class implements the normal POP3 commands. Each POP3 command is implemented by a separate method. The actual sending and receiving of commands is handled by a small set of methods that understand the format of the methods. As you will see, most of the methods that implement the POP3 commands are very similar. They all send a command and retrieve a response. Some of them return a string response, some of them return no response, and some of them return an array of strings for commands that give a multiline response. Listing 11.5 gives the source code for the POP3Session class.
Listing 11.5 Source Code for POP3Session.java import java.io.*; import java.net.*; import java.util.*; // // // // // // // This class implements a POP3 (Post Office Protocol 3) session with a mail server. It allows you to create remote mail readers. You create a POP3 session by providing a host name and a username/password combination for the user whose mailbox you are reading. After creating an instance of this class, you must call the connect method to actually connect to the server. You must always close the connection manually with the close method.
protected Socket pop3Sock; protected DataInputStream inStream; protected DataOutputStream outStream; // The host name and port we connect to. Default POP3 port is 110 public String host; public int port; // The user name and password of the mailbox we want public String userName; public String password; public POP3Session() { } public POP3Session(String host, String userName, String password) { this.host = host; this.port = 110; this.userName = userName; this.password = password; } public POP3Session(String host, int port, String userName, String password) { this.host = host; this.port = port; this.userName = userName; this.password = password; } // POP3 positive responses start with a '+', negative responses start with '-' // isErrorResponse returns true if a response does not start with a '+' protected boolean isErrorResponse(String str) { return str.charAt(0) != '+'; } // fetches the current number of messages using the POP3 STAT commant public int getMessageCount() throws IOException { // Send the command String response = doCommand("STAT"); // Check for error if (isErrorResponse(response)) { throw new IOException(response);
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch11.htm (12 of 19) [8/14/02 10:59:11 PM]
} // // // // The format of the response is +OK # number after the OK, but we need to We take the substring from offset 4 up to the first space, then convert try { int count = Integer.valueOf(response.substring(4, response.indexOf(' ', 4))). intValue(); return count; } catch (Exception e) { throw new IOException("Invalid response - "+response); } } // Get headers returns a list of message numbers along with some sizing // information, and possibly other information depending on the server. public String[] getHeaders() throws IOException { String response = doCommand("LIST"); if (isErrorResponse(response)) { throw new IOException(response); } return getData(); } // Get header returns the message number and message size for // a particular message number. It may also contain other information public String getHeader(int messageNumber) throws IOException { String response = doCommand("LIST "+messageNumber); if (isErrorResponse(response)) { throw new IOException(response); } return response; } // Retrieves the entire text of a message using the POP3 RETR command public String[] getMessage(int messageNumber) throws IOException { String response = doCommand("RETR "+messageNumber); other text, we are interested in the stop parsing before the other text. (the start of the number) and go that string to a number.
if (isErrorResponse(response)) { throw new IOException(response); } return getData(); } // Retrieves the first <linecount> lines of a message using the POP3 TOP // command. Note: this command may not be available on all servers. If // it isn't available, you'll get an exception. public String[] getMessageHead(int messageNumber, int lineCount) throws IOException { String response = doCommand("TOP "+messageNumber+" "+ lineCount); if (isErrorResponse(response)) { throw new IOException(response); } return getData(); } // deletes a particular message public void deleteMessage(int messageNumber) throws IOException { String response = doCommand("DELE "+messageNumber); if (isErrorResponse(response)) { throw new IOException(response); } } // Undoes any pending deletions public void reset() throws IOException { String response = doCommand("RSET"); if (isErrorResponse(response)) { throw new IOException(response); } } // Initiates a graceful exit public void quit() throws IOException {
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch11.htm (14 of 19) [8/14/02 10:59:11 PM]
String response = doCommand("QUIT"); if (isErrorResponse(response)) { throw new IOException(response); } } // Connects to the POP2 server and logs on with the USER and PASS commands public void connect() throws IOException { // Make the connection pop3Sock = new Socket(host, port); inStream = new DataInputStream(pop3Sock.getInputStream()); outStream = new DataOutputStream(pop3Sock.getOutputStream()); // Send a logon (USER) command String response = doCommand("USER "+userName); if (isErrorResponse(response)) { throw new IOException(response); } // Send a PASS command response = doCommand("PASS "+password); if (isErrorResponse(response)) { throw new IOException(response); } }
Notice that there is a lot of repetition in the methods that perform the different POP3 commands. These methods all use the doCommand method to send a command and then use isErrorResponse to see if the command resulted in an error. You could combine these steps into a single method. In addition, the POP3 commands either return a string, an array of strings, an integer, or no value. You could create command methods that execute POP3 commands and return each of these result types. In general, you do this kind of grouping if you have a large number of each command type. If you only have one or two, it may not be worth the effort, unless you think that there may be more in the future. The rest of the POP3Session class deals with establishing a connection and the sending and receiving of data. For most Internet protocols, you use the same format, or a small group of formats for all commands. You take advantage of this fact by writing methods that send commands in the format that the protocol expects. In the case of the POP3 protocol, every command returns a one-line response. The doCommand method sends a command string and waits for the response line. You can determine whether a response line is an error response by using the isErrorResponse method. Several POP3 commands also return multiple lines after the initial response. When you receive a '.' by itself on a line, you have reached the end of the response. Since a mail message might contain a '.' on a line by itself, POP3 specifies that any line beginning with a '.' will have an extra '.' at the beginning. The POP3 method handles these multi-line responses and returns an array of strings containing the lines in the response as shown in Listing 11.5.
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch11.htm (15 of 19) [8/14/02 10:59:11 PM]
Listing 11.5 Source Code for POP3Session.java // Connects to the POP2 server and logs on with the USER and PASS commands public void connect() throws IOException { // Make the connection pop3Sock = new Socket(host, port); inStream = new DataInputStream(pop3Sock.getInputStream()); outStream = new DataOutputStream(pop3Sock.getOutputStream()); // Send a logon (USER) command String response = doCommand("USER "+userName); if (isErrorResponse(response)) { throw new IOException(response); } // Send a PASS command response = doCommand("PASS "+password); if (isErrorResponse(response)) { throw new IOException(response); } } // Shuts down the connection immediately. You should call this if you // get an exception. public void close() throws IOException { pop3Sock.close(); pop3Sock = null; } // Sends a POP3 command and retrieves the response protected String doCommand(String command) throws IOException { outStream.writeBytes(command+"\n"); String response = inStream.readLine(); return response; } // Retrieves a multi-line POP3 response. If a line contains "." by itself, // it is the end of the response. If a line starts with a ".", it should // really have two "."'s We strip off the leading "." protected String[] getData()
throws IOException { // Don't know how many lines we're getting, so put them in a vector first Vector lines = new Vector(); String line; // Read lines from the server while ((line = inStream.readLine()) != null) { // If we get a "." on a line by itself, that's the end of the multi-line // response. Create a string array and copy the lines of the response // into it. if (line.equals(".")) { // Create the array to return String response[] = new String[ lines.size()]; // Copy the strings from the vector into the array lines.copyInto(response); return response; } // If a line starts with a ".", strip it off. if ((line.length() > 0) && (line.charAt(0) == '.')) { line = line.substring(1); } lines.addElement(line); } throw new IOException("Connection closed."); } }
Note Remember that applets are usually restricted to making socket connections only to the host they were loaded from. This means that your applet must be loaded from the POP3 server for you to use the POP3Session class in an applet. This is not unreasonable, however, since many Web servers also run the POP3 service for local e-mail accounts.
Listing 11.6 shows a very simple application that tests the features of the POP3Session class. Remember to replace YourPOP3Server, YourUserName, and YourPassword with your own values.
Listing 11.6 Source Code for TestPOP3.java public class TestPOP3 extends Object { public static void main(String[] args) { try { POP3Session pop3 = new POP3Session("YourPOP3Host", "YourUserName", "YourPassword"); // Connect to the server pop3.connect(); // Get a message count System.out.println("There are "+pop3.getMessageCount()+ " messages."); // Get a list of messages (the results look pretty boring) String[] headers = pop3.getHeaders(); System.out.println("Message headers:"); for (int i=0; i < headers.length; i++) { System.out.println(headers[i]); } // Try fetching message #1, hopefully there will be one String[] message = pop3.getMessage(1); System.out.println("Message #1"); for (int i=0; i < message.length; i++) { System.out.println(message[i]); } // Try fetching message #99. Unless your mailbox is really full, there won't // be one. We are expecting that and we try to cetch the exception. try { String header = pop3.getHeader(99); } catch (Exception e) { System.out.println("Got error getting message #99, good!"); } // Tell the server we're through pop3.quit(); // Close down the socket pop3.close(); } catch (Exception e) { // If we get any error at all, just print a stack trace e.printStackTrace();
} } }
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f11-1.gif
CONTENTS
G
G G
G G G G
Organizing Your Data for a Relational Database H Using SQL H Combining Data from Multiple Tables Using Joins Designing Client/Server Database Applications H Client/Server System Tiers H Handling Transactions H Dealing with Cursors H Replication H How Does JDBC Work? H JDBC Security Model H Accessing ODBC Databases with the JDBC-ODBC Bridge H JDBC Classes-Overview H Anatomy of a JDBC Application H JDBC API Examples The Connection Class Handling SQL Statements H Creating and Using Direct SQL Statements H Creating and Using Compiles SQL Statements (PreparedStatement) H Calling Stored Procedures (CallableStatement) Retrieving Results in JDBC Handling Exceptions in JDBC-SQLException Class Handling Exceptions in JDBC-SQLWarnings Class Handling Date and Time H java.sql.Date H java.sql.Time H java.sql.Timestamp Handling SQL Types H java.sql.Types JDBC in Perspective
Standard relational data access is very important for Java programs because the Java applets by nature are not monolithic, allconsuming applications. As applets by nature are modular, they need to read persistent data from data stores, process the data, and write the data back to data stores for other applets to process. Monolithic programs could afford to have their own
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch15.htm (1 of 31) [8/14/02 10:59:26 PM]
proprietary schemes of data handling. But as Java applets cross operating system and network boundaries, you need published open data access schemes. The Java Database Connectivity (JDBC) of the Java Enterprise API's JavaSoft is the first of such cross-platform, crossdatabase approaches to database access from Java programs. From a developer's point of view, JDBC is the first standardized effort to integrate relational databases with Java programs. JDBC has opened all the relational power that can be mustered to Java applets and applications. In this chapter, you will see how JDBC can be effectively used to develop database programs using Java. First, you will look at some basics applicable to databases in general.
During the 1970s and 1980s, the hierarchical scheme was very popular. This scheme treats data as a tree-structured system with data records forming the leaves. Examples of the hierarchical implementations are schemes like b-tree and multi-tree data access. In the hierarchical scheme, to get to data, users need to traverse up and down the tree structure. The most common relationship in a hierarchical structure is a one-to-many relationship between the data records, and it is difficult to implement a many-to-many relationship without data redundancy. The network data model solved this problem by assuming a multi-relationship between data elements. In contrast to the hierarchical scheme where there is a parent-child relationship, in the network scheme, there is a peer-to-peer relationship. Most of the programs developed during those days used a combination of the hierarchical and network data storage and access model. During the 90s, the relational data access scheme came to the forefront. The relational scheme views data as rows of information. Each row contains columns of data, called fields. The main concept in the relational scheme is that the data is uniform. Each row contains the same number of columns. One such collection of rows and columns is called a table. Many such tables (which can be structurally different) form a relational database. Figure 15.1 shows a sample relational database schema (or table layout) for an enrollment database. In this example, the database consists of three tables: the Students Table that contains student information, the Courses Table that has the courses information, and the StudentCourses Table that has the student course relation. The Students Table has student ID, name, address, and so on; the Courses Table contains the course ID, subject name or course title, term offered, location, and so on. Figure 15.1 : A sample relational database schema for the Enrollment Database. Now that you have the student and course tables of data, how do you relate the tables? That is where the relational part of the relational database comes in the picture. To relate two tables, either the two tables will have a common column, or you will need to create a third table with two columns, one from the first table and the second from the second table.
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch15.htm (2 of 31) [8/14/02 10:59:26 PM]
Take a look at how this is done. In this example, to relate the Students Table with the Courses Table, you need to make a new StudentCourses Table which has two columns: Student_ID and Course_ID. Whenever a student takes a course, make a row in the StudentCourses Table with the Student_ID and the Course_ID. Thus, the table has the student and course relationship. If you want to find a list of students and the subjects they take, go to the Student Courses Table, read each row, find the student name corresponding to the Student_ID, from the Courses Table find the course title corresponding to the Course_ID, and select the Student_Name and the Course_Title columns.
Using SQL
Once relational databases started becoming popular, database experts wanted a universal database language to perform actions on data. The answer was SQL, or Structured Query Language. The SQL existed before the relational concepts but the association of SQL and relational database concepts made SQL grow into a mainstream database language.SQL has constructs for: 1. 2. 3. 4. 5. manipulation, such as create, update, and delete. definition, such as create tables and columns. for restricting access to data elements and creating users and groups. management, including backup, bulk copy, and bulk update. Most importantly, transaction processing-SQL is used along with , C++, and others.
data handling and interaction with the back-end database management system. Tip Each database vendor has their own implementation of the SQL. In the Microsoft SQL server,which is one of the client/server relational DMBS, the SQL is called the Transact/SQL, while the Oracle SQL is called the PL/SQL. The different vendors have different extensions to the common X/Open and ANSI X3H2 standard. For the most part, SQL = SQL on any platform. The differences come in framework additions designed to take advantage of a particular database's functionality or capabilities.
Note SQL became an ANSI (American National Standards Institute) standard in 1986 and later was revised to become SQL-92. JDBC is SQL-92-compliant.
15.1 with a few records as shown in Tables 15.1, 15.2, and 15.3. In these tables, I show only the relevant fields or columns. Table 15.1 Students Table Student_ID 1 2 3 4 Student_Name John Mary Jan Jack Table 15.2 Courses Table Course_ID S1 S2 S3 S4 Course_Title Math English Computer Logic
Table 15.3 StudentCourses Table Student_ID 2 3 4 Inner Join A simple join called the inner join with the Students and StudentCourses Tables will give you a table like the one shown in Table 15.4. That is, you get a new table which combines the Students and StudentCourses Tables by adding the Student_Namecolumn to the StudentCourses Table. Table 15.4 Inner Join Table Student_ID 2 3 4 Student_Name Mary Jan Jack Course_ID S2 S1 S3 Course_ID S2 S1 S3
Just because you are using the Student_ID to link the two tables does not mean that you should fetch that column. You can exclude the key field from the result table of an inner join. The SQL statement for this inner join is as follows: SELECT Students.Student_Name, StudentCourses.Course_ID FROM Students, StudentCourses
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch15.htm (4 of 31) [8/14/02 10:59:27 PM]
WHERE Students.Student_ID = StudentCourses.Student_ID Outer Join An outer join between two tables (say Table1 and Table2) occurs when the result table has all the rows of the first table and the common records of the second table. (The first and second table are determined by the order in the SQL statement.) If you assume a SQL statement with the "FROM Table1,Table2" clause, in a left outer join, all rows of the first table (Table1) and common rows of the second table (Table2) are selected. In a right outer join, all records of the second table (Table2) and common rows of the first table (Table1) are selected. A left outer join with the Students Table and the StudentCourses Table creates Table 15.5. Table 15.5 Outer Join Table Student_ID 1 2 3 4 Student_Name John Mary Jan Jack Course_ID <null> S2 S1 S3
This join is useful if you want the names of all students, regardless of whether they are taking any subjects this term, and the subjects taken by the students who have enrolled in this term. Some people call it an if-any join, as in, "Give me a list of all students and the subjects they are taking, if any." The SQL statement for this outer join is as follows: (oj = Outer Join) SELECT Students.Student_ID,Students.Student_Name,StudentCourses.Course_ID FROM { oj c:\enrol.mdb Students LEFT OUTER JOIN c:\enrol.mdb StudentCourses ON Students.Student_ID = StudentCourses .Student_ID } The full outer join, as you may have guessed, returns all the records from both the tables merging the common rows, as shown in Table 15.6. Table 15.6 Full Outer Join Table Student_ID 1 2 3 4 <null> Subtract Join Student_Name John Mary Jan Jack <null> Course_ID <null> S2 S1 S3 S4
What if you want only the students who haven't enrolled in this term or the subjects who have no students (the tough subjects or professors)? Then, you resort to the subtract join. In this case, the join returns the rows that are not in the second table. Remember, a subtract join has only the fields from the first table. By definition, there are no records in the second table. The SQL statement looks like the following: SELECT Students.Student_Name FROM { oj c:\enrol.mdb Students LEFT OUTER JOIN c:\enrol.mdb StudentCourses ON Students.Student_ID = StudentCourses .Student_ID } WHERE (StudentCourses.Course_ID Is Null) General Discussion on Joins and SQL Statements There are many other types of joins, such as the self join, which is a left outer join of two tables with the same structure. An example is the assembly/parts explosion in a Bill of Materials application for manufacturing. But usually the join types that we have discussed so far are enough for normal applications. As you gain more expertise in SQL statements, you will start developing exotic joins. In all of these joins, you were comparing columns that have the same values; these joins are called equi-joins. Joins are not restricted to comparing columns of equal values. You can join two tables based on column value conditions (such as the column of one table being greater than the other). One more point: For equi-joins, as the column values are equal, you retrieved only one copy of the common column. Then, the joins are called natural joins. When you have a non equi-join, you might need to retrieve the common columns from both tables. Once a SQL statement reaches a database management system, the DBMS parses the SQL statement and translates the SQL statements to an internal scheme called a query plan to retrieve data from the database tables. This internal scheme generator, in all the client/server databases, includes an optimizer module. This module, which is specific to a database, knows the limitations and advantages of the database implementation. In many databases-for example, the Microsoft SQL Server-the optimizer is a cost-based query optimizer. When given a query, this optimizer generates multiple query plans, computes the cost estimates for each (knowing the data storage schemes, page I/O, and so on), and then determines the most efficient access method for retrieving the data, including table join order and index usage. This optimized query is converted into a binary form called the execution plan, which is executed against the data to get the result. There are known cases where straight queries take hours to perform that when run through an optimizer have resulted in an optimized query, which is performed in minutes. All the major client/server databases have the query optimizer module built in, which processes all the queries. A database system administrator can assign values to parameters such as cost, storage scheme, and so on, and fine-tune the optimizer.
Take the case of Federal Express. Their Web site can now schedule package pickups, track a package from pickup to delivery, and get delivery information and time. You are now on the threshold of an era where online commerce will be as common as shopping malls. Now, look at some of the concepts that drive these kinds of systems.
GUI Graphical User Interface, which consists of the screens, windows, buttons, list boxes, and so on. Business Logic The part of the program that deals with the various data element interactions. All processing is done based on values of data elements. A good example is the logic for determining the credit limit depending on the annual income. Another business logic is the calculation of income tax based on the tax tables (even though some people consider it illogical!). In manufacturing systems, a reorder point calculation logic based on the material usage belongs in the business logic category. DBMS The Database Management System that deals with the actual storage and retrieval of data.
Two-Tier Systems On the basic level, a two-tier system involves the GUI and business logic, directly accessing the database. The GUI can be on a client system, and the database can be on the client system or on a server. Usually, the GUI is written in languages like C++, Visual Basic, PowerBuilder, Access Basic, and Lotus Script. The database systems typically are Microsoft Access, Lotus Approach, Sybase "SQL Anywhere," or Watcom DB Engine and Personal Oracle. Three-Tier Systems Most of the organizational and many of the departmental client/server applications today follow the three-tier strategy, where the GUI, business logic, and the DBMS are in logically three layers. Here, the GUI development tools are Visual Basic, C++, and PowerBuilder. The middle-tier development tools also tend to be C++ or Visual Basic, and the back-end databases are Oracle, Microsoft SQL Server, or Sybase SQL Server. The three-tier concept gave rise to an era of database servers, application servers, and GUI client machines. Operating systems such as UNIX, Windows NT, and Solaris rule the application server and database server world. Client operating systems like Windows are popular for the GUI front end. Multi-Tier Systems Now with Internet and Java, the era of "network is the computer" and "thin client" paradigm shifts have begun. The Java applets with their own objects and methods created the idea of the multi-tiered client/server systems. Theoretically, a Java
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch15.htm (7 of 31) [8/14/02 10:59:27 PM]
applet can be a business rule, GUI, or DBMS interface. Each applet can be considered a layer. In fact, the Internet and Java were not the first to introduce the object-oriented, multi-tiered systems concept. OMG's CORBA architecture and Microsoft's OLE (now ActiveX) architectures are all proponents of modular object-oriented, multi-platform systems. With Java and the Internet, these concepts became much easier to implement. In short, the systems' design and implementation progressed from two-tiered architecture to three-tiered architecture to the current inter-networked, Java applet-driven multi-tier architecture.
Handling Transactions
The concept of transactions is an integral part of any client/server database. A transaction is a group of SQL statements that update, add, and delete rows and fields in a database. Transactions have an all or nothing property-either they are committed if all statements are successful, or the whole transaction is rolled back if any of the statements cannot be executed successfully. Transaction processing assures the data integrity and data consistency in a database. Note JDBC supports transaction processing with the commit() and rollback() methods. Also, JDBC has the autocommit() which, when on, all changes are committed automatically and, if off, the Java program has to use the commit() or rollback() methods to effect the changes to the data.
Transaction ACID Properties The characteristics of a transaction are described in terms of the Atomicity, Consistency, Isolation, and Durability (ACID) properties. A transaction is atomic in the sense that it is an entity. All the components of a transaction happen or do not happen. There is no partial transaction. If only a partial transaction can happen, then the transaction is aborted. The atomicity is achieved by the commit() or rollback() methods. A transaction is consistent because it does not perform any actions that violate the business logic or relationships between data elements. The consistent property of a transaction is very important when you develop a client/server system, because there will be many transactions to a data store from different systems and objects. If a transaction leaves the data store inconsistent, all other transactions also would potentially be wrong, resulting in a system-wide crash or data corruption. A transaction is isolated because the results of a transaction are self-contained. They do not depend on any preceding or succeeding transaction. This is related to a property called serializability, which means the sequence of transactions are independent; in other words, a transaction does not assume any external sequence. Finally, a transaction is durable, meaning the effects of a transaction are permanent even in the face of a system failure. That means some form of permanent storage should be a part of a transaction. Distributed Transaction Coordinator A related topic in transactions is the coordination of transactions across heterogeneous data sources, systems, and objects. When the transactions are carried out in one relational database, you can use the commit(), rollback(), beginTransaction(), and endTransaction() statements to coordinate the process. But what if you have diversified systems participating in a transaction? How do you handle such a system? As an example, look at the Distributed Transaction
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch15.htm (8 of 31) [8/14/02 10:59:27 PM]
Coordinator (DTC) available as a part of Microsoft SQL Server 6.5 database system. In the Microsoft DTC, a transaction manager facilitates the coordination. Resource managers are clients that implement resources to be protected by transactions-for example, relational databases and ODBC data sources. An application begins a transaction with the transaction manager, and then starts transactions with the resource managers, registering the steps (enlisting) with the transaction manager. The transaction manager keeps track of all enlisted transactions. The application, at the end of the multi-data source transaction steps, calls the transaction manager to either commit or abort the transaction. When an application issues a commit command to the transaction manager, the DTC performs a two-phase commit protocol: 1. It queries each resource manager if it is prepared to commit. 2. If all resources are prepared to commit, DTC broadcasts a commit message to all of them. The Microsoft DTC is an example of very powerful next generation transaction coordinators from the database vendors. As more and more multi-platform, object-oriented Java systems are being developed, this type of transaction coordinators will gain importance. Already, many middleware vendors are developing Java-oriented transaction systems.
A cursor can be viewed as the underlying data buffer. A fully scrollable cursor is one where the program can move forward and backward on the rows in the data buffer. If the program can update the data in the cursor, it is called a scrollable, updatable cursor. Caution
An important point to remember when you think about cursors is the transaction isolation. If a user is updating a row, other users might be viewing the row in a cursor of their own. Data consistency is important here. Worse, the other users also might be updating the same row!
Tip The ResultSet in JDBC API is a cursor. But it is only a forward scrollable cursorthis means you can move only forward using the getNext() method.
ODBC Cursor Types ODBC cursors are very powerful in terms of updatability, concurrency, data integrity, and functionality. The ODBC cursor scheme allows positioned delete and update and multiple row fetch (called a rowset) with protection against lost updates. ODBC supports static, keyset-driven, and dynamic cursors. In the static cursor scheme, the data is read from the database once, and the data is in the snapshot recordset form. Because the data is a snapshot (a static view of the data at a point of time), the changes made to the data in the data source by other users are not visible. The dynamic cursor solves this problem by keeping live data, but this takes a toll on network traffic and application performance. The keyset-driven cursor is the middle ground where the rows are identified at the time of fetch, and thus changes to the data can be tracked. Keyset-driven cursors are useful when you implement a backward scrollable cursor. In a keyset-driven cursor, additions and deletions of entire rows are not visible until a refresh. When you do a backward scroll, the driver fetches the newer row if any changes are made. Note ODBC also supports a modified scheme, where only a small window of the keyset is fetched, called the mixed cursor, which exhibits the keyset cursor for the data window and a dynamic cursor for the rest of the data. In other words, the data in the data window (called a RowSet) is keyset-driven, and when you access data outside the window, the dynamic scheme is used to fetch another keyset-driven buffer.
Cursor Applications You might be wondering where these cursor schemes are applied and why we need such elaborate schemes. In short, all the cursor schemes have their place in information systems.
Static Cursors
Static cursors provide a stable view of the data, because the data does not change. They are good for data mining and data warehousing types of systems. For these applications, you want the data to be stable for reporting executive information systems or for statistical or analysis purposes. Also, the static cursor outperforms other schemes for large amounts of data retrieval.
Dynamic Cursors
On the other hand, for online ordering systems or reservation systems, you need a dynamic view of the system with row locks and views of data as changes are made by other users. In such cases, you will use the dynamic cursor. In many of these applications, the data transfer is small, and the data access is performed on a row-byrow basis. For these online applications, aggregate data access is very rare. Bookmark Bookmark is a concept related to the cursor model, but is independent of the cursor scheme used. Bookmark is a placeholder for a data row in a table. The application program requests a bookmark for a row from the underlying database management system. The DBMS usually returns a 32-bit marker which can be later used by the application program to get to that row of data. In ODBC, you will use the SQLExtendedFetch function with SQL_FETCH_BOOKMARK option to get a bookmark. The bookmark is useful for increasing performance of GUI applications, especially the ones where the data is viewed through a spreadsheet-like interface. Positioned Update/Delete This is another cursor-related concept. If a cursor model supports positioned update/delete, then you can update/delete the current row in a result set without any more processing, such as a lock, read, or fetch. In SQL, a positioned update or delete statement is in the form of: UPDATE/DELETE <Field or Column values etc.> WHERE CURRENT OF <cursor name> The positioned update statement to update the fields in the current row is UPDATE <table> SET <field> = <value> WHERE CURRENT OF <cursor name> The positioned delete statement to delete the current row takes the form of: DELETE <table> WHERE CURRENT OF <cursor name> Generally, for this type of SQL statement to work, the underlying driver or the DBMS has to support updatability, concurrency, and dynamic scrollable cursors. But there are many other ways of providing the positioned update/delete capability at the application program level. Presently, JDBC does not support any of the advanced cursor functionalities. However, as the JDBC driver development progresses, I am sure there will be very sophisticated cursor management methods available in the JDBC API.
Replication
Data replication is the distribution of corporate data to many locations across the organization, and it provides reliability, faulttolerance, data-access performance due to reduced communication, and, in many cases, manageability as the data can be managed as subsets. As you have seen, the client/server systems span an organization, possibly its clients and suppliers, most probably in a wide geographic locations. Systems spanning the entire globe are not uncommon when you're talking about mission-critical applications, especially in today's global business market. If all the data is concentrated in a central location, it would be almost impossible for the systems to effectively access data and offer high performance. Also, if data is centrally located, in the case of mission-critical systems, a single failure will bring the whole business down. Using replicated data across an
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch15.htm (11 of 31) [8/14/02 10:59:27 PM]
organization at various geographic locations is a sound strategy. Different vendors handle replication differently. For example, the Lotus Notes group-ware product uses a replication scheme where the databases are considered peers, and additions/updates/deletions are passed between the databases. Lotus Notes has replication formulas that can select subsets of data to be replicated based on various criteria. The Microsoft SQL server, on the other hand, employs a publisher-subscriber scheme where a database or part of a database can be published to many subscribers. A database can be a publisher and a subscriber. For example, the western region can publish its slice of sales data while receiving (subscribing to) sales data from other regions. There are many other replication schemes from various vendors to manage and decentralize data. Replication is a young technology that is slowly finding its way into many other products. Now it is time for you to dive deep into the main topic, JDBC.
JavaSoft introduced the JDBC API specification in March 1996 as draft Version 0.50 and was open for public review. The specification went from Version 0.50 to 0.60 to 0.70 and now is at Version 1.01, dated August 8, 1996. The JDBC Version 1.01 specification available at http://splash.javasoft.com/jdbc/ (jdbc-0101.ps or jdbc-0101.pdf) includes all of the improvements from the four months of review by vendors, developers, and the general public. Most probably, by the time you are reading this chapter, JDBC Version 1.1 or even 2.0 might be available ! Now, look at the origin and design philosophies. The JDBC designers based the API on X/Open SQL Call Level Interface (CLI). It is not coincidental that ODBC is also based on the X/Open CLI. The JavaSoft engineers wanted to gain leverage from the existing ODBC implementation and development expertise, thus making it easier for Independent Software Vendors (ISVs) and system developers to adopt JDBC. But ODBC is a C interface to DBMSs and thus is not readily convertible to Java. So JDBC design followed ODBC in spirit as well in its major abstractions and implemented the SQL CLI with "a Java interface that is consistent with the rest of the Java system," as it is described in Section 2.4 of the JDBC specification. For example, instead of the ODBC SQLBindColumn and SQLFetch to get column values from the result, JDBC used a simpler approach (which you see later).
statements to the underlying DBMS through the statement object, and retrieves the results as well as information about the result sets. Typically, the JDBC class files and the Java applet/application reside in the client. They could be downloaded from the network also. To minimize the latency during execution, it is better to have the JDBC classes in the client. The Database Management System and the data source are typically located in a remote server. Figure 15.2 shows the JDBC communication layer alternatives. The applet/application and the JDBC layers communicate in the client system, and the driver takes care of interacting with the database over the network. Figure 15.2 : JDBC database communication layer alternatives. The JDBC driver can be a native library, like the JDBCODBC Bridge, or a Java class talking across the network to an RPC or Jeeves Servlet or HTTP listener process in the database server. The JDBC classes are in the java.sql package, and all Java programs use the objects and methods in the java.sql package to read from and write to data sources. A program using the JDBC will need a driver for the data source with which it wants to interface. This driver can be a native module (like the JDBCODBC.DLL for the Windows JDBC-ODBC Bridge developed by Sun/Intersolv), or it can be a Java program that talks to a server in the network using some RPC or Jeeves Servlet or an HTTP talker-listener protocol. Both schemes are shown in Figure 15.2. Note As you can see from Figure 15.2, JDBC can be implemented as a native driver or as a gateway to an RPC. Which implementation is better is a question that will be answered as the JDBC architecture matures. One reason to implement a native library is the advantage of speed. Also, local databases could be handled using native libraries more easily than gateways. On the other hand, for a handheld device or a network computer, "network is the system." For these devices, a full Java implementation of JDBC that talks to an RPC type of system or a Jeeves servlet on the database server is a good solution.
It is conceivable that an application will deal with more than one data source-possibly heterogeneous data sources. (A database gateway program is a good example of an application that accesses multiple heterogeneous data sources.) For this reason, JDBC has a DriverManager whose function is to manage the drivers and provide a list of currently loaded drivers to the application programs. Note Even though the word Database is in the name JDBC, the form, content, and location of the data is immaterial to the Java program using JDBC, so long as there is a driver for that data. Hence, the notation data source to describe the data is more accurate than Database, DBMS, DB, or just file. In the future, Java devices such as televisions, answering machines, or network computers will access, retrieve, and manipulate different types of data (audio, video, graphics, time series, and so on) from various sources that are not relational databases at all! And much of the data might not even come from mass storage. For example, the data could be video stream from a satellite or audio stream from a telephone. ODBC also refers to data sources, rather than databases when being described in general terms.
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch15.htm (13 of 31) [8/14/02 10:59:27 PM]
JDBC Classes-Overview
When you look at the class hierarchy and methods associated with it, the topmost class in the hierarchy is the DriverManager. The DriverManager keeps the driver infor-mation, state information, and so on. When each driver is loaded, it registers with the DriverManager. The DriverManager, when required to open a connection, selects the driver depending on the JDBC URL. Note True to the nature of the Internet, JDBC identifies a database with an URL. The URL is of the form: jdbc:<subprotocol>:<subname related to the DBMS/Protocol> For databases on the Internet/intranet, the subname can contain the Net URL //hostname:port/ The <subprotocol> can be any name that a database understands. The odbc subprotocol name is reserved for ODBC-style data sources. A normal ODBC database JDBC URL looks like: jdbc:odbc:<>;User=<>;PW=<>
If you are developing a JDBC driver with a new subprotocol, it is better to reserve the subprotocol name with JavaSoft, which maintains an informal subprotocol registry.
The java.sql.Driver class is usually referred to for information such as PropertyInfo, version number, and so on. So the class could be loaded many times during the execution of a Java program using the JDBC API. Looking at the java.sql.Driver and java.sql.DriverManager classes and methods, as listed in Table 15.9, you see that the DriverManager returns a Connection object when you use the getConnection() method. Other useful methods include the registerDriver(), deRegister(), and getDrivers() methods. Using the getDrivers() method, you can get a list of registered drivers. Figure 15.3 shows the JDBC class hierarchy, as well as the flow of a typical Java program using the JDBC APIs. Figure 15.3 : JDBC class hierarchy and a JDBC API flow. In the next subsection, follow the steps required to access a simple database access using JDBC and the JDBC-ODBC driver.
details. But wait, where do you put the rest of the details? This is where the ODBC setup comes into the picture. The ODBC Setup program runs outside the Java application from the Microsoft ODBC program group. The ODBC Setup program allows you to set up the data source so that this information is available to the ODBC Driver Manager, which, in turn, loads the Microsoft Access ODBC driver. If the database is in another DBMS form-say, Oracle-you configure this source as Oracle ODBC driver. In Windows 3.x, the Setup program puts this information in the ODBC.INI file. With Windows 95 and Windows NT 4.0, this information is in the Registry. Figure 15.5 shows the ODBC setup screen. Figure 15.5 : ODBC setup for the example database. After this setup, the example database URL is jdbc:odbc:StudentDB;uid="admin";pw="sa". Querying a Database with JDBC In Listing 15.1, you will list all of the students in the database by a SQL SELECT statement. The steps required to accomplish this task using the JDBC API are iterated as follows. For each step, the Java program code with the JDBC API calls follows the description of the steps.
Listing 15.1 Using a SQL SELECT Statement //Declare a method and some variables. public void ListStudents() throws SQLException { int i, NoOfColumns; String StNo,StFName,StLName; //Initialize and load the JDBC-ODBC driver. Class.forName ("jdbc.odbc.JdbcOdbcDriver"); //Make the connection object. Connection Ex1Con = DriverManager.getConnection( "jdbc:odbc:StudentDB;uid="admin";pw="sa"); //Create a simple Statement object. Statement Ex1Stmt = Ex1Con.createStatement(); //Make a SQL string, pass it to the DBMS, and execute the SQL statement. ResultSet Ex1rs = Ex1Stmt.executeQuery( "SELECT StudentNumber, FirstName, LastName FROM Students"); //Process each row until there are no more rows. // Displays the results on the console. System.out.println("Student Number First Name Last Name"); while (Ex1rs.next()) { // Get the column values into Java variables StNo = Ex1rs.getString(1); StFName = Ex1rs.getString(2); StLName = Ex1rs.getString(3); System.out.println(StNo,StFName,StLName); } }
The program illustrates the basic steps that are needed to access a table and lists some of the fields in the records.
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch15.htm (16 of 31) [8/14/02 10:59:27 PM]
Updating a Database with JDBC In Listing 15.2, you update the FirstName field in the Students Table by knowing the student's StudentNumber. As in the last example, the code follows the description of the step.
Listing 15.2 Updating the FirstName Field //Declare a method and some variables and parameters. public void UpdateStudentName(String StFName, String StLName, String StNo) throws SQLException { int RetValue; // Initialize and load the JDBC-ODBC driver. Class.forName ("jdbc.odbc.JdbcOdbcDriver"); // Make the connection object. Connection Ex1Con = DriverManager.getConnection( "jdbc:odbc:StudentDB;uid="admin";pw="sa"); // Create a simple Statement object. Statement Ex1Stmt = Ex1Con.createStatement(); //Make a SQL string, pass it to the DBMS, and execute the SQL statement String SQLBuffer = "UPDATE Students SET FirstName = "+ StFName+", LastName = "+StLName+ " WHERE StudentNumber = "+StNo RetValue = Ex1Stmt.executeUpdate( SQLBuffer); System.out.println("Updated " + RetValue + " rows in the Database."); }
In this example, you execute the SQL statement and get the number of rows affected by the SQL statement back from the DBMS. The previous two examples show how you can do simple yet powerful SQL manipulation of the underlying data using the JDBC API in a Java program. In the following sections, you examine each JDBC class in detail.
The data source identifier could be a port in the Internet database server that is identified by the //<server name>:port/... URL, just a data source name used by the ODBC driver, or a full path name to a database file in the local computer. For all you know, it could be a pointer to a data feed of the stock market prices from Wall Street!
Another important function performed by the Connection object is transaction management. The handling of transactions depends on the state of an internal autocommit flag that is set using the setAutoCommit() method, and the state of this flag can be read using the getAutoCommit() method. When the flag is true, the transactions are automatically committed as soon as they are completed. There is no need for any intervention or commands from the Java application program. When the flag is false, the system is in the manual mode. The Java program has the option to commit the set of transactions that happened after the last commit or to rollback the transactions using the commit() and rollback() methods. Note JDBC also provides methods for setting the transaction isolation modularity. When you are developing multi-tiered applications, there will be multiple users performing concurrently interleaved transactions that are on the same database tables. A database driver has to employ sophisticated locking and data-buffering algorithms and mechanisms to implement the transaction isolation required for a large-scale JDBC application. This is more complex when there are multiple Java objects working on many databases that could be scattered across the globe! Only time will tell what special needs for transaction isolation there will be in the new Internet/intranet paradigm.
Once you have a successful Connection object to a data source, you can interact with the data source in many ways. The most common approach from an application developer standpoint is the objects that handle the SQL statements.
The Connection object has the createStatement(), prepareStatement(), and prepareCall() methods to create these Statement objects. Before you explore these different statements, see the steps that a SQL statement goes through. A Java application program first builds the SQL statement in a string buffer and passes this buffer to the underlying DBMS through some API call. A SQL statement needs to be verified syntactically, optimized, and converted to an executable form before execution. In the Call Level Interface (CLI) Application Program Interface (API) model, the application program passes the SQL statement to the driver which, in turn, passes it to the underlying DBMS. The DBMS prepares and executes the SQL statement. After the DBMS receives the SQL string buffer, it parses the statement and does a syntax check run. If the statement is not syntactically correct, the system returns an error condition to the driver, which generates a SQLException. If the statement is syntactically correct, depending on the DBMS, then many query plans usually are generated that are run through an optimizer (often a cost-based optimizer). Then, the optimum plan is translated into a binary execution plan. After the execution plan is prepared, the DBMS usually returns a handle or identifier to this optimized binary version of the SQL statement back to the application program. The three JDBC statement (viz., Statement, PreparedStatement, and CallableStatement) types differ in the timing of the SQL statement preparation and the statement execution. In the case of the simple Statement object, the SQL is prepared and executed in one step (at least from the application program point of view. Internally, the driver might get the identifier, command the DBMS to execute the query, and then discard the handle). In the case of a PreparedStatement object, the driver stores the execution plan handle for later use. In the case of the CallableStatement object, the SQL statement is actually making a call to a stored procedure that is usually already optimized. Note As you know, stored procedures are encapsulated business rules or procedures that reside in the database server. They also enforce uniformity across applications, as well as provide security to the database access. Stored procedures last beyond the execution of the program. So the application program does not spend any time waiting for the DBMS to create the execution plan.
Now, look at each type of statement more closely and see what each has to offer a Java program.
int Boolean Boolean void int void int void void int void void java.sql.SQLWarning void void ResultSet int
executeUpdate execute getMoreResults close getMaxFieldSize setMaxFieldSize getMaxRows setMaxRows setEscapeProcessing getQueryTimeout setQueryTimeout cancel getWarnings clearWarnings setCursorName getResultSet getUpdateCount
(String sql) (String sql) ( ) ( ) ( ) (int max) ( ) (int max) (boolean enable) ( ) (int seconds) ( ) ( ) ( ) (String name) ( ) ( )
The most important methods are executeQuery(), executeUpdate(), and execute(). As you create a Statement object with a SQL statement, the executeQuery() method takes a SQL string. It passes the SQL string to the underlying data source through the driver manager and gets the ResultSet back to the application program. The executeQuery() method returns only one ResultSet. For those cases that return more than one ResultSet, theexecute() method should be used. Caution Only one ResultSet can be opened per Statement object at one time.
For SQL statements that do not return a ResultSet such as the UPDATE, DELETE, and DDL statements, the Statement object has the executeUpdate() method that takes a SQL string and returns an integer. This integer indicates the number of rows that are affected by the SQL statement. Note The JDBC processing is synchronous; that is, the application program must wait for the SQL statements to complete. But because Java is a multithreaded platform, the JDBC designers suggest using threads to simulate asynchronous processing.
The Statement object is best suited for ad hoc SQL statements or SQL statements that are executed once. The DBMS goes through the syntax run, query plan optimization, and the execution plan generation stages as soon as this SQL statement is received. The DBMS executes the query and then discards the optimized execution plan. So, if the executeQuery() method is called again, the DBMS goes through all of the steps again.
The following example program shows how to use the Statement class to access a database (The database schema is shown in Figure 15.4 earlier in this chapter). In this example, you will list all of the subjects (classes) available in our enrollment database and their location and day and times. The SQL statement for this is "SELECT ClassName, Location, DaysAndTimes FROM Classes". You will create a Statement object and pass the SQL string during the executeQuery() method call to get this data. //Declare a method and some variables. public void ListClasses() throws SQLException { int i, NoOfColumns; String ClassName,ClassLocation, ClassSchedule; //Initialize and load the JDBC-ODBC driver. Class.forName ("jdbc.odbc.JdbcOdbcDriver"); //Make the connection object. Connection Ex1Con = DriverManager.getConnection( "jdbc:odbc:StudentDB;uid="admin";pw="sa"); //Create a simple Statement object. Statement Ex1Stmt = Ex1Con.createStatement(); //Make a SQL string, pass it to the DBMS, and execute the SQL statement. ResultSet Ex1rs = Ex1Stmt.executeQuery( "SELECT ClassName, Location, DaysAndTimes FROM Classes"); //Process each row until there are no more rows. // And display the results on the console. System.out.println("Class Location Schedule"); while (Ex1rs.next()) { // Get the column values into Java variables ClassName = Ex1rs.getString(1); ClassLocation = Ex1rs.getString(2); ClassSchedule = Ex1rs.getString(3); System.out.println(ClassName,ClassLocation,ClassSchedule); } } As you can see, the program is very straightforward. You do the initial connection and so on, and create a Statement object. Pass the SQL along with the method executeQuery() call. The driver will pass the SQL string to the DBMS, which will perform the query and return the results. After the statement is done, the optimized execution plan is lost.
Parameter ( ) ( ) ( )
One of the major features of a PreparedStatement is that it can handle IN types of parameters. The parameters are indicated in a SQL statement by placing the ? as the parameter marker instead of the actual values. In the Java program, the association is made to the parameters with the setXXXX() methods, as shown in Table 15.9. All of the setXXXX() methods take the parameter index, which is 1 for the first "?," 2 for the second "?,"and so on. Table 15.9 java.sql.PreparedStatement-Parameter-Related Methods Return Type void void void void void void void void void void void void void void void void void void Method Name clearParameters setAsciiStream setBinaryStream setBoolean setByte setBytes setDate setDouble setFloat setInt setLong setNull setNumeric setShort setString setTime setTimestamp setUnicodeStream Parameter ( ) (int parameterIndex, java.io. InputStream x, int length) (int parameterIndex, java.io. InputStream x, int length) (int parameterIndex, boolean x) (int parameterIndex, byte x) (int parameterIndex, byte x[ ]) (int parameterIndex, java.sql.Date x) (int parameterIndex, double x) (int parameterIndex, float x) (int parameterIndex, int x) (int parameterIndex, long x) (int parameterIndex, int sqlType) (int parameterIndex, Numeric x) (int parameterIndex, short x) (int parameterIndex, String x) (int parameterIndex, java.sql.Time x) (int parameterIndex, java.sql.Timestamp x) (int parameterIndex, java.io.InputStream x, int length)
Advanced Features-Object Manipulation void setObject (int parameterIndex, Object x, int targetSqlType, int scale)
void void
setObject setObject
In the case of the PreparedStatement, the driver actually sends only the execution plan ID and the parameters to the DBMS. This results in less network traffic and is well-suited for Java applications on the Internet. The PreparedStatement should be used when you need to execute the SQL statement many times in a Java application. But remember, even though the optimized execution plan is available during the execution of a Java program, the DBMS discards the execution plan at the end of the program. So, the DBMS must go through all of the steps of creating an execution plan every time the program runs. The PreparedStatement object achieves faster SQL execution performance than the simple Statement object, as the DBMS does not have to run through the steps of creating the execution plan. The following example program shows how to use the PreparedStatement class to access a database. (The database schema is shown in Figure 15.4 earlier in this chapter.) In this example, you will be a little more aggressive and optimize the example you developed in the Statement example. The simple Statement example can be improved in a couple of major ways. First, the DBMS will go through building the execution plan every time. So you will make it a PreparedStatement. Secondly, the query will list all courses which could scroll away. You will improve this situation by building a parameterized query as follows: //Declare class variables Connection Con; PreparedStatement PrepStmt; boolean Initialized = false; private void InitConnection() throws SQLException { //Initialize and load the JDBC-ODBC driver. Class.forName ("jdbc.odbc.JdbcOdbcDriver"); //Make the connection object. Con = DriverManager.getConnection( "jdbc:odbc:StudentDB;uid="admin";pw="sa"); //Create a prepared Statement object. PrepStmt = Ex1Con.prepareStatement( "SELECT ClassName, Location, DaysAndTimes FROM Classes WHERE ClassName = ?"); Initialized = True; } public void ListOneClass(String ListClassName) throws SQLException { int i, NoOfColumns; String ClassName,ClassLocation, ClassSchedule; if (! Initialized) { InitConnection(); } // Set the SQL parameter to the one passed into this method PrepStmt.setString(1,ListClassName); ResultSet Ex1rs = PrepStmt.executeQuery() //Process each row until there are no more rows and // display the results on the console. System.out.println("Class Location Schedule"); while (Ex1rs.next()) { // Get the column values into Java variables ClassName = Ex1rs.getString(1); ClassLocation = Ex1rs.getString(2); ClassSchedule = Ex1rs.getString(3); System.out.println(ClassName,ClassLocation,ClassSchedule);
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/ch15.htm (23 of 31) [8/14/02 10:59:27 PM]
} } Now, if a student wants to check the details of one subject interactively, the above example program can be used. You will save execution time and network traffic from the second invocation onwards because you are using the PreparedStatement object.
After the stored procedure is executed, the DBMS returns the result value to the JDBC driver. This return value is accessed by the Java program using the methods in Table 15.11. Table 15.11 CallableStatement Parameter Access Methods Return Type Boolean byte byte[] Method Name getBoolean getByte getBytes Parameter (int parameterIndex) (int parameterIndex) (int parameterIndex)
java.sql.Date double float int long Numeric Object short String java.sql.Time
getDate getDouble getFloat getInt getLong getNumeric getObject getShort getString getTime
(int parameterIndex) (int parameterIndex) (int parameterIndex) (int parameterIndex) (int parameterIndex) (int parameterIndex, int scale) (int parameterIndex) (int parameterIndex) (int parameterIndex) (int parameterIndex) (int parameterIndex)
()
If a student wants to find out the grades for a subject in the database schema shown in Figure 15.4, you need to do many operations on various tables such as find all assignments for the student, match them with class name, calculate grade points, and so on. This is a business logic (academics is also a business and the concepts apply here, too !) well-suited for a stored procedure. In this example, we give the stored procedure a student ID, class ID, and it will return the grade! Your client program becomes simple, and all the processing is done at the server. This is where you will use a CallableStatement. The stored procedure call is of the following form: studentGrade = getStudentGrade(StudentID,ClassID). In the JDBC call, you will create a CallableStatement object with the ? symbol as placeholders for parameters and then connect Java variables to the parameters as shown in the following example: public void DisplayGrade(String StudentID, String ClassID) throws SQLException { int Grade; //Initialize and load the JDBC-ODBC driver. Class.forName ("jdbc.odbc.JdbcOdbcDriver"); //Make the connection object. Connection Con = DriverManager.getConnection( "jdbc:odbc:StudentDB;uid="admin";pw="sa"); //Create a Callable Statement object. CallableStatement CStmt = Con.prepareCall({?=call getStudentGrade[?,?]}); // Now tie the placeholders with actual parameters. // Register the return value from the stored procedure // as an integer type so that the driver knows how to handle it. // Note the type is defined in the java.sql.Types. CStmt.registerOutParameter(1,java.sql.Types.INTEGER); // Set the In parameters (which are inherited from the PreparedStatement class) CStmt.setString(1,StudentID);
CStmt.setString(2,ClassID); // Now we are ready to call the stored procedure int RetVal = CStmt.executeUpdate(); // Get the OUT parameter from the registered parameter // Note that we get the result from the CallableStatement object Grade = CStmt.getInt(1); // And display the results on the console. System.out.println(" The Grade is: "); System.out.println(Grade); } As you can see, JDBC has minimized the complexities of getting results from a stored procedure. It still is a little involved, but is simpler. Maybe in the future, these steps will become simpler. Now that you have seen how to communicate with the underlying DBMS with SQL, see what you need to do to process the results sent back from the database as a result of the SQL statements.
java.sql.Numeric Object short String java.sql.Time java.sql.Timestamp java.io.InputStream Get Data by Column Name java.io.InputStream java.io.InputStream boolean byte byte[] java.sql.Date double float int long java.sql.Numeric Object short String java.sql.Time java.sql.Timestamp java.io.InputStream int SQLWarning void String ResultSetMetaData
getNumeric getObject getShort getString getTime getTimestamp getUnicodeStream getAsciiStream getBinaryStream getBoolean getByte getBytes getDate getDouble getFloat getInt getLong getNumeric getObject getShort getString getTime getTimestamp getUnicodeStream findColumn getWarnings clearWarnings getCursorName getMetaData
(int columnIndex, int scale) (int columnIndex) (int columnIndex) (int columnIndex) (int columnIndex) (int columnIndex) (int columnIndex) (String columnName) (String columnName) (String columnName) (String columnName) (String columnName) (String columnName) (String columnName) (String columnName) (String columnName) (String columnName) (String columnName, int scale) (String columnName) (String columnName) (String columnName) (String columnName) (String columnName) (String columnName) (String columnName) ( ) ( ) ( ) ( )
As you can see, the ResultSet methods-even though there are many-are very simple. The major ones are the getXXX() methods. The getMetaData() method returns the meta data information about a ResultSet. The DatabaseMetaData also returns the results in the ResultSet form. The ResultSet also has methods for the silent SQLWarnings. It is a good practice to check any warnings using the getWarning() method that returns a null if there are no warnings.
The SQLException class in JDBC provides a variety of information regarding errors that occurred during a database access. The SQLException objects are chained so that a program can read them in order. This is a good mechanism, as an error condition can generate multiple errors and the final error might not have anything to do with the actual error condition. By chaining the errors, you can actually pinpoint the first error. Each SQLException has an error message and vendor-specific error code. Also associated with a SQLException is a SQLState string that follows the XOPEN SQLstate values defined in the SQL specification. Table 15.13 lists the methods for the SQLException class. Table 15.13 SQLException Methods Return Type SQLException SQLException SQLException SQLException String int SQLException void Method Name SQLException SQLException SQLException SQLException getSQLState getErrorCode getNextException setNextException Parameter (String reason, String SQLState, int vendorCode) (String reason, String SQLState) (String reason) ( ) ( ) ( ) ( ) (SQLException ex)
JDBC. These classes include Date, Time, TimeStamp, Numeric, and so on. Most of these classes extend the basic Java classes to add the capability to handle and translate data types that are specific to SQL.
java.sql.Date
This package gives a Java program the capability to handle SQL DATE information with only year, month, and day values. This package contrasts with the java.util.Date, where the time in hours, minutes, and seconds is also kept (see Table 15.15). Table 15.15 java.sql.Date Methods Return Type Date Date String Method Name Date valueOf toString Parameter (int year, int month, int day) (String s) ( )
java.sql.Time
As seen in Table 15.16, the java.sql.Time adds the Time object to the java.util.Date package to handle only hours, minutes, and seconds. java.sql.Time is also used to represent SQL TIME information. Table 15.16 java.sql.Time Methods Return Type Time Time String Method Name Time Time toString Parameter (int hour, int minute, int second) valueOf(String s) ( )
java.sql.Timestamp
The java.sql.Timestamp package adds the Timestamp class to the java.util.Date package. It adds the capability of handling nanoseconds. But the granularity of the subsecond timestamp depends on the database field as well as the operating system (see Table 15.17). Table 15.17 java.sql.Timestamp Methods Return Type Timestamp Timestamp String int void Method Name Timestamp valueOf toString getNanos setNanos (int n) Parameter (int year, int month, int date, inthour, int minute, int second, int nano); (String s) ( ) ( )
boolean
equals
(Timestamp ts)
java.sql.Types
This class defines a set of XOPEN equivalent integer constants that identify SQL types. The constants are final types. Therefore, they cannot be redefined in applications or applets. Table 15.18 lists the constant names and their values. Table 15.18 java.sql.Types Constants Constant Name BIGINT BINARY BIT CHAR DATE DECIMAL DOUBLE FLOAT INTEGER LONGVARBINARY LONGVARCHAR NULL NUMERIC OTHER REAL SMALLINT TIME TIMESTAMP TINYINT VARBINARY VARCHAR Value -5 -2 -7 1 91 3 8 6 4 -4 -1 0 2 1111 7 5 92 93 -6 -3 12
JDBC in Perspective
In this chapter, you saw how JDBC has ushered in an era of simple yet powerful database access for Java programs. JDBC is
an important step in the right direction to elevate the Java language to the Java platform. The Java APIs-including the Enterprise APIs (JDBC, RMI, Serialization, and IDL), Security APIs, and the Server APIs-are the essential ingredients for developing enterprise-level, distributed, multi-tier client/server applications. The JDBC specification life cycle happened in the speed of the Net-one Net year is widely clocked as equaling seven normal years. The version 1.01 JDBC specification is fixed, so the developers and driver vendors are not chasing a moving target. Another factor in favor of JDBC is its similarity to ODBC. JavaSoft made the right decision to follow ODBC philosophy and abstractions, thus making it easy for ISVs and users to leverage their ODBC experience and existing ODBC drivers. In the JDBC specification, this goal is described as "JDBC must be implementable on top of common database interfaces." By making JDBC a part of the Java language, you received all of the advantages of the Java language concepts for database access. Also, as all implementers have to support the Java APIs, JDBC has become a universal standard. This philosophy, stated in the JDBC specification as "provide a Java interface that is consistent with the rest of the Java system," makes JDBC an ideal candidate for use in Java-based database development. Another good design philosophy is the driver independence of the JDBC. The underlying database drivers can either be native libraries-such as a dynamic link lbrary (.dll) for the Windows system or Java routines connecting to listeners. The full Java implementation of JDBC is suitable for a variety of Network and other Java OS computers, thus making JDBC a versatile set of APIs. Note In my humble opinion, the most important advantage of JDBC is its simplicit and versatility. The goal of the designers was to keep the API and common cases simple and "support the weird stuff in separate interfaces." Also, they wanted to use multiple methods for multiple functionality. They have achieved their goals even in this first version. For example, the statement object has the executeQuery() method for SQL statements returning rows of data, and it has the executeUpdate() method for statements without data to return. Also, uncommon cases, such as statements returning multiple ResultSets, have a separate method-execute().
As more applications are developed with JDBC and as the Java platform matures, more and more features will be added to JDBC. One of the required features, especially for client/server processing, is a more versatile cursor. The current design leaves the cursor management details to the driver. I would prefer more application-level control for scrollable cursors, positioned update/delete capability, and so on. Another related feature is the bookmark feature, which is useful especially in a distributed processing environment such as the Internet.
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f15-1.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f15-2.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f15-3.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f15-3.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f15-4.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f15-5.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f20-1.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f16-1.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f16-2.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f16-3.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f16-4.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f16-5.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f10-1.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f10-2.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f10-3.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f6-1.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f6-2.gif
file:///E|/Java%20Professor/Hacking%20Java%20Professional%20Resource%20Kit/f6-3.gif