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 : [email protected]
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: [email protected], [email protected], or [email protected]. 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 [email protected]. 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 c