Survey
* Your assessment is very important for improving the workof artificial intelligence, which forms the content of this project
* Your assessment is very important for improving the workof artificial intelligence, which forms the content of this project
Brought to you by ownSky! Jini™ and JavaSpaces™ Application Development By Robert Flenner Publisher Pub Date ISBN Pages : Sams Publishing : December 05, 2001 : 0-672-32258-7 : 480 Interest in Jini and JavaSpaces development has greatly increased as developers discover its power for building network services in a Web services environment. Jini and JavaSpaces Application Development provides examples of Jini-based network services, intelligent agents, and mobile computing solutions. Jini and JavaSpaces Application Development incorporates recent innovations in Java and Jini technology, including Web services, XML integration, and open-source Java technologies. Author Robert Flenner has built, and presents in the book, an innovative Jini-based application including groupware functionality, workflow processing, autonomous agents, and B2B integration. Jini and JavaSpaces Application Development demonstrates real-world usage and application of the Jini technology. Specifically the book presents and builds on the concept of a "virtual development center" It utilizes the concepts of distributed development, open-source, virtual corporations, and the borderless world as a backdrop to developing products and services using Jini and JavaSpaces technologies. Table of Content Table of Content ........................................................................................................ii About the Author .......................................................................................................v Acknowledgments......................................................................................................v Tell Us What You Think! ..........................................................................................v Introduction...............................................................................................................vi What's Ahead?....................................................................................................vi Part I: The Jini Architecture.......................................................................................1 Chapter 1. Introduction to Jini ...................................................................................2 The Jini Evolution ................................................................................................2 Goals of the Technology ....................................................................................2 What Are Jini Services? .....................................................................................3 How Does Jini Solve Real-World Problems? .................................................5 The Evolving View of Services..........................................................................8 The Tipping Point ..............................................................................................10 Chapter 2. Jini in the Age of Services......................................................................12 The Emerging Networked Application ...........................................................12 Understanding the Layers of Communication ..............................................13 The New Protocols............................................................................................17 Existing Protocols in the Middleware Maze ..................................................26 Summary ............................................................................................................33 Chapter 3. Supporting Technologies........................................................................34 The HTTP Server ..............................................................................................34 Remote Method Invocation..............................................................................37 Summary ............................................................................................................58 Chapter 4. Building on Jini Foundation Concepts ...................................................60 Foundational Services ......................................................................................60 Summary ............................................................................................................90 Part II: Designing Applications Using the Jini Framework.....................................91 Chapter 5. JavaSpaces Service.................................................................................92 The JavaSpaces Service .................................................................................92 The Architecture of JavaSpaces.....................................................................92 Entries Revisited................................................................................................95 The Simple API..................................................................................................97 Distributed Data Structures ...........................................................................100 Application Basics ...........................................................................................103 Deploying JavaSpaces...................................................................................104 A Sample Application—Building the MessagePad ...................................107 Summary ..........................................................................................................123 Chapter 6. Transaction Service ..............................................................................124 Distributed Transactions ................................................................................124 The Jini Transaction Framework ..................................................................127 The Mahalo Implementation..........................................................................131 JavaSpaces Transaction Example ...............................................................132 Transactional Shell Example.........................................................................136 Summary ..........................................................................................................146 Chapter 7. The Helper Services .............................................................................147 ii Overview of Helper Services .........................................................................147 The Disconnected Client or Service Entity..................................................150 The LDS Interface Explained ........................................................................150 The LRS Interface Explained ........................................................................155 The EMS Interface Explained .......................................................................159 Summary ..........................................................................................................172 Chapter 8. Service Administration.........................................................................173 Service Administration Overview..................................................................173 Client Administration.......................................................................................173 The Administrative Interfaces........................................................................176 Building on Your Service Framework...........................................................180 Client-Side Implementation ...........................................................................192 Summary ..........................................................................................................195 Chapter 9. Security in Jini......................................................................................196 Defining Security Requirements ...................................................................196 Jini-Specific Requirements ............................................................................197 Java Security....................................................................................................197 The Java Virtual Machine ..............................................................................198 Configuring Basic Security with Policy Files ...............................................201 Additional Security Considerations and Alternative Techniques .............205 In Need of Evolution .......................................................................................220 Summary ..........................................................................................................221 Part III: Developing Applications..........................................................................223 Chapter 10. GroupWare .........................................................................................224 Defining the JWorkPlace................................................................................224 The Role of XML in Workplace Definition ...................................................225 The Supporting Services: reggie, outrigger, and mahalo .........................226 Client-Side Service Discovery.......................................................................234 Introducing the ServiceUI Project .................................................................240 Summary ..........................................................................................................249 Chapter 11. Workflow ...........................................................................................250 What Is Workflow? ..........................................................................................250 A Collaborative Workflow Framework Example .........................................253 The Supporting Services................................................................................255 Change in State as a Flow-of-Objects .........................................................255 The Event Mailbox Service as a Remote Event Notification Platform....260 Integrating Workers with JavaSpaces and the Event Mailbox Service ..266 Content Management Workflow ...................................................................282 Summary ..........................................................................................................286 Chapter 12. Agents.................................................................................................287 Agents of Collaboration ..................................................................................287 Simple Agent Frameworks.............................................................................288 An Example Process Control Agent.............................................................290 Future Jini Agents ...........................................................................................299 Summary ..........................................................................................................300 Chapter 13. Jini as a Web Service .........................................................................302 Business-to-Business E-commerce .............................................................302 What Are Web Services?...............................................................................302 Simple Object Access Protocol (SOAP) as a Transport Technology .....303 Jini in the B2B Context ...................................................................................312 iii SOAP-to-Jini Integration ................................................................................313 Summary—The Road to (Dis)-Integration...................................................318 Chapter 14. Pervasive Computing and Mobile Devices ........................................320 Jini Environment and Assumptions ..............................................................321 Can Jini Scale Down? ....................................................................................322 Will Devices Scale Up? ..................................................................................331 The Network Reality—Pervasive Computing .............................................332 Summary ..........................................................................................................332 Part IV: Appendixes...............................................................................................334 Appendix A. Setting Up Your Environment..........................................................335 What You Will Need........................................................................................335 Gotchas and Common Pitfalls.......................................................................336 Appendix B. Tools .................................................................................................339 Start-up Scripts................................................................................................339 Building Jini ......................................................................................................340 Service Browsing and Administration ..........................................................341 Summary ..........................................................................................................343 Appendix C. Links, Community, Resources .........................................................344 Links ..................................................................................................................344 Specifications...................................................................................................344 Community .......................................................................................................344 Resources ........................................................................................................345 iv About the Author Robert Flenner is an active author, software developer, and independent contractor. His articles on e-commerce, Java development, and Jini have been featured on many popular Web sites. He is actively involved in business-to-business technologies, peer-to-peer technologies, and GroupWare applications. Robert has extensive experience consulting with global companies on both strategic and tactical system development and deployment. His technical consulting experience includes Web, CORBA, distributed object modeling, workflow, and high-availability transaction processing. He is currently promoting and developing a framework for distributed development and collaboration using Jini and JavaSpaces technologies at www.jworkplace.org. Robert can be reached [email protected]. Acknowledgments I am very grateful to the many people who helped with this book and provided valuable support. I would like to thank Michael Stephens at Sams Publishing for giving me the opportunity to write this book. All the folks at Sams, especially Christy Franklin, Seth Kerney, Christina Smith, and technical reviewer Piroz Mosheni, that assisted with the production, design, and integrity of the book. Without their effort, this book would not be possible. Additionally my thanks to the Jini community, who daily provide insight and direction, and are bound together by this technology. And of course my family and especially my wife Jane for her encouragement and never ending support. Tell Us What You Think! As the reader of this book, you are our most important critic and commentator. We value your opinion and want to know what we're doing right, what we could do better, what areas you'd like to see us publish in, and any other words of wisdom you're willing to pass our way. As an Executive Editor for Sams Publishing, I welcome your comments. You can fax, e-mail, or write me directly to let me know what you did or didn't like about this book—as well as what we can do to make our books stronger. Please note that I cannot help you with technical problems related to the topic of this book, and that due to the high volume of mail I receive, I might not be able to reply to every message. v When you write, please be sure to include this book's title and author's name as well as your name and phone or fax number. I will carefully review your comments and share them with the author and editors who worked on the book. Fax: E-mail: Mail: 317-581-4770 [email protected] Michael Stephens Executive Editor Sams Publishing 201 West 103rd Street Indianapolis, IN 46290 USA Introduction This book is about developing Jini and JavaSpaces applications in the midst of a technology revolution. It is about emerging technologies that are changing the way we build and think about systems. The intent is to provide a practical approach to using and developing Jini services by working through examples. Although you will cover the framework and the concepts underlying Jini, the primary goal is to provide practical guidance for developing applications and services in a Jini network. In addition, a wide range of application areas to discover and extend Jini capabilities are covered. This book is for Java developers, architects, and managers of distributed applications who require practical knowledge of Jini services, and the role those services play in building network-centric applications. This book also highlights three major trends shaping the new networked applications—Web services, peer-to-peer (P2P) systems, and the Semantic Web. Where and how does Jini fit into these efforts? This book will attempt to answer these questions. It will also demonstrate how Jini is well positioned for the technology changes unfolding on the Internet and the changes driving the business-to-business and the emerging peer-to-peer markets. What's Ahead? Part I: The Jini Architecture Chapter 1—Introduction to Jini The notion of a service is fundamental to Jini. This chapter provides an overview of Jini services. It also compares and contrasts Jini to competing technologies and trends in distributed application development. Chapter 2—Jini in the Age of Services What differentiates a Jini network from other networks? This chapter defines a Jini network. The objective is to clarify many misconceptions about the proper positioning of Jini in the Open Systems Interconnection (OSI) model. vi Chapter 3—Supporting Technologies This chapter presents an overview of remote method invocation (RMI). It builds a sample nonactivatable and activatable service to prepare for the introduction of Jini concepts. The intent is to provide you with enough background to get started in setting up the Jini environment and an understanding as to how the supporting services fit. Chapter 4—Building on Jini Foundation Concepts This chapter is critical to the overall comprehension of the Jini framework. It will provide you with sufficient detail to grasp key Jini concepts. Multicast and unicast discovery is discussed in detail, as well as the role of the lookup service. You will evolve the RMI service built in Chapter 3 to your first Jini service. Part II: Designing Applications Using the Jini Framework Chapter 5—JavaSpaces Service This chapter provides an overview of the JavaSpaces service. It extends the notion of entries defining service attributes to entries defining models of communication and control in JavaSpaces, such as messaging systems and workflow control systems. Chapter 6—Transaction Service This chapter explains distributed transactions and how they are implemented in the Jini model. This chapter provides an overview of the Jini Transaction service. Chapter 7—The Helper Services This chapter provides an overview of the Jini 1.1 helper services and how they can be used to complement core and user-defined services. This includes the lookup discovery service, the lease renewal service, and the event mailbox service. Chapter 8—Service Administration How are Jini services administered? How can you reduce the human requirements on service administration? This chapter answers these questions and introduces the service administration interfaces. It explains the management of groups, locators, and entries in service administration. In addition, you will define a Jini service to provide administrative functionality. Chapter 9—Security in Jini How is security implemented in Jini networks? This chapter explains the current state of security in Jini technology. It highlights the dependencies on the Java 2 platform, RMI, and the Java language. In addition, common security mechanisms and standards are evaluated for distributed Jini services. Part III: Developing Applications Chapter 10—GroupWare This chapter provides practical applications and examples of the Jini and JavaSpaces concepts introduced in the prior two parts. You will develop a sample GroupWare application that includes peer-to-peer content sharing, visual UI interface techniques, and online chat service. This example vii is based on JWorkPlace, the author's original creation. Visit www.jworkplace.org for more information. Chapter 11—Workflow This chapter demonstrates the power of Jini and JavaSpaces in controlling distributed workflow processes. It presents the author's concept called STAR. This acronym breaks down as (S)pace, (T)ime, (A)ctivity, and (R)esource entries as they define the core model in distributed workflow processing. An example workflow application is presented that uses the Jini Event Mailbox service and JavaSpaces. Chapter 12—Agents This chapter provides an overview of software agents and the mapping of agent requirements to the Jini services. Demonstration of simple agents of collaboration in a development scenario is explained. Exercises in Chapters 10 and 11 are built upon to automate communication between the development community. The example uses the Jini Lease Renewal Service for managing resources and providing time event notification. Chapter 13—Jini as a Web Service This chapter presents Jini in the context of the current trend towards XML-defined Web services. It provides an overview of the Web Service Definition Language (WSDL), Universal Description and Discovery Interface (UDDI), and Simple Object Access Protocol (SOAP). The intent is to demonstrate how Jini can play a part in service discovery and B2B marketplace evolution. Chapter 14—Pervasive Computing and Mobile Devices The final chapter presents Jini in the emerging mobile device space. It includes an overview of the Java 2 Micro Edition (J2ME) platform, which includes Java support for mobile devices. The chapter also includes an overview of the Surrogate architecture, which has been developed to support resource constrained devices in a Jini network. Can Jini scale down? Will devices scale up? This chapter asks and answers these questions. Appendix A—Setting Up Your Environment Appendix A provides the information necessary to get your Jini environment running. It includes links to important Jini distribution information, system requirements, and a troubleshooting section of "Gotchas and Common Failures." Appendix B—Tools Appendix B provides an overview of important Jini tools for new developers. It includes service startup scripts, building the Jini distribution using the Build Tool, and the Lookup Browser for service administration. Appendix C—Links, Community, Resources Appendix C provides links to valuable Jini resources available on the Web. viii Part I: The Jini Architecture IN THIS PART 1 Introduction to Jini 2 Jini in the Age of Services 3 Supporting Technologies 4 Building on Jini Foundation Concepts 1 Chapter 1. Introduction to Jini IN THIS CHAPTER • • • • • • The Jini Evolution Goals of the Technology What Are Jini Services? How Does Jini Solve Real-World Problems? The Evolving View of Services The Tipping Point The Jini Evolution There are two important trends in the Jini evolution: The focus on evolving solutions to promote a more effective global network using the Internet, and the common concepts and terminology that are emerging from those solutions. It is my belief—and one shared by many others—that networks and networked services are being driven toward the solutions supported by Jini. Jini is one of the current catalysts for change in the era of the networked application. Jini provides a solution for defining, deploying, and administering services on a network. The following definition comes from the official Jini architecture specification, located on the Sun Web site (http://www.sun.com/jini/specs): A Jini system is a distributed system based on the idea of federating groups of users and the resources required by those users. The focus of the system is to make the network a more dynamic entity that better reflects the dynamic nature of the workgroup by enabling the ability to add and delete services flexibly. Jini network technology provides a network-centric view of services and service delivery. Services have a broad definition; from applications to mobile appliances and anything in between that is on the network. Jini is based on the Java platform, and has both the advantages and disadvantages inherent in that platform. Jini has the built-in capability of understanding Java types. Services and interfaces are defined using Java types. Types are richer than XML in being able to define semantics between machines to exchange information. As Jini is based on Java, which is an object-oriented language, it supports polymorphism. Polymorphism permits us to support objects of different types in consistent ways; it helps us deal with technological evolution. Of course, Java is more difficult to learn than XML, and Java has the disadvantage of not being the smallest or fastest engine on the block. In addition, the current Jini release relies heavily on the RMI (Remote Method Invocation) protocol, although this is not a requirement. Goals of the Technology The overall goal of Jini is to "…turn the network into a flexible, easily administered tool on which resources can be found by human and computational clients." Jini attempts to minimize the amount of administration required to operate and manage a network of services. It is recognized 2 that the proliferation of computers and devices that contain computers is outpacing current administration capabilities. Jim Waldo of Sun Microsystems made the point that computers will soon outnumber humans on the planet. How can we continue to manage our networks and millions of devices connected to the network—cable or wireless—with current practices? Today, system and network administration still requires a significant amount of human intervention and monitoring. Jini services require less configuration and administration because Jini provides a framework for knowledgeable services. Knowledgeable services are aware of the environment in which they inhabit. Knowledgeable services actively participate in coping with changes to that environment. Jini provides a solution to network and system instability. For years I dealt with technologies that promoted location and network transparency. The idea was that client applications should not care whether the process invoked is local or remote. Using these technologies, we tried to mask process remoteness from the client. This would enable the same programming model to function in either scenario, and remote processes could move on the network without requiring changes to client code. However, process or service location is important! Distributed services have the potential to behave differently than local services, and therefore the mask is superficial. Any sophisticated application ultimately has to deal with issues such as latency, and partial and permanent failures in distributed systems and networks. So, while the invocation can be made consistent, the results cannot. Jini was designed to recognize that there is a difference between the remote and local model that cannot be hidden. In fact, the model ensures that services are built to accommodate failures and instability in systems and networks. What Are Jini Services? The notion of a service is fundamental to Jini. The Jini Specification defines a service as "…something that can be used by a person, a program, or another service." Jini services are usually manifested as Java objects that expose an interface that conforms to the Jini specification. The type of service determines the interfaces that make up that service. Jini services advertise their operations by registering an object with a Jini-compliant lookup service. Service registration is at the core of building the Jini network community. The Lookup Service Every Jini community must have at least one registry or lookup service available. The lookup service (LUS) to Jini can be compared to: • • • • The registry in RMI (Remote Method Invocation) The Name Server in DNS (Domain Name Systems) The COS Naming Service in CORBA The UDDI in Web services Each of these systems provides a mechanism for resolving a name to an information processing location. Each system takes a different approach and extends the functionality in different ways. For instance, RMI maps a name (String) to an object. DNS maps hostnames to IP addresses, and UDDI provides a directory to register a Web service. 3 The LUS enables service-using entities to register and find services that meet specific criteria. The LUS provides functions similar to a yellow page service. To Jini, the LUS is just another service on the network, albeit an important one. The LUS is what in effect bootstraps the Jini network. Services register with the LUS, and applications find services of interest using the LUS. After a service is found, an application can invoke the methods that the service exposes through its public interface. The LUS addresses the requirements that broadly refer to the notion of discovery. This notion is refined in this book by examining the degree of dynamic discovery required in an application and the alternative approaches. The LUS is defined by the Sun Lookup Service Specification. Sun Microsystems has developed an implementation of the LUS called reggie. The Lookup Discovery Service The lookup discovery service (LDS) provides support for disconnected clients. It acts as a proxy for the Jini client to discover changes in the services composing the Jini network. For instance, services can become active and join the Jini community at any time. In addition, they can become inactive or unreachable. One approach sometimes seen to solve dynamic network changes is through broadcasting and replication. With the LDS, a proxy can alert services of changes by activating a service based on a type of discovery event. The service registers interest with the LDS for notification. This reduces the resource load on systems, and eliminates the need for clients and services to remain active. The LDS is defined by the Sun Lookup Discovery Service Specification. Sun has developed an implementation of the LDS called fiddler. The Lease Renewal Service One of the concepts introduced in Jini is resource leasing, or time-based resource allocation. Leasing of resources has direct implications on the programming model used and the services that must be implemented to support a Jini network. Leasing implies that nothing is permanent in the Jini system. You must renew leases and renew interest in resources in order for them to remain active and available. Otherwise, they will be purged from the network by their removal from the LUS. Imagine how much friendlier the Web would be if bad or invalid links were just magically purged or disappeared! There is a lease renewal service—defined as a helper service in the Jini framework—that renews interest in resources for you. This is another service that enables disconnected clients and minimizes the amount of work required by developers that use the Jini model. You delegate the responsibility to renew leases to the lease renewal service. So, your service can then disconnect/go idle and not have to worry about losing leases. The lease renewal service (LRS) is defined by the Sun Lease Renewal Service Specification. Sun has developed an implementation of the LRS called norm. The Event Mailbox Service Notification of changes in the state of network services is enabled by remote events. Events have long been understood as a technique for state change notification. The event model provides the capability to decouple event observers from event sources. This promotes consistency in change notification and reusability. The Jini event interface is simple and designed to enable event chains or filters to grow dynamically as events dictate. 4 The Jini framework provides an event mailbox service that provides event "store-and-forward" functionality. This enables a using entity to disconnect from the network and have the service queue events until those events are requested by the using entity. The event mailbox service (EMS) is defined by the Sun Event Mailbox Service Specification. Sun has developed an implementation of the EMS called mercury. The Transaction Service Transactions are at the core of legacy and e-commerce systems. Jini defines a lightweight transaction interface that supports a two-phase commit protocol. The intent of the transaction interface is that services will determine the degree of transactional integrity required, depending on the application context. Unlike typical database transactional systems or transaction monitors, Jini does not try to guess the degree of transactional integrity required. It does, however, provide common transaction semantics that can be assumed by using entities. In other words, a tremendous amount of flexibility is left to the implementation. The transaction service is defined by the Sun Transaction Service Specification. Sun has developed an implementation of the transaction service called mahalo. The Persistence Service JavaSpaces is an example of a Jini service that runs on the Jini network and is closely tied to the Jini architecture. JavaSpaces was heavily influenced by the concept of a tuple space, which was first described in 1982 in a programming language called Linda. The basic idea is that you can have many active programs distributed over physically dispersed machines, unaware of each other's existence, and yet the programs are still able to communicate. They communicate to each other by updating shared memory. Programs read, write, and take entries from this shared workspace. Peer-to-peer systems have already demonstrated the need for a distributed network of loosely coupled collaborating processes. These processes require task management and coordination. JavaSpaces provides a lightweight implementation of persistence that can aid in implementing a collective memory. The JavaSpaces service is defined by the Sun JavaSpaces Service Specification. Sun has developed an implementation of the persistence service called outrigger. Links to all of these service specifications can be found in Appendix C. The Killer Service Of course, the killer Jini service is the one waiting for you to develop! The services just mentioned form the building blocks. This book demonstrates the usage of these foundational services in application-specific scenarios. The intention is to arm you with enough knowledge to start down the path to building the next generation of networked applications. How Does Jini Solve Real-World Problems? We have taken a broad look at the systems and changes occurring in networked-applications. But, how does Jini solve real-world problems? 5 The Benefits of Distribution and Parallelism Jini is based on the premise that services are distributed across a network. The benefits of distribution are well-known. We generally distribute processing when we need to scale our systems to support increased demand for resources. We distribute for geographic reasons, to move resources and processes closer to their access point. We distribute to provide better fault resistance and network resilience. We distribute resources to enable sharing of resources and promote collaboration. Of course, with distribution comes a number of significant challenges. The management of the network is much more difficult. One of the advantages of centralization is central administration and monitoring. Knowing where resources are and how they are behaving is a tremendous advantage. In a distributed environment, failures are not always detected immediately. Worse yet, partial failures allow for results and side effects that networks and applications are not prepared to deal with. Response time and latency issues introduced as a result of remote communication can be unpredictable. The network can have good days and bad days. The bad days can damage the reputation and usability of a product in the eyes of the user community. Application-to-application interaction might become unstable as error paths and timeouts get triggered excessively. Synchronization can often require excessive strain on bandwidth. Any solution that is based on distribution should be able to eliminate or mitigate these issues. Jini has evolved more than most in this area of management. Parallelism is not new to computing. In fact, much of what we do in computing is done in parallel. Multiprocessor machines and operating systems rely on the ability to execute tasks in parallel. Threads of control enable us to partition a process into separate tasks. However, to date, parallelism has not been the norm in application development. While we design applications to be multithreaded, this generally has involved controlling different tasks required of a process, such as reading from a slow device, or waiting for a network response. We have not defined many applications that run the same tasks in parallel, such as searching a large database, or filtering large amounts of information concurrently. Parallelism can provide us with a divide-and-conquer approach to many repetitive tasks. The SETI@Home project demonstrated that personal computers could be harnessed together across the Internet to provide extraordinary computing power. The Search for Extraterrestrial Intelligence project examines radio signals received from outer space in an attempt to detect intelligent life. It takes a vast amount of computing power to analyze the amount of data that is captured and the computations involved. People volunteered for the project by downloading a screensaver from the SETI Web site. The screensaver was capable of requesting work units that were designed to segment the mass amount of radio signals received. In the first week after the project launched, more than 200,000 people downloaded and ran the software. This number grew to more than 2,400,000 by the end of 2000. The processing power available as a result of this network outpaced the ASCI White that was developed by IBM and is the fastest supercomputer built to date. Jini and JavaSpaces are designed to meet this growing trend in divide-and-conquer strategies. Coping with the Inherent Failure of Networks and Hardware Jini was built under the assumption that services are distributed over a network, and that the network is unreliable. Therefore, to mitigate the issue of reliability, Jini introduced the concept of leasing. Leasing ensures that failing services get recognized and removed from the supporting Jini community. For example, to be recognized on the Jini network, you must register with the LUS. To remain active in the LUS, you must renew a lease. This renewal process ensures that garbage 6 does not accumulate in key components, and it does this without requiring human intervention. It's part of the overall framework that makes Jini work. Exploiting Interface-Based Design Object-oriented development has been proclaiming the virtues of designing with interfaces as opposed to concrete implementations for more than a decade. Jini relies heavily on this concept. An interface provides a standard protocol for interacting with objects that implement the interface. Interfaces are key to enabling alternative implementations and technology evolution. They permit the semantics of the interaction to evolve separately from the implementation, which gives providers tremendous flexibility. For instance, provider implementations might optimize different requirements, such as scalability, footprint size, throughput, and so on. Users can select the service that best fits their needs and not change the client interaction. Of course, this is not new to distributed development and object technology in general. The Common Object Request Broker Architecture (CORBA) developed by the Object Management Group (OMG) defines interoperability based on interface definitions. CORBA also provides separation of interface definition from implementation. In CORBA and many other technologies, Interface Definition Language (IDL) is used to provide this property. However, there are differences enabled through RMI, such as dynamic class loading that permits subclasses to be resolved by remote objects even though that subclass is not defined by the original interface. A remote object might not even have the definition of a subclass available, but can retrieve the class definition by requesting and marshalling the object across the network via RMI. This concept is not supported natively in any of the other technologies. Promoting Service Reuse Promoting service reuse will be the key to the success of service-based architectures. In other words, if I have to re-invent these underlying services for each application, then the benefit becomes questionable. Service reuse will not be an easy task to implement. Although it is easy to imagine a network of services, cooperation and collaboration of services internally and externally to an organization can be an elusive goal. To promote reuse, Jini provides: • • • • Object-oriented types and interface sub-typing A standard mechanism for publishing and advertising services Improved quality of service through self-healing networks Reduced time of development by providing core services out of the box The Veracity in the Technology So why hasn't Jini taken off and been adopted as quickly as some had projected in 1999? Ask any two people and you will get two different answers. My opinion can be summarized as follows: After the initial introduction, Jini became labeled as the device technology. It got promoted as the technology that was going to enable devices to flourish on the Internet. Your refrigerator could actually dial-up your refrigerator repairman without you knowing it! Every appliance in your house would begin to appear on your home network. Software agents and devices would be interacting to bring you the cartoon world of George Jetson. Well, it didn't happen in 1999, and it hasn't happened in 2001. My refrigerator has not made a single call, has yours? Unfortunately, the appliances most likely to use this technology initially were cell phones and Palm Pilots, neither of which is known for an abundance of processing capacity. These devices tend to be resource-constrained. Although Jini was designed with a relatively small footprint, it 7 was in excess of the easy reach for most devices at the time. So, rapid adoption by the device market did not occur. In addition, Jini deals with complex networking issues. The learning curve to Jini cannot be underestimated. Many developers new to Jini find it difficult to get started with the technology. Jini relies on technologies like Java and RMI—at least the initial implementation—in addition to network technologies. Jini builds on these technologies, thus making it more difficult for the newcomer. Of course, this is also the strength of the framework, because it does not try to invent something that already exists. The general lack of Jini tools has not helped the learning process. In contrast, the JXTA project has released the JXTA Shell as an application that enables users to interact with the JXTA platform through a command-line interpreter. It is an example application to jump-start the learning and discovery process in JXTA. Jini has not released anything comparable to the JXTA Shell. A service start-up program that was "buggy" was all that was delivered. In contrast to the difficulty in learning Jini is the ease with which XML can be adopted. Couple this with the Web services, and you have yet another impediment to adoption. However, I will contend that XML has tremendous benefits if applied properly—such as document definition and technical configurations—but will be stretched to deal with evolving network applications, including changing behavior and versioning. Successful adoption of a technology requires many things to come together in the right way. These things could include the following: • • • • • The integrity of the product The demand for the functionality The maturity of the market The availability of skilled resources The cost to a corporation and individual Competing Technologies Of course, since the introduction of Jini, many competing technologies have surfaced that are quickly carving out niches in this global network of services. In some respects, getting recognized in the crowd will become more difficult. We will not be able to look at any solution in a vacuum. We have the opportunity to avoid the mistakes of the past—developing narrow solutions—by developing interoperable applications. However, one size will not fit all. This choice will require that we look at more than one solution, and develop and evolve our level of understanding of peer patterns, service patterns, and the semantics of the relationships between them. The Evolving View of Services Almost everyone is talking about services of interacting with services. As mentioned previously, Jini is based on services that are available on a network. The Current "Tug-of-War"—Services Versus Devices Jini blurs the distinction between hardware and software. This might have contributed to the initial "technology for intelligent devices" mantra that was echoed so strongly in most publications 8 defining Jini. But at the core, Jini is about services. It is about delivering services regardless of the platform. Whether the code is burnt into a device or deployed on a farm of servers is not the issue. The intent is that services are not limited to traditional platforms. The Jini design recognizes the emergence of devices as the next logical platform for executing networked-application code. The Complexity of Cooperation and Collaboration Service-based architectures that use dynamic discovery will be difficult to build. Despite the claims that XML-based protocols will usher in a new level of standard definitions, XML is simply a markup language. The difficult part is still gaining consensus on the usage and conformance to the specifications. XML has evolved with many standards groups, industry groups, and independent organizations defining metadata definitions that are not compatible. It is not clear how many competing definitions will ultimately survive. As of this writing, there is a tremendous amount of hype over Web services, although major gaps still exist in the definition. For instance, how is service composition defined, and how is the coordination of message interaction choreographed? Even Jini does not adequately define service composition. However, projects like RIO (http://www.developer.jini.org/exchange/projects/rio/) and Abaco(http://www.developer.jini.org/exchange/projects/abaco/) are presenting solutions using container-based models. These containers provide the services that control quality of service, operations, administration, and management parameters. Comparing Jini to Other Solutions How is Jini different from other solutions? Dynamic Code Movement A primary difference between Jini and other solutions is its capability to move code around the network. Because it uses RMI and object serialization, it is able to download code on demand. There is no human intervention required in the process. This has some distinct advantages over other systems being defined. The client of a service does not need the software installed prior to running the application. As long as the client can attain the interface to the service, it can execute the code. Software agent technology, especially mobile agents, can benefit from the framework Jini already has established. Dynamic Construction and Formation of Groups and Communities Dynamic discovery enables peers and services to find each other. It allows for the quick formation of groups and communities sharing a common interest. However, speed of assembly must be tempered with network integrity. Jini uses a combination of unicast and multicast protocols to dynamically discover services on the network. There is the notion of a "well-behaved service" that implements these protocols in a specific manner. The intent is to ensure broadcast storms do not occur, and that there is a higher level of resiliency in the network. Resource conservation will be required in any system that needs to scale dynamically. Whether it is enforced explicitly in the API and/or implicitly through "rules of engagement" remains unknown. Applications that use broadcast techniques to discover peers can contribute to serious network congestion when traffic does not get distributed appropriately. Just how dynamic service discovery will be in the Web services architecture has not been determined. 9 There is no clear winner is this space. However, one of the key components of the Jini framework is dynamic service discovery and the controlled formation of communities. Self-Healing Networks Jini introduces another concept to minimize disruption; that of leasing resources. It is fundamental to the Jini model. It ensures that resources that are no longer available eventually get removed from the network. This is not done through human intervention, but rather through system grooming. This has been compared to a "survival of the fittest" philosophy. Services that don't perform well will eventually lose their lease in the Jini network and be pruned from the system. Every resource must have a management process (leasing) that periodically checks to see if everything is OK, and to ensure the resource is still required by the system or user. Leasing is one of the most important features, providing the robustness of the Jini infrastructure. Leasing permits the LUS to delete services that have failed to renew their lease. If you provide a service, or if you are using a service, you must periodically renew your lease. The renewal period is dependent on the application. You request a time-based renewal period on your lease, and the service provider determines the actual lease commitment. Today when you use the Web, often you are hit with bad or invalid URL references, or sites that are slow to respond. Business-to-Business (B2B) e-commerce is being built on this same infrastructure and architecture. System and network instability will be an issue. B2B e-commerce will require the self-healing properties of a Jini network. In addition, Jini supports the core requirements of B2B e-commerce—distributed transactions. Richer Type-Based and Polymorphic Service Lookups Jini is Java-based. Interfaces define the services and the protocols in the Jini network. Jini does not support the notion of XML being fundamental to the process of defining and accessing services on the network. In terms of the other solutions, Jini appears to be the maverick. Jini relies on the richness of type semantics. There is also the belief that these type definitions and the ease with which they support sub-typing (inheritance) resembles the way we build systems. Systems can evolve as the technology evolves. The other approaches rely on XML definitions or APIs that limit the systems' adaptability to change. This introduction has tried to convince you that many of the requirements of the new wave of distributed services are now aligned to push Jini into the forefront of the emerging networkedapplication. The Tipping Point In the Malcolm Gladwell book The Tipping Point, the author describes social, economic, and technological changes that happen very quickly and sometimes from small events. He compares the spread of these changes to epidemics, and cites numerous examples. The tipping point for Hush Puppies—a company that makes brushed-suede shoes—happened in the mid-nineties. The brand was facing extinction, with sales totals reaching only 30,000 pairs per year. The company responsible for making the shoe was contemplating phasing out the line. Then something unusual started to happen. The shoe became popular in the trendy Soho district of New York. Young males were buying up the shoe in resale stores, and it was becoming hip in the clubs and bars of downtown Manhattan. The story goes on to recount the rapid increase in 10 popularity, all without any promotional activity. The increase in popularity was attributed strictly to word of mouth and the "right" people wearing the shoes and spreading the look. In 1995 the company sold 430,000 pairs of the shoes, and doubled that in the next year. The year 1995 was the tipping point for Hush Puppies. Similar stories are told throughout the book of changes that start small but quickly spread like epidemics; changes that many times go against our intuition and experience. Gladwell concludes that starting epidemics requires concentrating resources on a few key areas and a few key types of individuals, and testing your own intuition. I believe there are changes happening in networks that are going to cause technologies—some new, some not so new—to soon hit the tipping point. Jini has the potential to be one of them. If not Jini, some technology that embodies Jini's characteristics will soon find itself in that position. Understanding the concepts and techniques employed by Jini technology will prepare you for the inevitable tipping point in the networked application. 11 Chapter 2. Jini in the Age of Services IN THIS CHAPTER • • • • The Emerging Networked Application Understanding the Layers of Communication The New Protocols Existing Protocols in the Middleware Maze What differentiates a Jini network from other networks? This chapter will define a Jini network. The objective is to clarify many misconceptions about the proper positioning of Jini in the Open Systems Interconnection (OSI) model, and compare and contrast a Jini network to the emerging Web services, peer-to-peer systems, and the Semantic Web. The Emerging Networked Application In Chapter 1, you learned about the rise of the networked application. It is not new. It has been around since networks themselves. It involves application-to-application (A2A) communication over a shared private or public network, as illustrated in Figure 2.1. Figure 2.1. Application-to-Application (A2A) communication. Protocols and Services The fundamental requirement supporting this model is that the applications agree on the information exchange and the service that is provided. This information exchange is referred to as the protocol. A protocol is a set of rules and accepted conventions for communication. It defines the packet (exchange) format and data structure to use for service requests; the service being the agreement or the expected action as a result of the exchange. Protocols are defined at different levels of abstraction, from low-level transmission protocols to high-level application semantics. Any non-trivial application being defined today must consider the implications of emerging protocols and services. 12 In addition, applications are required to be more adaptive to the systems they inhabit and the networks they roam. As a result, development has become more complex. Programming languages, toolkits, and integrated development environments have had trouble keeping pace with the rate of change in applications. What once was considered impossible over networks has become commonplace. The explosive growth of network technologies is promoting new business opportunities and business partnerships. Globalization of the business economy will only fuel demand for networked applications. In addition, network devices are gaining popularity with both consumers and business market segments. The proliferation of wireless technologies is forcing us to rethink our assumptions on application and network protocol integration. The applications being developed today are more closely tied to the network than ever before. Of course, the Internet and the Web have been the most recent catalysts for this change. The proliferation of applications on the Internet is the result of the establishment of standard protocols—namely HTTP over TCP/IP. With the standardization and ubiquity of TCP/IP networks, higher-layer protocols have become the new battleground for application interoperability. The new breed of protocols is battling for market and mind share to influence and direct the development of post-millennium applications. Let's review networking protocols and the services that have evolved to support the global-local network. Then the new protocol drivers will be put into perspective. Understanding the Layers of Communication Protocol layering is a common technique for simplifying network design by defining functional layers and assigning protocols to perform each layer's task. For example, it is common to separate the functions of data delivery and connection management into separate layers, and therefore separate protocols. Protocol layering produces simple protocols, each with a few well-defined tasks. These protocols can then be assembled into a useful whole. Individual protocols can also be removed or replaced as needed for particular applications. A layered approach is considered better practice than implementing a large "brick" of code that is difficult to understand. Layered architectures promote reuse, exchangeability, incremental development, and offer the advantage of localizing changes to specific layers. As the network evolves, layers can be replaced or exchanged without reengineering the entire communication architecture. Open System Interconnection (OSI) Model The International Standardization Organization (ISO) defined the OSI 7-layer model, illustrated in Figure 2.2, to describe the protocols and services required for A2A communication. The origins of the work can be traced back to 1977. To this day it is used as the reference model to discuss and compare application communication over distributed systems. Figure 2.2. The OSI reference model. 13 The model provides the following layer definitions. Physical Layer Layer 1 is the physical layer of the OSI model. It defines the physical and electrical characteristics of the network. The NIC cards in your PC and the interfaces on your routers all run at this layer. Data-Link Layer Layer 2 is known as the data-link layer. It defines the access strategy for sharing the physical medium. Protocols, such as PPP, SLIP, and HDLC reside here. On an Ethernet, this is where the access to each NIC takes place. Devices that depend on this layer include bridges and switches. Network Layer Layer 3 is the network layer, providing a means to establish, maintain, and terminate network connections. The IP protocol resides at this layer as do some routing protocols. Routers in the network are operating at this layer. Transport Layer Layer 4 is the transport layer, and it is where TCP and UDP reside. For TCP, this layer relieves the session layer's burden of ensuring data reliability and integrity. Should a packet fail to arrive— perhaps because of miswriting—it will be retransmitted to this layer. Session Layer 14 Layer 5 is the session layer. It provides services for the management and control of data flow between two computer systems. For example, the session layer enables the insertion of synchronization points in the flow of application information between computer systems. Management services enable activities to be started, halted, and abandoned under the direction of the application. Session management has implications to the load balancing that are possible across different servers in a server pool. For instance, care must be taken to understand when a session is taking place not to interfere with it. Presentation Layer Layer 6 is the presentation layer. This is where application data is either packed or unpacked, ready for use by the running application. Protocol conversions, encryption/decryption, and graphics expansion all take place here. This is the layer that ensures a common representation of application information is provided while information is in transit between computer systems. Application Layer Finally, Layer 7 is the application layer. This is where the end user and end-application protocols, such as HTTP, telnet, ftp, and mail (pop3 and smtp) are mapped; although OSI purists might only map ACSE, ROSE, FTAM, and other OSI-compliant protocol data units. OSI is an important reference model, and TCP/IP has become the de facto standard for the network and transport layers. The definition above the transport layer, however, tends to be application-specific. Session, presentation, and application are often collapsed into a single process-application layer. The DoD Model The Department of Defense Four-Layer Model was developed in the 1970s for the DARPA Internetwork Project that eventually grew into the Internet. The core Internet protocols adhere to this model, seen in Figure 2.3. Figure 2.3. The DoD reference model. 15 The four layers in the DoD model are the network access layer, the Internet layer, the host-to-host layer, and the process layer. Network Access Layer The network access layer is responsible for delivering data over the particular hardware media in use. Different protocols are selected from this layer, depending on the type of physical network. Internet Layer The Internet layer is responsible for delivering data across a series of different physical networks that interconnect a source and destination machine. Routing protocols are most closely associated with this layer, as is the IP Protocol, which is the Internet's fundamental protocol. Host-to-Host Layer The host-to-host layer handles connection rendezvous, flow control, retransmission of lost data, and other generic data flow management. The TCP and UDP protocols are the dominant Internet protocols at this layer. Process Layer The process layer contains protocols that implement user-level functions, such as mail delivery, file transfer, and remote login. You can map common Internet application protocols easily into the DoD model, seen in Figure 2.4. 16 Figure 2.4. Protocol mapping to the DoD model. What surfaces from the mapping is the variety of protocols that have emerged in the process application space. In addition, rather than defining similar supporting functions and protocols per layer, the protocols at the higher layers address specific application needs. For instance, SMTP provides mail support, while FTP provides file transfer capabilities. Both protocols require common access to remote systems; however, each protocol manages sessions, presentation, and application functionality specific to its unique requirements. This lack of factoring out common functions at higher layers of the OSI model has driven single-application, single-protocol development. While HTTP has become the de facto standard communication protocol for the Web, it is not clear how emerging protocols will leverage or compete with its definition. Many have decided to simply tunnel through HTTP for transport and define higher-level application-specific protocols and services to complement it. Streaming audio, instant messaging, remote method invocations, and even mobile agents are being tunneled through HTTP. Often this decision is based on the global access and reach HTTP enjoys into organizations and through firewalls. However, despite the dominance of HTTP, a new battleground for protocols is taking place above the transport layer. The New Protocols This section will focus on the protocols being defined in the higher layers above the transport. We will outline the services and protocols supporting: • • • E-commerce (Web services) Peer-to-peer applications The Semantic Web Web Services—E-commerce Protocols and Services 17 The focus of Web services is on e-commerce. A Web service is built on existing and emerging standards such as HTTP; Extensible Markup Language (XML); Simple Object Access Protocol (SOAP); Web Services Description Language (WSDL); and Universal Description, Discovery, and Integration (UDDI). A Web service is an interface that describes a collection of operations that are network-accessible through standardized XML messaging. A Web service is described, using XML notation, by its service description. It covers all the details necessary to interact with the service, including message formats that detail the operations, transport protocols, and location. The goal of Web services is to enable companies to reduce the cost of participating in e-commerce by standardizing the definition of common A2A communication. Companies may also consider Web services as a distributed component model, enabling reuse within the enterprise. The Web services architecture is based upon the interactions between three roles: • • • Service provider Service registry Service requestor The interaction of these roles can be seen in Figure 2.5. Figure 2.5. Role interaction in Web services. The service provider is the platform that hosts access to the Web service. This is typically the business process owner. The service requestor is the application that is searching for, and invoking or initiating an interaction with a service. This can be a person or a program without a user interface; for example, another Web service. The service registry is a searchable registry of service descriptions where service providers publish their service definitions. Service requestors find services and obtain binding information (in the service descriptions) for services during development or during execution. 18 The service description contains the details of the interface and implementation of the service. This includes its data types, operations, binding information, and network location. The service is the actual implementation. The Core Services The Web services architecture defines three core services: • • • The publish service enables the service provider to publish a service description, so the service requestor can find, understand, and use the service. The find/discovery service enables the service requestor to retrieve a service description directly, or query the service registry for the type and description of the service required for invocation. The bind service enables a service requestor to invoke or initiate an interaction with the service at runtime using the binding details in the service description. These three operations—publish, find, and bind—are core to the Web services model. All three services are provided in the higher layers of the OSI reference model. Protocols Now let's look at the protocols involved in the Web services architecture, as shown in Figure 2.6. Figure 2.6. Protocols supporting Web services. Web services are being built on common Internet protocols in addition to some emerging XMLbased encoding. The SOAP layer represents the use of XML as the basis for the messaging protocol. SOAP is an enveloping mechanism for communicating document-centric messages and remote procedure calls using XML. It is basically an HTTP POST with an XML envelope as the payload. It defines a mechanism to incorporate extensions to the HTTP message using SOAP headers and a standard encoding of functions. A SOAP message consists of three parts: • • • A mandatory SOAP envelope An optional SOAP header A mandatory SOAP body The envelope is the top element of the XML document, representing the message. The header is a mechanism for adding features to a SOAP message without prior agreement between the communicating parties. SOAP defines a few attributes that can be used to indicate who should deal with a feature and whether it is optional or mandatory. 19 The body is a container for mandatory information intended for the ultimate recipient of the message. SOAP defines one element for the body: the fault element used for reporting errors. SOAP messages support the publish, find, and bind services in the Web Services architecture. SOAP provides the standard access protocol, which enables Web services to be integrated into Web architectures and existing legacy architectures. The next layer, WSDL, provides the standard for service definition. WSDL is an XML-based service description language. WSDL defines the interface and mechanics of service interaction as a set of endpoints operating on messages containing either document-oriented or procedureoriented messages. The service interface defines WSDL elements that comprise the service description. The portType element defines the operations of the Web service. The operations define what XML messages can appear in the input and output data flows. The message element is used to define the input and output parameters of each operation identified in the portType element. The types element is used to describe the use of complex data types within the message. The binding element describes the protocol, data format, security, and other attributes for a particular service interface. Additional description is necessary to specify the business context, qualities of service, and service-to-service relationships. The WSDL document is used with other service description documents to describe higher-level aspects of the Web service. For example, business context is described using UDDI data structures in addition to the WSDL document. In summary, Web services are being built on the common protocols of the Internet protocol suite, TCP/IP, and initially HTTP. In addition, bindings to other higher layer protocols, such as FTP and SMTP, are being defined. Web services add SOAP as the common access protocol, and XMLbased service description protocols such as WSDL and UDDI. More information about SOAP and WSDL can be found at the W3C consortium Web site at www.w3.org. Specific SOAP releases can be found at the Apache Web site xml.apache.org/soap. Information relevant to UDDI can be found at www.uddi.org. Web services have gained considerable vendor support as a result of the involvement of IBM and Microsoft. Microsoft is basing its new generation .Net applications on the Web services architecture. P2P Protocols and Services The term peer-to-peer (P2P) indicates that two sides of a communication link use the same protocol to conduct a networked conversation. Any computer can initiate a conversation with any other computer. Peer-to-peer defines an application-to-application model that presents communicating nodes as equals in terms of functional capabilities. It promotes a decentralized model, where peer or service discovery is not confined to a specific node or role in the architecture. This is illustrated in Figure 2.7. Figure 2.7. P2P: A network of "equals." 20 P2P architectures have not been standardized above the transport layer, although most have gravitated to exploiting the HTTP protocol. This provides P2P applications with simple universal access across the Web. The services being offered by P2P applications can be classified into four functional categories: • • • • Managing and sharing information Collaboration Networking and Infrastructure E-Commerce Enablement Managing and Sharing Information P2P services that distribute the management and sharing of computer resources across the network are in this category. File Sharing With file sharing, each node in the network represents a potential repository for files. In effect, the network becomes a distributed database of massive size and potential. Supporting protocols provides management and discovery of resource locations and file transfers. Resource Sharing Complete resource sharing distributes computations out onto the network, which enables a node to act as a general task manager. It can then collect and aggregate results. What makes the model interesting is its capability to dynamically enlist worker machines to perform a common task. Distributed Search Engines The capability to query for specific information using distributed search engines has proven valuable in navigating the vast amount of information that is available on the Internet. Search protocols that use a divide-and-conquer approach to information discovery are popular. Collaboration P2P services that provide the organization and collaboration of groups with common interests or goals are in this category. Document Exchange Management 21 Products like Groove (www.groove.net) are promoting a secure document management and exchange facility over public networks. Group collaboration is promoted by assigning levels of trust to peer participants. Instant Messaging As opposed to e-mail, instant messaging products are offering real-time people-to-people conversations over the Internet. These products have been heavily promoted and integrated into commercial products. Additional features being developed include secure and reliable messaging. Inter-Organizational Workflow A new area of development for P2P is in the coordination of workflow processes within an organization. The capability to dynamically broadcast changes in application state makes some P2P models attractive for adaptation in process control applications. Communities Fundamental to P2P applications is the capability to discover peers who share a common interest. Communities that dynamically form as a result of common interest or motivation are at the core of many P2P applications. Internet Gaming Entertainment applications that provide group interaction are often encoded using P2P protocols with a single task and game state coordinator. Networking and Infrastructure P2P services that provide the management and integration of applications with network resources are in this category. Integration with Web Services P2P applications provide another integration model for Web services. As opposed to centralized repositories, more decentralized implementations support consumer-oriented services. Device management, service monitoring, and resource management applications are being developed to support and complement current network management protocols. E-commerce Enablement A plethora of e-commerce enablement applications are being defined, such as: • • • • • • Online auctions Pricing and payment models Trading hubs and e-commerce communities Customer Care Services for mobile users Service personalization Protocols 22 The protocols involved in the P2P model are as varied as the applications being designed, as seen in Figure 2.8. Figure 2.8. Lack of standards has led to application-specific protocol definition. JXTA Protocol Sun has recently released JXTA (juxtapose), which is an attempt to standardize the high-level protocols being used in the P2P space. JXTA is transport- and language-agnostic; however, and not surprisingly, Java and TCP/UDP are the areas of initial development. In JXTA all protocols are defined as XML messages sent between two peers. JXTA messages define the protocols used to discover and connect peers and peer groups and to access network services offered by peers and peer groups. Messages are sent between peer endpoints. A peer endpoint is a logical destination (embodied as a URI) on any transport capable of sending and receiving datagram-style messages. Endpoints are mapped into physical addresses by the messaging layer at runtime. JXTA defines the following XML-encoded protocols: • • • • • • Peer Discovery Protocol—enables a peer to find advertisements on other peers and can be used to find any of the peer, peer group, or advertisements. Peer Resolver Protocol—enables a peer to send and receive generic queries to find or search for peers, peer groups, pipes, and other information. Peer Information Protocol—enables a peer to learn about other peers' capabilities and status. Peer Membership Protocol—enables a peer to obtain group membership requirements, such as an understanding of the necessary credential for a successful application to join the group. Pipe Binding Protocol—enables a peer to bind a pipe advertisement to a pipe endpoint, thus indicating where messages actually go over the pipe. A pipe can be viewed as an abstract, named message queue that supports a number of abstract operations such as create, open, close, delete, send, and receive. Peer Endpoint Protocol—enables a peer to ask a peer router for available routes for sending a message to a destination peer. Gnutella Protocol Gnutella is a peer-to-peer file sharing and searching system that uses a decentralized control model. It uses a broadcast messaging protocol for peer discovery. The message broadcasting technique allows for the dynamic discovery of peers—systems running the Gnutella software also known as servants—that are capable of responding to a Gnutella request. The protocol uses HTTP as the underlying transport. Every message has a unique ID and defines the following functions: 23 • • • • • Ping Request—This is sent by a client requesting a pong response from everyone on the network. This message is propagated on the network until the message is either dropped or returned to the originator. Pong Response—This is sent in response to a ping request. This message contains the host IP, port identification, a count of how many files the host is sharing, and the total size of those files. Client Push Request—This is for servants behind a firewall where the client cannot reach the server directly. A push request message is sent, asking the server to connect out to the client and perform an upload. Search—A search message contains a query string, as well as the minimum network speed that is acceptable to the client. Search Response—This contains the IP address, port, and speed of the servant followed by a list of file sizes and names and a unique ID that identifies the servant. In summary, P2P applications are emerging as another model of networked applications that exploit the decentralized nature of the Internet and leverage the vast array of resources that are accessible. New application protocols are being defined on top of the common protocols of the Internet protocol suite, which includes TCP/IP and HTTP. Much like the workhorse protocols—FTP, Telnet, and SMTP—the new breed of protocols tend to be application-specific. However, with JXTA, Sun is trying to define some level of standardization. It is too early to determine the degree of success Sun will have with this effort. Semantic Web Protocols and Services The Semantic Web is trying to extend the existing Web architecture to enable computers and people to work more efficiently. The proposed solution hinges on the use of metadata to describe the data contained on the Web. Metadata is data about data. In the context of the Semantic Web, it is data specifically describing Web resources. The fundamental component of the Semantic Web is the XML-encoded Resource Description Framework (RDF). RDF is a foundation for processing metadata. It provides interoperability between applications that exchange machine-understandable information on the Web. RDF can be used in a variety of application areas including: • • • • • Resource discovery to provide search engine capabilities Cataloging to describe the content and content relationships available at a particular Web site, page, or digital library Intelligent software agents to facilitate knowledge sharing and exchange Content rating when describing collections of pages that represent a single logical document Intellectual property rights of Web pages, and for expressing the privacy preferences of a user as well as the privacy policies of a Web site RDF is also a model for representing named properties and property values. RDF properties can be thought of as attributes of resources, and in this sense, correspond to traditional attribute-value pairs. Additionally, RDF properties represent relationships between resources, so an RDF model can resemble an entity-relationship diagram. Entities described by RDF expressions are called resources. A resource identifier identifies resources. A resource identifier is a URI plus an optional anchor ID. A property is a specific attribute or relationship used to describe a resource. 24 A specific resource, together with a named property plus the value of that property for that resource, is an RDF statement. The statement Robert Flenner is the creator of the resource at http://www.jworkplace.org is represented in RDF/XML as: <rdf:RDF> <rdf:Description about="http://www.jworkplace.org"> <s:Creator>Robert Flenner</s:Creator> </rdf:Description> </rdf:RDF> A complete HTML document containing RDF metadata describing itself is as follows: <html> <head> <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:dc="http://purl.org/metadata/dublin_core#"> <rdf:Description about=""> <dc:Creator> <rdf:Seq ID="CreatorsAlphabeticalBySurname" rdf:_1="Mary Andrew" rdf:_2="Jacky Crystal"/> </dc:Creator> </rdf:Description> </rdf:RDF> </head> <body> <P>This is a fine document.</P> Sc2 </body> </html> While RDF is the fundamental encoding of semantic information, the Semantic Web model includes higher-level constructs, such as ontologies, to provide a common classification and vocabulary for metadata definitions. An ontology provides a common vocabulary for classifying and defining information or knowledge. It allows systems to share information in a structured and meaningful way. Often artificial intelligence programs or software agents are used to process an ontology and make decisions based on the content of the information. (See Figure 2.9.) Figure 2.9. Semantic Web concepts. 25 RDF is not tied to any specific transport protocol, but does build on and attempt to complement the current Internet protocols. RDF also relies heavily on the concepts of XML, Namespaces, and URI-based addressing. One of the advantages to this approach for improving networked applications is that it aligns with the current Web architecture using familiar constructs. Existing Protocols in the Middleware Maze The battleground above the transport layer is not new. Middleware, which includes services such as messaging, Transaction Processing (TP) monitors, and Remote Procedure Calls (RPC), has been involved in defining higher-layer services for years. These services provide added value in building traditional distributed systems. Middleware fits into the OSI (Open Systems Interconnection) model as shown in Figure 2.10. Figure 2.10. The middleware domain also includes protocols above the transport layer. Middleware supports the application, presentation, and session layers, and might involve some portion of the transport layer. The application, presentation, and session layers can contain: • • Remote Procedure Calls (RPC) Message Oriented Middleware (MOM) Systems 26 • • • Object Request Brokers (ORB) Transaction Processing (TP) monitors Database access technologies, such as SQL Middleware The Middleware domain is typically between the application and the interface to the operating system and network services. However, it is often difficult to position middleware products because their use and scope vary significantly. Middleware Types Middleware has also surfaced in a number of protocols at higher layers that must be understood in the context of the networked application. (See Figure 2.11.) Figure 2.11. Middleware protocols at higher layers are often product- and vendorspecific. Message Oriented Middleware Message oriented middleware (MOM) permits general message exchange using message queues. It has become a popular approach to integrating inter- and intra-enterprise applications. The message queuing facility enables clients and servers to communicate indirectly, which provides several advantages. Applications are provided a higher level of abstraction when dealing with network communication. Destinations, or queues, can have logical names and be built up hierarchically. Clients and servers do not have to be running at the same time to carry on a conversation. Messaging products can therefore help support the disconnected client. Traffic can be distributed or load balanced across multiple queues and multiple systems. This provides a scalable solution to high throughput requirements. Messaging queues are versatile. They enable you to define communication models that support one-to-one or one-to-many patterns of message transfer. MOM products offer various levels of message assurance, from best effort to guaranteed delivery. This is provided through persistent queues that are capable of storing messages until acknowledged. Messaging products should continue to gain in popularity because of their broad range of capabilities in application communication. However, these products are more prevalent within an enterprise rather than between enterprises because of the proprietary protocols that often need to be supported. 27 Remote Procedure Calls Like messaging, a remote procedure call (RPC) provides communication between a client and a server process. RPCs try to hide the intricacies of the transport by using ordinary procedure call semantics. A client calls a remote procedure and suspends processing until it gets back a response. Under the covers, an RPC runtime is capable of intercepting the call, and marshalling and unmarshalling the parameters for network transport. In a general sense, RPCs are used to describe Request-Reply message exchanges with basic datatype translation and connection-oriented communication services. It can also refer to products that use an Interface Definition Language (IDL) to describe the argument lists for outgoing and incoming parameters. Many database management systems build RPC processing into their applications; in addition, RPCs might be found in other products such as network operating systems and transaction monitors. RPCs are always synchronized. Web services rely heavily on RPC semantics because of the single-request, single-response exchanges popular on the Web today. CORBA (Common Object Request Broker Architecture) The OMG (Object Management Group) defined the Common Object Request Broker Architecture (CORBA) in an attempt to standardize the communication between distributed object systems. The OMG has gone to great lengths to define common services required of distributed object systems. CORBA was defined to promote and enable interoperability between heterogeneous systems. Applications can be written in different programming languages and still be able to communicate. An Object Request Broker (ORB) provides a common software bus connecting distributed objects. Requests are sent over the network using standard, on-the-wire IIOP (Internet Interoperability Protocol) or GIOP (General Interoperability Protocol). The OMG has defined a number of services to support the object request broker architecture. These include everything from a Naming Service to an Object Transaction Service. OMG defines standards at an interface level using a standard Interface Definition Language (IDL). Implementers of the services are free to optimize different features and characteristics of the service to best fit their target market. CORBA-based architectures have been most prevalent in large enterprises, where heterogeneous systems dominate. Transaction Processing Monitors TP monitors specialize in managing transactions from clients across one or more servers. When a transaction ends, the TP monitor ensures that all systems involved in the transaction are left in a consistent state. The classic example of the bank credit transaction followed by the bank debit transaction is often cited to demonstrate the need for transactional integrity. Transaction monitors are able to cross system boundaries to manage the transaction process. This includes starting server processes, routing and monitoring their execution, and balancing their workloads. In addition, TP monitors are used to guarantee the ACID properties to all programs that run under their control. 28 The ACID properties are: • • • • Atomicity—All the operations grouped under a transaction occur or none of them do. Consistency—The completion of a transaction must leave the system in a consistent state. If the transaction cannot achieve stable end-state, it must return the system to its initial state. Isolation—Transactions should not be affected by other transactions that execute concurrently. Participants in a transaction should see only intermediate states resulting from the operations of their own transaction, not the intermediate states of other transactions. Durability—The results of a transaction should be as persistent as the entity on which the transaction commits. TP monitors often use thread pools to assign work to available threads that act as a mechanism to balance and control the load on any one server process. The TP monitor is able to funnel work to shared server processes and act as a gatekeeper for the OS. This function—load balancing—is a primary benefit to systems that must scale to support high transaction volumes. TP monitors are able to establish priorities for tasks, and thus prioritize transactions within the system. Fundamentally, the TP monitor assures ACID properties while enabling high throughput. This alleviates much of the work application programmers would normally have to be concerned with, such as network failures, concurrency, load balancing, and the synchronization of resources across multiple nodes. SQL Middleware Unfortunately, while SQL has been standardized for years, vendor extensions and variations still make access across products a challenge. Many middleware products exist to provide access to multiple databases and transform SQL calls to a native database server's access language. Typically, SQL middleware must deal with vendor-proprietary APIs across Windows, Linux, Macintosh, and Unix clients. In conjunction with the API is a unique driver that handles runtime calls and formats SQL messages. The driver typically provides a proprietary transport exchange format with the server. Therefore, the format and protocols defined to access the database are vendor-defined. Most vendors support multiple protocol stacks that might include a common transport interface, such as sockets or named pipes. Even providing a common SQL interface still requires management of multiple vendor drivers. In addition, although a common API exists, multiple formats and protocols are still supported under the covers. A number of solutions, such as ODBC and JDBC, have emerged, but often run into performance and scalability issues under high access scenarios. Gateway protocol converters are often used, such as Remote Data Access (RDA) and IBM's Distributed Relational Data Access (DRDA). In summary, middleware has an established presence in most organizations. The protocols that support middleware services often reflect the vendor's integration strategy. In other words, Oracle middleware will offer Oracle database integration and IBM middleware will offer IBM product integration to products such as CICS, DB2, and MQSeries. The Protocol and Services Maze One thing is clear: The number of protocols and services continues to expand. Integrating applications with traditional middleware technologies has been difficult. 29 The introduction of Web services only presents a standard definition for defining integration specifications, not a standard solution. P2P systems are ripping into conventional techniques, but have not matured to the level necessary to demonstrate clear direction and solutions. The introduction of network-aware devices, such as cell phones and PDAs, over Wireless Access Protocols (WAP) will further complicate an already complex networked application environment. The rise of the "device market" is ushering in a host of new protocols geared not only at traditional wide-area and local-area networks, but also new residential markets. For instance, standards such as the Home Audio and Video Interoperability (HAVi) standard, the Home Phoneline Networking Alliance (HomePNA), HomeRF, and the Universal Serial Bus (USB) are all developing standards to communicate with devices. These devices all require management and service coordination. The proliferation of technologies such as narrowband Internet access, as well as the faster digital subscriber loop (DSL) and cable modem technologies, will further complicate compatibility with market-specific devices and network management schemes such as DOCSIS and SNMP. It is in this context that we introduce Jini as a framework for building order out of chaos in the age of services and networked applications. Jini—Building Order Out of Chaos As mentioned in Chapter 1, Jini is about federating services on a network. In this respect, it shares many of the concepts of the technologies reviewed in this chapter. Federating services enables us to create logical groupings of services and then become a member of the federation. However, Jini has matured far beyond the basic "find and bind" scenarios that have been covered to this point. The Jini Services Jini services can be classified as the following: • • • Core Services responsible for the creation and organization of the federation Helper Services instrumental to providing a framework for good Jini citizenship User Defined Services providing application-specific extensions to the Jini federation Discovery Service The discovery service enables us to find registries—LUS—in Jini and to locate service interfaces. The discovery service is implemented by the discovery protocol. The discovery protocol relies on a TCP/IP network that supports unicast and multicast messaging. Unicast and multicast messaging will be discussed in detail in Chapter 4, "Building on Jini Foundation Concepts." Join/Publish Service Join is a form of discovery in that it requires a lookup service to be found and accessed in order to publish service interface definitions. It uses similar unicast- and multicast-messaging mechanisms to those just mentioned. Join and lookup are also discussed in detail in Chapter 4. Event Service The event service provides an interface to enable distributed event notification. Jini also provides a helper service—the event mailbox service—that is used to enable store and forward-type 30 semantics for event notification. The mailbox service also provides critical support for disconnected clients and services. The event mailbox service is discussed in Chapter 7, "The Helper Services." Leasing Service Jini introduces the idea of a lease to service management. If you are a user or a provider of a service, you must ensure that the resources you consume are managed through the leasing process. All resources must have some management process that periodically checks to see whether everything is OK, and to determine whether the resource is still an active participant. Leasing is an important part of Jini infrastructure robustness. Leasing enables the LUS to delete services that have failed to renew their lease. If you provide or are using a service, you must periodically renew your lease. The renewal period depends on the application. You request a finite renewal period on your lease, and the service provider determines the actual lease commitment. Leasing is often referred to as providing the "self-healing" properties of a Jini network. Look for the concept of leasing to enter into the definition of other service-based architectures! Transaction Service Jini defines a transaction interface and provides a reference implementation. Transactions are at the core of many systems. The exchange of goods and services involves monetary transactions. Transactions and transactional integrity are not new issues in distributed computing. However, traditional solutions have tended to be heavyweight and complex. As the notion of distributed transactions is expanded to include a broader array of clients, such as cell phones, Palm Pilots, and mobile agents, we will require support for more flexible and lightweight transactions. Jini uses the traditional two-phase commit transaction protocol to enforce the well-known ACID properties in data manipulations. Database management systems go to great lengths to ensure ACID properties. As a result, system overhead is generally proportional to the degree of transactional integrity. More system resources are required to ensure data integrity in the light of failures that might occur in reading, writing, and updating information. The notion of data consistency with regard to partial failure is promoted through the two-phase commit-based interface. But, the key is the degree to which the implementation will try to ensure consistency despite a partial or catastrophic failure. By providing only an object interface to transactions, Jini does not dictate implementation. Lightweight implementations will reduce the overhead and time required to deploy transactional systems. Similar to MOM, which provides various levels of message assurance, the new breed of transactional systems will provide various levels of transactional integrity. Persistence Service Jini provides a persistence interface and a reference implementation called JavaSpaces. JavaSpaces was heavily influenced by the concept of tuple spaces, which were first described in 1982 in the Linda programming language. The basic idea is that you can have many active programs distributed over physically dispersed machines, unaware of each other's existence, able to communicate by releasing tuples into the shared tuple space. Programs read, write, and take tuples from the tuple space that are of interest to them. JavaSpaces provides a mechanism to loosely couple systems through remote events and a persistent shared-memory model. Processes can exchange information through this space and be notified of changes through asynchronous events. JavaSpaces supports two implementation models: transient spaces and persistent spaces. Transient spaces do not survive across a system restart, while persistent spaces use secondary storage to save and restore services upon activation. 31 The simplicity of the JavaSpaces API provides an easy interface, yet it still provides a powerful programming model. Jini Protocols Jini is different. Jini assumes it's a Java world. Although the specification does not require Java, this is like saying you don't need wheels on your car to drive it. You can always pick it up and carry it; however, that is clearly not the path of least resistance. The power of Jini shines through when used in the homogeneous environment of Java and RMI. This provides the linchpin to integrate the disparate technologies that seem to be flourishing out of control. (See Figure 2.12.) Figure 2.12. Most Jini implementations rely heavily on Java and RMI. The Jini services are defined using Java Interfaces. So, unlike the XML-encoded protocols of Web services such as RDF, JXTA, and a host of other incarnations, Jini relies on Java bytecodes. Essentially, you need a Java virtual machine (JVM) somewhere on your network to use Jini effectively. In the next chapter, we will discuss setting up a Jini environment and concentrate on RMI to demonstrate the power of the Jini underpinnings. Bytecode At the base level, Jini interfaces are defined using bytecode, which is the representation of a Java class file to the JVM. It is what contains the instructions and definitions required for the JVM. It is possible to generate Java bytecode without ever writing Java source code. However, this should be considered the exception as opposed to the norm. RMI-Marshalled Objects Java objects that are transported across the network are wrapped in a MarshalledObject, which serializes the object into a byte array. RemoteObjects within a marshalled object are serialized as stubs. Stubs and skeletons will be explained in the next chapter. Interfaces As opposed to other networked applications, Jini applications are defined by Java remote interfaces. XML is not used as a high-level interface definition, description, or protocol construct. Interfaces provide for polymorphic changes in software evolution. Systems built on XML will soon discover that XML only goes so far in defining system behavior. It is the behavior of the exchange, not just the information that gets exchanged, that ultimately defines the application. XML is a time bomb for service definition that has started ticking! Entries 32 The Entry interface provides the basis for adding attributes to service definitions. When services register with a lookup service, they can provide additional information to aid in client search and selection. Summary Jini, unlike the other solutions discussed, is closely tied to a specific programming language. It is very much a Java-centric solution to the networked application. Java has many benefits that make it a contender in building service-oriented applications. It is proven, has a widespread distribution, runs on multiple platforms, is object-oriented, and has built-in security. Also unlike the other solutions, RMI provides Jini with the capabilities of mobile code "out-ofthe-box." This might prove to be one of the biggest differentiating factors in service capabilities. 33 Chapter 3. Supporting Technologies IN THIS CHAPTER • • • The HTTP Server Remote Method Invocation Summary Jini is about federating and organizing services on a network and allowing clients to discover and use services. The Jini framework is designed to minimize the amount of human intervention required to manage services. When you first start establishing a Jini environment, you may doubt that the goal of minimal human intervention was attained. There are a number of services that need to be started in order to deploy a basic Jini network. The initial process can be difficult, especially if you are new to remote method invocation (RMI), but after your network is established, the base services seldom need administration or management. There are three infrastructure services that will be required somewhere on your Jini network. They are as follows: • • • An HTTP server that supports dynamic downloading of code The Remote Method Invocation daemon (rmid) that provides the distributed remote object lookup and activation mechanism, covered later in this chapter The Jini LUS that provides the service registry and the service lookup capabilities on the Jini network, which is covered in Chapter 4, "Building on Jini Foundation Concepts." The HTTP Server The HTTP server should not be new to you. Anyone who has surfed the Web has encountered an HTTP server. It is based on support for the HTTP protocol, which is the standard protocol for fetching Web pages across the Internet. Jini requires that an HTTP server be available on the network—or a server capable of file transport (ftp)—because Jini uses this familiar protocol to transport files (code) from machine to machine. The HTTP Server is one of the first services that you will start when setting up your Jini network. This enables Jini to transport downloadable code to applications when the applications need it. You can run one or more HTTP servers on your Jini network. I recommend starting with a single HTTP server. You will want to monitor when the code gets transported across the network by various service requests. It is much easier to start and monitor a single instance of a service until you are comfortable with the service interactions. It might then be appropriate to introduce additional servers to support multiple service configurations, redundancy, scalability, and unique application requirements. The Role of the HTTP Server and Protocol Any HTTP server should be able to satisfy the requirements of Jini. The simple and familiar GET method of the HTTP protocol is used to request a specific file (URL) for download. We will discuss the mechanics of requests in the section on RMI, but for now just recognize that requested files are jar or class files necessary to instantiate objects to process distributed requests. 34 Jini-Supplied The class com.sun.jini.tool.ClassServer that is supplied with the Jini distribution implements a simple HTTP server for serving up jar and class files. Note You can download the Jini binaries and source code from the Java Developer Connection at http://developer.java.sun.com/developer/products/jini. Appendix A, "Environment Set Up," provides the necessary links and resources for the Jini distribution. The HTTP server can be started with the following command on Windows: java -jar %JINI_HOME%\lib\tools.jar -port 8080 -dir %JWORK_HOME%\lib -trees -verbose where %JINI_HOME% references the Jini implementation directory, for example C:\files\Jini1_1; and %JWORK_HOME% references the download directory that contains the jar and class files that you will expose to clients. Any files that you want to make available to clients or services should be placed in this directory location. The other arguments include: • • • • [-port port] [-dir dir] [-trees] [-verbose] The default port is 8080 and can be overridden with the -port option. The default directory on Windows is J: and the default directory on non-Windows systems is /vob/jive/jars. The default can be overridden with the -dir option. By default, all files under the default directory (including all subdirectories) are served up via HTTP. If the -verbose option is given, then all attempts to download files are output to the console. The -trees option can be used to serve up individual files stored within jar files, in addition to the files that are served up by default. If this option is used, the server searches all jar files in the top-level directory. If the name of the jar file is name.jar, then any individual file named within that jar file can be downloaded using a URL in the form of http://host:port/name/file. When this option is used, an open file descriptor and cached information are held for each jar file, for the life of the process. In the examples, I've set the following Windows variables to define the location of the Jini install directory and the location of files available via the HTTP server. Your setup will probably be different. set JINI_HOME=D:\files\jini1_1\ set JWORK_HOME=C:\JiniJavaSpaces\services\lib GUI-Activated 35 Jini also comes with a GUI-supplied service activation program. It enables you to enter the required and optional parameters to start Jini services in a tabbed pane as an alternative to scripts. The following command starts the service launcher interface. From the command prompt, type the command: java -cp %JINI_HOME%\lib\jini-examples.jar com.sun.jini.example.launcher.StartService To use the launcher, you can open and edit the properties file located under the /jini1_1/ examples/launcher directory from the file menu. This will generate the tabs shown in Figure 3.1 and enable you to customize the parameters to your environment. There are properties files for both Unix and Windows that supply a list of default values. Figure 3.1. A graphical view to starting and stopping Jini services. After you load the appropriate properties file, your display should look similar to this: If you are new to Jini, you can use this interface to examine the basic parameters that are available to start Jini services in addition to rmid and the HTTP server. This will help you become familiar with the commands that are used to start the various services. To start the Web server supplied with Jini, simply select the WebServer tab and set the following properties, as seen in Figure 3.2: • • • Executable jar file: Specify the path to Jini tools.jar Port: Specify the port on which to run the HTTP server Document Area: Specify the path from which to serve jar files Figure 3.2. Example Web Server parameter settings. 36 Now select the Run tab and start the Web server. On the console you will see the command invoked and a list of the files that are in the directory pointed to by the -dir argument of the Document Area property. If the service failed to start, you might want to check the "Gotchas and Common Failures" section in Appendix A to troubleshoot the problem. Alternatives As mentioned at the start of this section, any HTTP server should be able to perform the functions required to support Jini. In fact, the Jini HTTP server implementation is a minimalist approach to satisfying the startup requirements. If you already have a Web server, check your server documentation to determine the necessary configuration parameters to access files. This is usually nothing more than a parameter in a startup script indicating a root directory or path where you place Web files. Now let's take a look at another infrastructure service: remote method invocation (RMI). RMI is often the source of confusion when establishing a Jini environment. For this reason, it is important to understand how RMI fits into the Jini architecture and ensure that you understand the dependencies between RMI and the Jini environment. Remote Method Invocation RMI enables Java applications running in different Java Virtual Machines (JVM) to communicate. Whether these JVMs exist on the same host machine or on different machines does not matter. Of course, this is not new to distributed processing. Just like RPC (Remote Procedure Calls), the processes need not exist in the same address space or even on the same machine. However, RMI is able to offer a number of advantages over other distributed models because it assumes the environment is Java-enabled. 37 The Java platform's remote method invocation system has been specifically designed to operate in the Java application environment. The RMI system assumes the homogeneous environment of the JVM; therefore, the system can take advantage of the Java platform's object model. Client applications invoke local calls through a stub interface that communicates with the actual remote object as illustrated in Figure 3.3. The RMI runtime performs all the necessary communication housekeeping to ensure that two processes running on separate JVMs can exchange invocation requests and results through an exposed common interface definition. Figure 3.3. Client invocation using RMI. In the Java platform's distributed object model, a remote object is described by one or more remote interfaces, which are interfaces written in the Java programming language. A remote interface is a Java interface that extends the java.rmi.Remote interface and defines the methods, which are all made available to remote clients. All methods declared in the interface that can be invoked remotely must declare that they throw a java.rmi.RemoteException. For example, the following simple Agent interface extends Remote, and the talk method throws RemoteException. The object that implements this interface is exposing its talk method to clients in other JVMs. Listing 3.1 The Agent Interface import java.rmi.Remote; import java.rmi.RemoteException; public interface Agent extends Remote { public String talk() throws RemoteException; } A client with a reference to the Agent can invoke the talk method regardless of where the Agent implementation physically resides. Of course, there are network and security considerations that impact this capability. Firewalls are one of the most frequently encountered inhibitors to RMI communication. Security will be discussed in detail in Chapter 9, "Security in Jini." The RMI Registry In the previous chapter, you saw the importance of discovery to the emerging networked applications. So how are remote objects in RMI discovered? 38 The remote object registry (RMI registry) is a repository for associating a name with a remote object. The java.rmi.Naming class provides methods for storing and obtaining references to remote objects in the rmiregistry. The Naming class's methods (bind, unbind, rebind) take as one of their arguments a name that is a URL-formatted java.lang.String of the form: [rmi:][//][host][:port][/name] where: • • • • rmi names the protocol and may be omitted host is the host (remote or local) where the registry is located, port is the port number on which the registry accepts calls, name is a string that represents the remote object. Both host and port are optional. If host is omitted, the host defaults to the local host. If port is omitted, then the port defaults to 1099. Binding a name to a remote object associates the name with the remote object. If you have used other distributed object systems such as CORBA, this process should not be new. A remote object can be associated with a name using the Naming class's bind or rebind methods. For security reasons, the Naming.bind, Naming.rebind, and Naming.unbind methods can only be executed on the same host as the RMI registry. After a remote object is registered (bound) with the RMI registry on the local host, callers on a remote (or local) host can look up the remote object by name, obtain its reference, and then invoke remote methods on the object. Unicast Servers RMI provides convenience classes that remote object implementations can extend to facilitate remote object creation. These classes are java.rmi.server.UnicastRemoteObject and java.rmi.activation.Activatable. A remote object implementation must be exported to RMI. Exporting a remote object makes that object available to accept incoming calls from clients. For a remote object implementation that is exported as a UnicastRemoteObject, the exporting involves listening on a TCP port for incoming calls. More than one remote object can accept incoming calls on the same port. A remote object implementation can extend the class UnicastRemoteObject to make use of its constructors that export the object, or it can export the object via UnicastRemoteObject's static exportObject methods. Listing 3.2 The UnicastRemoteObject Class public class UnicastRemoteObject extends RemoteServer{ protected UnicastRemoteObject() protected UnicastRemoteObject(int port) protected UnicastRemoteObject(int port, RMIClientSocketFactory csf, RMIServerSocketFactory ssf) public Object clone() public static RemoteStub exportObject(Remote obj) 39 public static Remote exportObject(Remote obj, int port) public static Remote exportObject(Remote obj, int port, RMIClientSocketFactory csf, RMIServerSocketFactory ssf) public static boolean unexportObject(Remote obj, boolean force) } The no argument constructor of the UnicastRemoteObject, creates and exports a remote object on an anonymous port chosen at runtime. The second form of the constructor takes a single argument, port, that specifies the port number on which the remote object accepts incoming calls. The third constructor creates and exports a remote object that accepts incoming calls on the specified port via a ServerSocket created from the RMIServerSocketFactory. Clients make connections to the remote object via Sockets supplied from the RMIClientSocketFactory. You will make use of socket factories to change the underlying socket behavior to support additional connection features, such as security and encryption. Chapter 9 will examine customizing socket connections. Let's run through an example that demonstrates the power of RMI and, in the process, prepares your environment for Jini. Building an RMI Non-Activatable Service You will define, build, and test a command processor that will enable you to invoke commands on a remote machine and return an object that represents the command's result. The steps involved include the following: 1. Define the example command processor. Define the remote interface (in this example, the Shell interface that extends java.rmi.remote). Create an RMI service implementing the remote interface. Create a client application and example command. 2. Build the example command processor, demonstrating the necessary files required. Compile the files. Create the required stub interfaces. Create jar files (client-side and server-side). Define the security policy files for client and server. 3. Test the example command processor and supporting RMI environment. Start the RMI registry. Start the RMIShell server. List the registry contents to verify setup. Test the client. 40 Define Remote Interface (extend java.rmi.remote) Interfaces are important to Jini and RMI. You'll build a command processor that will enable us to invoke commands on any machine on the network using the Shell interface. You'll use the familiar Command pattern (see Design Patterns by Erich Gamma, et al. for more information) to define an interface that has an execute method and returns an object. This enables us to define a simple but powerful interface. Notice we are not extending the java.rmi.Remote interface, but rather Serializable. Listing 3.3 The Command Interface public interface Command extends java.io.Serializable { public Object execute(); } The Command interface will be used as a parameter of the remote interface Shell as seen in the following code. Therefore, the Command interface is not the interface that you expose and invoke remotely. Instead, it will be used as a parameter to the remote interface. Parameters that are defined in the method signatures of an interface must be serializable so that they can be serialized (converted into byte streams) and transported between virtual machines. Additionally, parameters that implement remote interfaces are passed as stubs (references) to the calling process. The remote interface Shell takes a Command object as an argument, extends java.rmi.remote, and throws RemoteException. Listing 3.4 The Shell Interface public interface Shell extends java.rmi.Remote { public Object executeCommand(Command cmd) throws RemoteException; } The two interfaces, Shell and Command, are delivered to developers of clients and servers of the command processor. This is all that would be required to enable developers anywhere in the world to start developing commands that would work in the processor framework. Of course, the more developers building and sharing Command objects, the better. One hundred developers building command processors based on this interface would make things both interesting and exciting. One thousand developers building and linking command processors around the globe—well, we just don't know! Create RMI Service That Implements Remote Interface Let's start with a simple command processor, RMIShell. RMIShell implements the Shell interface and extends UnicastRemoteObject. Listing 3.5 The RMIShell Class // Our Command Processor Class public class RMIShell extends UnicastRemoteObject implements Shell { public RMIShell() throws RemoteException { super(); 41 } As mentioned previously, the UnicastRemoteObject class defines a remote object whose references are valid only while the server process is running. The UnicastRemoteObject class provides support for point-to-point active object references using TCP streams. Because you are extending UnicastRemoteObject, the object is automatically exported to the RMI runtime during construction. Inheriting from UnicastRemoteObject eliminates the need to call exportObject. Let's look at the main method for RMIShell. Listing 3.6 The RMIShell Class Continued // Our service requires remote behavior and extends UnicastRemoteObject import java.rmi.server.UnicastRemoteObject; // Our interface definitions import org.jworkplace.command.Command; import org.jworkplace.command.Shell; public static void main(String[] args) { if(args.length < 1) { System.out.println("usage [hostname]"); System.exit(1); } // set the hostname from the 1st argument String hostname = args[0]; // must set a security manager to load classes if (System.getSecurityManager() == null) { System.setSecurityManager(new RMISecurityManager()); } try { Shell shell = new RMIShell(); // prepare to bind the server with a name String name = "//" + hostname + "/RMIShell"; Naming.rebind(name, shell); System.out.println("RMIShell bound"); } catch (Exception e) { System.err.println("RMIShell exception: " + e.getMessage()); e.printStackTrace(); } } // implementation of the Shell interface public Object executeCommand(Command cmd) { return cmd.execute(); } } The first thing you must do is install and set an appropriate security manager that protects access to system resources from untrustworthy downloaded code. Forgetting to do this will result in a runtime error, which will throw a java.lang.SecurityException. The security manager determines whether downloaded code has access to the local file system or can perform any other privileged operations. 42 All programs using RMI must install a security manager, or RMI will not download classes other than from the local classpath in remote method calls. This restriction ensures that the operations performed by downloaded code go through a set of security checks. In addition to installing a security manager, you also define an associated security policy. grant { permission java.security.AllPermission "", ""; }; Security will be examined in detail in Chapter 9. For now, just ensure that you have a policy file available that grants all permissions, similar to the one shown previously. You can save it in a working directory as policy.all. This should only be used in a test situation where access to outside networks is nonexistent or well-controlled. You will eventually set the policy file controlling the process at runtime, by setting a property on the command line, when you start the service. After you have installed a security manager and exported the remote object to RMI, you need to associate a name to the object so that clients can find the service. As mentioned before, this is done by using the java.rmi.Naming class. The Naming class takes a name that is a URLformatted java.lang.String of the form: [rmi:][//][host][:port][/name] A sample fragment would look like the following: String name = "//hostname/RMIShell"; Naming.rebind(name, shell); This code fragment associates the name RMIShell with the remote object running on the designated host machine. The port is optional and defaults to 1099, which is the default port for the RMI registry. The only thing left necessary to complete our service is to implement the Shell interface. public Object executeCommand(Command cmd) { return cmd.execute(); } The implementation invokes the Command interface's execute method and returns the resulting object to the client. Of course, the server has no idea what the command actually is, because it is passed to the server as a parameter in the method invocation. In this case, the client is providing the server with the command to execute. Because we have not set any security, almost any command could be passed, which is a dangerous proposition. Let's look at how the client code is developed. Create a Client Application and Example Command The client implements the CommandLine class and also defines a class that implements the Command interface. Listing 3.7 The CommandLine Class import java.rmi.*; import org.jworkplace.command.*; 43 public class CommandLine { public static void main(String args[]) { if(args.length < 2) { System.out.println(" usage [hostname] [remote directory]"); System.exit(1); } // even clients must set a security manager if (System.getSecurityManager() == null) { System.setSecurityManager(new RMISecurityManager()); } try { // we pass the hostname in as an argument and perform lookup String name = "//" + args[0] + "/RMIShell"; Shell shell = (Shell) Naming.lookup(name); // we define a class that implements the Command interface // argument 1 represents a directory on a remote machine FileList command = new FileList(args[1]); // execute command and cast result object String[] files = (String[]) (shell.executeCommand(command)); // error invalid directory supplied if(files == null) { System.out.println("Invalid directory"); } else { // display results to console for(int i=0; i<files.length; i++) System.out.println(files[i]); } } catch (Exception e) { System.err.println("FileList exception: " + e.getMessage()); e.printStackTrace(); } } } The CommandLine class first installs a security manager. Like the server-side development, you will also need a very liberal client-side security policy file: grant { permission java.security.AllPermission "", ""; }; You will use the java.rmi.Naming.lookup() method to get a reference to a remote object. This is the same name that was supplied to the server when it was registered: //hostname/RMIShell. The RMI runtime will return an instance of the Shell remote interface. This is actually a reference to a remote interface that the local stub object will use to resolve to the remote implementation object when the remote object is accessed by calling the executeCommand method. The command object developed in this example is a FileList object. 44 Listing 3.8 The FileList Class import java.io.File; import org.jworkplace.command.*; // implements our serializable Command interface public class FileList implements Command { private String path; // we paramaterize the command with a path public FileList(String path) { this.path = path; } // invoke the command and return object public Object execute() { File file = new File(path); // simple file list return file.list(); } } The FileList class implements the Command interface. The constructor takes a directory path as a parameter. The execute method creates a File object and returns a String[] array of file and directory names. On the command line, you supply the remote directory name, which in this case is args[0]. This enables us to get a directory listing from a remote machine. The remote machine executes the command locally and returns the result to the client. The remote machine does not know the command that is being executed. In fact, the remote machine does not even have the FileList class available. This might be a good point to stop and look at the sequence of exchanges that are supporting the command processor, as illustrated in Figure 3.4. Figure 3.4. Sequence of exchanges in the Filelist command example. 45 1. The RMIShell registers with the RMI registry using the RMI protocol. 2. The RMI registry uses the HTTP server identified by the codebase property of the startup script to download the required files (for instance, RMIShell_Stub.class in rmishell-dl.jar) using the URL protocol. 3. The client retrieves the remote reference to the RMIShell interface by invoking the java.rmi.Naming class to resolve the name-to-object reference. 4. The client downloads the required files from the server-side HTTP server to invoke the service. 5. The client invokes executeCommand, which is processed locally by the stub class RMIShell_Stub. RMIShell_Stub and the RMI runtime marshals (formats) the parameters necessary to invoke the command remotely and sends the request using the RMI protocol. 6. The RMIShell implementation (actually the RMI runtime classloader) requires the definition of the FileList class from the client and accesses the class definition from the client-side HTTP server using the URL protocol. As you can see, there is a lot of communication going on under the covers in this scenario. It requires an HTTP server on the client-side and the server-side to satisfy requests for missing class definitions. This capability to dynamically move code to the point of execution demonstrates a significant difference between RMI and other distributed object systems. The client contacted the HTTP server on the implementing host and required the remote stub interface (RMIShell) to download the required classes. The server required the definition of the command FileList and contacted the client-side HTTP server to download the required classes. You could have also defined a scenario where a parameter passed would have been implemented as a Remote interface. In this case, a stub (reference) would have been downloaded from the client and a remote request from the server to the client would have resulted. In effect, a callback to the client would have made the server a client of the client. Compile the Files Change to the chapter3 directory, where you copied the examples for this book; for example, C:>cd \chapter3\example1. You should see the following Java files along with other files that will help you build the service: Command.java CommandLine.java FileList.java RMIShell.java Shell.java There is a compile.bat file included that will compile the files. Simply invoke: 46 C:\chapter3\example1>compile This should create the necessary class files in the current directory and the stub file RMIShell_Stub.class. Create the Required Stub Interfaces The RMI stub protocol is used by the stub to communicate with the server. To generate a stub, you must invoke the rmic compiler for the remote service you are defining; in this case RMIShell. The supplied compile.bat file invokes the rmic to generate the stub file for the RMIShell class. You'll be using the 1.2 version of rmic because we are assuming JDK 1.2 or later support across the environment. Create Jar Files (Client-Side and Server-Side) There is a jars.bat file included in the directory that generates the necessary jar files for deployment. Simply invoke C:\chapter3\example1>jars You should study the commands in the jars file, so that you understand how the jar files are created, and perhaps more importantly, why. The jar files generate the following: • service.jar— Contains the RMIShell command processor and supporting classes. • • • service-dl.jar— Contains the RMIShell_Stub class and supporting classes. client.jar— Contains the CommandLine client and supporting classes. client-dl.jar— Contains the Command and the FileList classes. The jars.bat also uses manifest files to identify the main class in each jar file (client—service) to be invoked at runtime. As a general rule, you will use the -jar option when invoking java. This helps to alleviate classpath problems by ensuring that all files are accessed from jar files as opposed to current classpath settings. Getting into this habit of construction will help in your future development. Copy the command-dl.jar file to the client HTTP directory. This is the same location that you used to start the HTTP server. Copy the client-dl.jar and service-dl.jar file to the appropriate HTTP directories: the client-dl.jar to the client HTTP server directory and the service-dl to the service HTTP directory, as seen in Figure 3.5. Figure 3.5. Client and server HTTP directory display. 47 Define the Security Policy Files for Client and Server Create the policy.all file for both the server and the client: grant { permission java.security.AllPermission "", ""; }; These files will set the security permissions to use for the JVM when you run the Java programs. Start the RMI Registry On the server, unset the classpath and start the RMI Registry: C:\>setenv CLASSPATH= C:\> rmiregistry& Try to use the same directory every time you start the RMI Registry until you are sure that your environment is set up properly. Also, avoid starting the registry in the same directory in which you are about to run your server. The classpath for the downloaded jar files will not be set properly if the required files are found in the current directory. If you use the convention (as shown) of always creating jar files, this should eliminate some of the potential problems. Start the RMIShell Server Go to the directory where the RMIShell has been developed. Start the RMIShell command processor. In this example there is a "service" directory under example1 that contains a startService script. The script defined for Windows is startService.bat. It is invoked by the command: C:\JiniJavaSpaces\chapter3\example1\service>startService remote_host_name The remote_host_name argument is the hostname or IP address where the RMI Registry is running. For instance, you would invoke startService 172.16.1.2 if you were running RMI Registry on IP address 172.16.1.2. 48 The startService script requires that an environment variable, HTTP_ADDRESS, be set prior to running the script. The HTTP_ADDRESS is the location of the running HTTP_SERVER. For example: set HTTP_SERVER=172.16.1.2:8081 would indicate that the HTTP server is running on the host address 172.16.1.2 at port 8081. The startService is defined as follows: Listing 3.9 The startService.cmd script @echo off if not "%HTTP_ADDRESS%" == "" goto gotServerHTTP echo You must set HTTP_ADDRESS to point at your service HTTP server echo For example set HTTP_ADDRESS=[hostname][:port] goto exit :gotServerHTTP java -jar -Djava.security.policy=%JINI_HOME%\policy\policy.all Djava.rmi.server.codebase=http://%HTTP_ADDRESS%/service-dl.jar service.jar %1 :exit You will notice the script requires that two additional parameters be set— Djava.security.policy and the -Djava.server.codebase. The -Djava.security.policy parameter is set to the server-side location of the policy.all file. The script defaults to the Jini distribution file at \policy\policy.all. The -Djava.rmi.server.codebase parameter is set to the server-side HTTP server address (hostname:port) and the jar file (service-dl.jar) you make available to clients. You should see on the HTTP console the access to the service-dl.jar file. If you do not see this access, then you might have a classpath problem. The registry should gain access to the class definitions using the HTTP server, and not on the local classpath. If the registry does not use the HTTP server, you need to verify that the classpath was not set when you started the RMI registry. Stop the registry and verify. List the Registry Contents to Verify Setup There is a list registry program that you can use on the server to verify that the RMIShell service is registered and active as RMIShell in the registry. The program is ListRMIServices and is in the directory \utilities. Copy the program to a directory on a machine that has access to the RMI registry. To verify, run the program using the following command: C:\JiniJavaSpaces\utilities\java ListRMIServices You should see the service listed on the console as: 49 rmi://:1099/RMIShell Test Client To test the client, move to the directory and machine where you deployed the client software. Run the following client command: C:\JiniJavaSpaces\chapter3\example1\client>startClient remote_host_name remote_host_directory Where -Djava.security.policy is the path to your policy file, and Djava.rmi.server.codebase is set to the client-side HTTP server address (hostname:port) and the jar file you will make available to servers. remote_host_name is the name of the remote host, and remote_ host_directory is the remote directory to search and display files. Again the script requires that the environment variable for HTTP_ADDRESS be set to the location of the client-side HTTP server. For example, on the command line type: set HTTP_SERVER=[hostname][:port] where hostname is the name or IP address of the machine running the client-side HTTP server; and :port is the port number you assigned or the default 8080. If you are trying this example on a single machine, you will have to change either the server or client port assignment; otherwise, a port conflict will result. You will see the contents of the remote directory displayed on the console. You have just overcome the first major hurdle in establishing an RMI-Jini environment. Congratulations! Building on the Java 2 Platform With Java 2, RMI defined two types of service based on activation policy—activatable and nonactivatable. A non-activatable service is one that you must manually start—like the RMIShell service you just defined. RMI also supports activatable services. An activatable service is one that RMI can start automatically, depending on its configuration. RMI has the capability to restart services when it is activated or to start services when they receive their first incoming call. This automatic activation is instrumental to enabling a more robust distributed environment. An activation daemon takes care of restarting, deactivating, and reactivating services. As I'll demonstrate, Jini uses this environment in a number of interesting ways. The Intelligent Socket The next approach to writing RMI services is by extending Activatable as opposed to UnicastRemoteObject as seen in Figure 3.6. There are a number of key components to the activation model that should be highlighted. Because many of the Jini services are activatable services, this background will be beneficial. 50 Figure 3.6. Class hierarchy for remote objects. The java.rmi.activation.Activator interface provides the basic functionality of activation. The Sun implementation of this interface is provided through the sun.rmi.server.Activation. GroupImpl and is invoked by starting the RMI daemon process. The rmid is a daemon process that is started when you reboot your system. The typical command to start the process is: rmid –J-Dsun.rmi.activation.execPolicy=none Of course, this is bypassing security constraints and continues with the theme in this chapter of gratuitous security policies—a theme that will change in subsequent chapters! In addition to the Activator, other key elements of the activation model include: • • • The java.rmi.activation.ActivationGroup, which is a group of services that have been identified to share a common JVM. The java.rmi.activation.ActivationMonitor, which tracks and monitors the state of an object in an activation group and the state of the activation group as a whole. The java.rmi.activation.ActivationSystem, which provides the interface to register activatable objects and groups. Building an RMI-Activatable Service Let's outline the changes necessary to make the RMIShell an activatable service. 51 1. Change inheritance from UnicastRemoteObject to Activatable. 2. Create an activation group. 3. Create an activation object description. 4. Register the activation description with rmid. 5. Bind the stub to the registry. The RMIActivatableShell class provides an example of an RMI activatable service. Listing 3.10 The RMIActivatableShell Class package org.jworkplace.service; import java.rmi.Naming; import java.rmi.RemoteException; import java.rmi.RMISecurityManager; import import import import import import import import java.util.Properties; java.rmi.activation.Activatable; java.rmi.activation.ActivationDesc; java.rmi.activation.ActivationID; java.rmi.activation.ActivationGroup; java.rmi.activation.ActivationGroupDesc; java.rmi.activation.ActivationGroupID; java.rmi.MarshalledObject; import org.jworkplace.command.*; // extend Activatable public class RMIActivatableShell extends Activatable implements Shell { public RMIActivatableShell(ActivationID id, MarshalledObject data) throws RemoteException { // register object with activation system using anonymous port super(id, 0); } // our implementation public Object executeCommand(Command cmd) { return cmd.execute(); } public static void main(String[] args) { if(args.length < 3) { System.out.println("usage [hostname] [codebase] [policy]"); System.exit(1); } String hostname = args[0]; String codebase = args[1]; String policy = args[2]; // set security manager try { if (System.getSecurityManager() == null) { System.setSecurityManager(new RMISecurityManager()); 52 } // create the activation group description Properties props = new Properties(); props.put("java.security.policy", policy); // set class path props.put("java.class.path", System.getProperty("java.class.path")); props.put("java.rmi.server.codebase", codebase); ActivationGroupDesc agd = new ActivationGroupDesc(props, null); ActivationGroupID agi = ActivationGroup.getSystem().registerGroup(agd); ActivationGroup.createGroup(agi, agd, 0); // create the activation object description ActivationDesc ad = new ActivationDesc("org.jworkplace.service. RMIActivatableShell", codebase, null, true); // register the activation description with rmid Shell shell = (Shell) Activatable.register(ad); // bind the stub to the registry String name = "//" + hostname + "/RMIActivatableShell"; Naming.rebind(name, shell); System.out.println("RMIActivatableShell bound"); System.exit(0); } catch (Exception e) { System.err.println("RMIActivatableShell exception: " + e.getMessage()); e.printStackTrace(); } } } Change Inheritance From UnicastRemoteObject to Activatable The first change is to redesign the class hierarchy so that the new shell inherits from Activatable. The Activatable constructor will export the object just like UnicastRemoteObject did. The activation system will call the constructor with a unique activation ID and a marshalled object that contains any information that you want to pass to the activated object. This information is passed when you register the service, as illustrated in the following: public class RMIActivatableShell extends Activatable implements Shell { public RMIActivatableShell(ActivationID id, MarshalledObject data) throws RemoteException { // register object with activation system using anonymous port super(id, 0); } Create an Activation Group Description 53 An activation group descriptor contains the information necessary to create and re-create an activation group in which to activate objects. The description contains: • • • The group's class name The group's code location (the location of the group's class) A "marshalled" object that can contain group-specific initialization data The group's class must be a concrete subclass of ActivationGroup. A subclass of ActivationGroup is created/re-created via the ActivationGroup.createGroup static method that invokes a special constructor that takes two arguments—the group's ActivationGroupID, and the group's initialization data (in a java.rmi.MarshalledObject). You construct an activation group descriptor that uses the system defaults for group implementation and code location. Properties specify Java environment overrides (which will override system properties in the group implementation's JVM). The command environment can control the exact command/options used in starting the child JVM, or can be null to accept rmid's default. // set the policy and codebase properties to start the group Properties props = new Properties(); props.put("java.security.policy", policy); props.put("java.rmi.server.codebase", codebase); // use the rmid default command environment ActivationGroupDesc agd = new ActivationGroupDesc(props, null); // get the activation group id ActivationGroupID agi = ActivationGroup.getSystem().registerGroup(agd); // create the group ActivationGroup.createGroup(agi, agd, 0); Create an Object Activation Description You've just created a group description, now you need to create a specific object description. An activation descriptor contains the information necessary to activate an object, which includes the following: • • • • • The object's group identifier The object's fully qualified class name The object's code location (the location of the class) and a codebase URL path The object's restart mode A marshalled object that can contain object-specific initialization data A descriptor registered with the activation system can be used to re-create/activate the object specified by the descriptor. The MarshalledObject in the object's descriptor is passed as the third argument to the remote object's constructor for objects to use during reinitialization/activation. ActivationDesc ad = new ActivationDesc("RMIActivatableShell", // class name codebase, // codebase null, // no marshalled data passed true); // restart 54 Register and Bind Register the object with rmid and then bind the name to the registry. Shell shell = (Shell) Activatable.register(ad); String name = "//" + hostname + "/RMIActivatableShell"; Naming.rebind(name, shell); That is all that is required to create an activatable object. As a result of the changes, you now have an object implementation that can be created on demand, and if a failure occurs, will be automatically started when the rmid process is restarted. Let's return to the GUI-supplied service activation program so you can start rmid and test your activatable service. To start rmid, simply select the RMID tab and set the following properties, as shown in Figure 3.7: • • RMID command: rmid Options: -J-Dsun.rmi.activation.execPolicy=none Figure 3.7. Parameters to start the rmid application. Then, select the Run tab and start rmid. The activation daemon, when started, silently executes unless a parameter for debug information has been set, or something goes wrong! If the service fails to start, you might want to check the "Gotchas and Common Failures" section in Appendix A to troubleshoot the problem. When rmid starts, it creates a log directory and file used to record the state of the activation system that rmid manages. Now let's build and test our RMI-activatable Service. The compilation and stub generation is similar to the non-activatable shell you just created. 55 Compile the Files Change to the chapter3 directory where you originally copied the examples for this book; for example, C:>cd \chapter3\example2. You will see the following files: CommandLine.java FileList.java Command.java Shell.java RMIActivatableShell.java Invoke the java compiler: D:\chapter3\example2>compile.bat Create the Required Stub Interfaces The RMI stub protocol is used by the stub to communicate with the server. To generate a stub you must invoke the rmic compiler for the remote service you are defining; in this case RMIActivatableShell. The supplied compile.bat file invokes the rmic compiler to generate the stub file for the RMIActivatableShell class. Create Jar Files (Client-Side and Server-Side) There is a jars.bat file included in the directory that will generate the necessary jar files for deployment. Simply invoke: C:\chapter3\example2>jars The same procedure should be followed as in example1 for coyping the jar files to the appropriate directories. You will replace the current jar files. See example1 earlier in this chapter for details. Define the Security Policy Files for Client and Server The policy file setup is the same as in example1. Start the RMI Registry On the server, unset the classpath and start the RMI registry. \usr\files\JWorkPlace>setenv CLASSPATH= \usr\files\JWorkPlace> rmiregistry& Again, try to use the same directory every time you start the RMI registry until you are sure that your environment is set up properly. Also, avoid starting the registry in the same directory in which you are about to run your server. 56 Start the RMIActivatableShell Server Go to the directory where the RMIActivatableShell was developed. Start the RMIActivatableShell command processor. Again, in this example there is a "service" directory under example2 that contains a startService script. D:\chapter3\example2\service>startService You should verify the contents of the script. Specifically, the three parameters at the end of the command are for: • • • The hostname of the RMI registry The codebase parameter for service startup The policy file for service activation This time when you start the service, it will register with the activation system (rmid) and then exit. When the client invokes the FileList command, the activation system will start the service. Again, you will see the access to the service-dl.jar file on the HTTP console. If you do not see this access, then a classpath problem may be the culprit. List the Registry Contents to Verify Setup To verify the service, move to the directory and machine where you deployed the server software: \usr\files\JWorkPlace>java – Djava.security.policy=/usr/JWorkPlace/policy.all org.jworkplace.rmi.ListRMIServices You will see the service listed on the console as rmi://:1099/RMIActivatableShell. Test Client The test client step is exactly as in example1. Again, you will see the contents of the remote directory displayed on the console. You no longer have to start the shell manually. It will be started automatically when the first request to access the service is invoked (see Figure 3.8). Figure 3.8. Activatable service activation. 57 Congratulations! You now have an environment in place that provides the necessary support for bootstrapping Jini. Jini and the RMI Activation Daemon An RMI activation daemon is needed by several Jini services—for example, the Jini LUS, the Transaction Manager, and the persistent JavaSpaces Service. An instance of the daemon needs to be active on each of the machines where Jini services will run. You will see that the RMI framework significantly impacts and influences the design of Jini services. When a Jini service starts, it registers with rmid. This registration process is run in its own JVM spawned by the RMI daemon. At the completion of registration, the process exits. The registration process consists of passing parameters to rmid that are sufficient to register and start a Jini service or, as was just demonstrated, any remote service. So, there are two steps: 1. The service will register with rmid in a separate JVM. 2. Activation of the service by rmid will also occur in a separate JVM when requested. Summary The Jini reference implementation relies heavily on RMI. So in effect, it inherits all the benefits of the RMI infrastructure. Although it is not a requirement to use RMI with Jini, you will lose many of the "out-of-the-box" benefits—a distributed object model supporting code mobility for one. RMI provides: 58 • • • • Automatic activation of services and service restoration Resource conservation by only activating a service when requested Support for activation groups that share a single JVM Automatic generation of proxy objects for remote communication There might be cases where the target platform or device in which a Jini service has been implemented is resource-constrained and unable to fully participate in an RMI environment. There are alternative architectures available, such as the surrogate architecture that will be covered later in the book. For now, we need to demonstrate the role RMI plays in a typical Jini network. With the background presented on RMI, you are ready to move into the core Jini services, which are the subjects of the next several chapters. 59 Chapter 4. Building on Jini Foundation Concepts IN THIS CHAPTER • • Foundational Services Summary Foundational Services The most important concept within the Jini architecture is that of a service. The specification (see Appendix C, "Links, Community, Resources") defines a service as "an entity that can be used by a person, a program, or another service. A service might be a computation, storage, a communication channel to another user, a software filter, a hardware device, or another user." This is a rather broad definition of what a service may entail. This section will provide an overview of the foundational services within a Jini network to provide a more concrete representation of Jini services. Let's start with the services that support the process of service discovery. Service Discovery As was highlighted in Chapter 1, "Introduction to Jini," discovery of services is an important concept. In peer-to-peer systems, it enables us to discover peers on the network. Web services promoted service discovery through a Universal Description and Discovery (UDDI) registry. Even the concepts of the Semantic Web involved software agents being able to dynamically discover services. So, it is not surprising that Jini also addresses the discovery of services in a Jini network as fundamental to enabling the formation of groups and peers. In Jini parlance, these groups or federations are referred to as a djinn, or Jini community. Services are found and resolved by a lookup service (LUS). The lookup service is the central bootstrapping mechanism for the system, and provides the major point of contact between the system and users of the system. A lookup service maps Java language-defined interfaces indicating the functionality provided by a service to sets of objects that implement the service. In contrast to the RMI registry, which uses name-to-object mapping (see Figure 4.1), the Jini lookup service uses interface-to-object mapping. This proves to be a powerful concept, because interfaces are inherently extensible. Figure 4.1. The RMI registry associates a name to an object, as opposed to Jini, which associates an interface to an object reference. 60 Jini lookup services are organized into groups. You can create groups with any name that you desire. There is also a default group called the public group. When you start a lookup service, you specify the groups that the lookup service will support. In addition, when you search for a lookup service, you specify the groups you require the lookup service to support. Groups can be organized along departmental boundaries, such as Marketing Department or Advertising Department, or along other dimensions, such as areas of interest—sports services, vacation services, and so on. The organization of lookup services is really system- and application-dependent. Security considerations often influence the configuration. A service is added to a lookup service, and therefore to a group by a pair of protocols called discovery and join. Understanding Discovery Management The discovery and join protocols are protocols that enable services to discover, become part of, and advertise supplied services to the other members of the Jini community. The discovery protocol is used to: • • • Announce the presence of a lookup service on a local area network (LAN multicast). Discover one or more lookup services on a local area network (LAN multicast). Establish communication with a specific lookup service over a wide area network(WAN unicast). The discovery protocol requires support for multicast or restricted-scope broadcast (such as UDP), along with support for reliable unicast delivery in the transport layer (such as TCP). The discovery protocol makes use of the Java platform's object serialization capabilities to exchange information in a platform-independent manner. The join protocol makes use of the discovery protocol to provide a standard sequence of steps that services should perform when they are starting up and registering themselves with a LUS. The join protocol is used to register a service and advertise its functionality in all lookup services of interest. The discovery protocol supports multicast and unicast messaging. 61 Multicast Versus Unicast Messaging Multicast is much like radio or TV in the sense that only those who have tuned their receivers—by selecting a particular frequency—receive the information. You hear the channel you are interested in, but not the others. The sender sends the information without knowledge of the number of receivers. In contrast, when you send a packet and there is only one sender and one recipient, this is referred to as unicast. TCP is unicast-oriented. On the other hand, UDP supports more paradigms, such as unicast and multicast. Jini uses both multicast and unicast to support the discovery protocols. IP addresses are defined using the classification and address ranges seen in Figure 4.2. Figure 4.2. Multicast addresses are defined in a specific IP address range. Multicast addresses are in the 224 to 239 range. Jini has reserved the following two multicast addresses for its use: • • 224.0.1.84—jini-announcement 224.0.1.85—jini-request You can see all the reserved multicast addresses by going to this Web site: http://www.iana.org/assignments/multicast-addresses So, how do lookup services bootstrap to the community and use these multicast addresses? Jini uses the jini-announcement address (224.0.1.84) and the multicast announcement protocol for lookup services to advertise their existence. When a new lookup service is started, it announces its availability to potential clients. Also, if a network failure occurs, this protocol can be used by the lookup service to make clients aware that it is available after the network has been restored. The multicast announcement protocol follows these steps: 1. Interested entities on the network listen for multicast announcements from lookup services. If an announcement of interest arrives at such an entity, the entity uses the unicast discovery protocol to contact the specific lookup service. 62 2. Lookup services take part in the unicast discovery protocol (see Figure 4.3) by sending multicast announcements of their existence at regular intervals. Figure 4.3. The multicast announcement protocol provides a "heartbeat" broadcast for lookup services to advertise their presence. So, the lookup service can broadcast its existence, like a heartbeat on the network, and interested clients are listening for heartbeats. It is assumed that lookup services are relatively stable in the community. In other words, lookup services should not be entering and leaving the community as frequently as other services or clients. The lookup service (hereafter referred to as LUS) is required and fundamental to the Jini community. So, how do other services that become active broadcast their existence? Jini uses the jini-request address(224.0.1.85) and the multicast request protocol to discover nearby lookup services. This is the protocol used by services that become active and need to locate Jini communities that are within the multicast radius. The multicast radius is roughly the number of hops beyond which neither requests nor announcements will traverse the network. Network configurations and hardware devices such as routers control this. Multicast request datagrams are encoded as a sequence of bytes, using the data and object serialization facilities of the Java programming language. A multicast discovery request packet must be 512 bytes in size or less in order to fit into a single UDP datagram. Table 4.1 illustrates the contents of a multicast request packet body. Table 4.1. Multicast Request Datagram Count (Occurrences) 1 1 1 variable 1 variable Serialized Type Description Protocol version Int The port to respond to Int The count of lookups Int net.jini.core.lookup.ServiceID An array of lookups that have already responded The count of groups Int java.lang.String An array of groups of interest 63 These multicast request datagrams are sent out on the network to find the lookup services that are within reach. In Figure 4.4, a service becomes active and broadcasts a request to find nearby LUSes of interest, for example, groups that it wants to join. Each LUS discovered will return a MarshalledObject that implements the net.jini.core.lookup.ServiceRegistrar interface. This will enable the service to register with the LUS, and in effect, join and advertise its functionality to the Jini community. Figure 4.4. When a service becomes active, it multicasts a request to find nearby LUSes. The multicast protocol is a key to enabling the dynamics of a Jini community. It permits the community to grow and form dynamically, based on network "closeness" versus traditional programming lookups or network IP configurations. The using entity does not have a priori knowledge of the request recipient. However, this protocol only works on networks that are considered close or nearby. Routers control the propagation of messages across the network. This is administered through a Time-ToLive (TTL) parameter. Network administrators use this value to control traffic patterns and to ensure that broadcast storms do not occur. In Figure 4.5, the Time-To-Live parameter is set to 4 and the multicast request needs to make 5 hops—pass through 5 routers—to make it to the nearest LUS. The LUS will never hear the request, and the service will never be able to register with that community. However, the unicast discovery protocol provides an alternative. Figure 4.5. The TTL parameter constrains the distance that packets can travel over the network. 64 The unicast discovery protocol is used to communicate with a specific service or LUS. This is useful for dealing with non-local communities and for using services in specific communities over a long period of time. This protocol is able to bypass the constraints of the multicast radius. The unicast discovery protocol works as follows: 1. The LUS listens for incoming connections and, when a connection is made by a client, responds with a MarshalledObject that implements the net.jini.core.lookup.Service Registrar interface. This is also what occurred as a result of the preceding multicast request. 2. An entity that wants to contact a particular LUS uses known host and port information to establish a connection as seen in Figure 4.6. The client-entity sends a unicast discovery request and listens for a MarshalledObject in response. Figure 4.6. A client uses unicast discovery to target a specific LUS. So, the LUS is crucial to the establishment of a Jini community. The discovery protocol provides both a dynamic multicast and a directed unicast approach to finding LUSes. Bootstrapping the Jini Network Let's return to the GUI-supplied service activation program to bootstrap your Jini network. The Jini reference implementation provides a LUS named reggie. reggie is an activatable RMI service. When reggie is started, it will register with rmid and multicast its announcement packets on the network. Be sure that rmid is running when you start reggie, or an activation exception will be thrown. To start reggie, select the Reggie tab and set the following properties, as seen in Figure 4.7: • • • • • Executable jar file—path to the Jini reggie.jar Codebase—path to the downloadable reggie-dl jar Security Policy File—path to policy.all file Log Directory—path to reggie persistent state Groups—groups this LUS will support 65 Figure 4.7. The Start Service GUI can be used to set the parameters and activate an HTTP server, rmid, and Jini services. Select the Run tab and start reggie. reggie will create a log file under the directory specified by the log directory property. You might want to verify the existence of that file after you have started the service. If the service fails to start, check the "Gotchas and Common Failures" section in Appendix A, "Setting Up Your Environment," to troubleshoot the problem. After you have started reggie, you should be able to start the LookupBrowser, which permits you to browse the services that are currently running in your Jini community. At this point, the only service running will be the LUS itself. To start the LookupBrowser supplied with Jini, select the LookupBrowser tab and set the following properties, illustrated in Figure 4.8: • • • • Executable jar file—path to the Jini examples jar file Codebase—path to the downloadable examples jar file Security Policy File—path to the browser policy file LookupBrowser—com.sun.jini.example.browser.Browser Figure 4.8. The LookupBrowser can be used to verify and inspect the Jini services running in your community. 66 You should see a screen that looks like Figure 4.9: Figure 4.9. The LookupBrowser should display the LUS launched. The browser verifies that the LUS is running. You can use the browser as an aid in determining what services are active, how many LUSes are running, attributes of a service, and so on. As we define services and work through the examples, you might want to start the browser and use it to look at the different service definitions. At this point, reggie should be sending announcement packets out on the network. By default, reggie sends an announcement packet every two minutes. Let's get under the covers of reggie to verify that multicast packets are being sent. Building the Multicast Sniffer 67 The multicast sniffer is designed to provide a view into the multicast network, specifically to monitor the behavior of Jini services. The sniffer defines and creates two threads—an announcement thread and a request thread. Listing 4.1 MulticastSniffer Class import java.net.*; import java.io.*; public class MulticastSniffer { public MulticastSniffer() { // the announce thread listens on the Jini reserved multicastannouncement address // and uses the default port 4160 AnnounceThread athread = new AnnounceThread("224.0.1.84", 4160); athread.start(); // the request thread listens on the Jini reserved multicast request address // and uses the default port 4160 RequestThread rthread = new RequestThread("224.0.1.85", 4160); rthread.start(); } public static void main(String args[]) { MulticastSniffer sniffer = new MulticastSniffer(); } class AnnounceThread extends Thread { // The announcement address InetAddress iaAnnounce = null; // buffer to hold announcement packet byte[] buffer = new byte[576]; // A DatagramPacket is used to receive the multicast announcement DatagramPacket dp = new DatagramPacket(buffer, buffer.length); // // // // address, and The the The The IncomingMulticastAnnouncement class is used to unmarshall packets sent by the LUS LUS sends the datagram packets to announce its existence announcement includes the LUS service ID, the LUS unicast the groups the // the LUS supports IncomingMulticastAnnouncement ima = null; // The socket used to join the multicast group and receive datagram packets MulticastSocket ms = null; public AnnounceThread(String ip, int port) { try { // create the socket and join the multicast group iaAnnounce = InetAddress.getByName(ip); ms = new MulticastSocket(port); ms.joinGroup(iaAnnounce); } catch (UnknownHostException e) 68 { e.printStackTrace(); } catch (IOException ie) { ie.printStackTrace(); } } public void run() { System.out.println("Starting multicast announce sniffer"); try { while(!isInterrupted()) { dp.setLength(buffer.length); try { ms.receive(dp); } catch (NullPointerException e) { break; } // Unmarshall the incoming announcement ima = new IncomingMulticastAnnouncement(dp); // display the contents to the console String[] groups = ima.getGroups(); LookupLocator locator = ima.getLocator(); ServiceID id = ima.getServiceID(); Date date = new Date(System.currentTimeMillis()); DateFormat df = DateFormat.getDateTimeInstance(); System.out.println(df.format(date)); System.out.println("Multicast Announcement from "); System.out.println("Host " + locator.getHost()); System.out.println("Port " + locator.getPort()); System.out.println("Groups: " ); for(int i=0; i<groups.length; i++) { System.out.println(groups[i]); } System.out.println("Service ID " + id); System.out.println("*************************************"); } } catch (SocketException se) { System.err.println(se); } catch (IOException ie) { System.err.println(ie); } } } class RequestThread extends Thread { InetAddress iaRequest = null; byte[] buffer = new byte[576]; DatagramPacket dp = new DatagramPacket(buffer, buffer.length); // The IncomingMulticastRequest class is used to unmarshall the packets // sent by the discovering // entities to request LUSes // The request includes the network address of the service, // the groups the service wants to join and // the service ID's of LUSes that have already responded IncomingMulticastRequest imr = null; MulticastSocket ms = null; public RequestThread(String ip, int port) { try { iaRequest = InetAddress.getByName(ip); ms = new MulticastSocket(port); ms.joinGroup(iaRequest); } catch (UnknownHostException e) { e.printStackTrace(); 69 } catch (IOException ie) { ie.printStackTrace(); } } public void run() { System.out.println("Starting multicast request sniffer"); try { while(!isInterrupted()) { dp.setLength(buffer.length); try { ms.receive(dp); } catch (NullPointerException e) { break; } // Unmarshall the incoming request imr = new IncomingMulticastRequest(dp); // display the contents to the console String[] groups = imr.getGroups(); ServiceID[] ids = imr.getServiceIDs(); Date date = new Date(System.currentTimeMillis()); DateFormat df = DateFormat.getDateTimeInstance(); System.out.println(df.format(date)); System.out.println("Multicast request from "); System.out.println("Host " + imr.getAddress()); System.out.println("Port " + imr.getPort()); System.out.println("Groups: " ); for(int i=0; i<groups.length; i++) { System.out.println(groups[i]); } System.out.println("Service IDs: " ); for(int i=0; i<ids.length; i++) { System.out.println(ids[i]); } System.out.println("*************************************"); } } catch (SocketException se) { System.err.println(se); } catch (IOException ie) { System.err.println(ie); } } } } To build this example, change to the directory where you copied the book examples. Locate the Chapter 4 Example 1 directory. The build script requires the location of the Jini distribution to be set in the environment variable JINI_HOME (see Listing 4.2). Once set, invoke the build.cmd. C:\JiniJavaSpaces\Chapter4\example1>build Listing 4.2 build.cmd Script for Chapter 4 Example 1 @echo off echo deleting *.class files del *.class echo echo echo echo echo ------------------------------------------Jini and JavaSpaces Application Development Chapter 4 - Example 1 compiling the MulticastSniffer ------------------------------------------- 70 if not "%JINI_HOME%" == "" goto compile echo You must set JINI_HOME to point to your Jini installation echo For example set JINI_HOME=c:\files\jini1_2 goto exit :compile javac -d . -classpath %JINI_HOME%\lib\jinicore.jar;%JINI_HOME%\lib\jini-ext.jar;.; MulticastSniffer.java :exit To test the sniffer, enter the following command: C:\JiniJavaSpaces\Chapter4\example1>startSniffer Listing 4.3 startSniffer.cmd Script for Chapter 4 Example 1 @echo off echo echo echo echo echo ------------------------------------------Jini and JavaSpaces Application Development Chapter 4 - Example 1 The MulticastSniffer ------------------------------------------- if not "%JINI_HOME%" == "" goto run echo You must set JINI_HOME to point to your Jini installation echo For example set JINI_HOME=c:\files\jini1_2 goto exit :run java -cp %JINI_HOME%\lib\jini-ext.jar;.; MulticastSniffer :exit You should see an announcement packet sent out every two minutes. You'll display the service ID that was assigned to the LUS. The service ID is a globally unique identifier (GUID). Every service in the Jini community, including the LUS, is assigned a unique ID. The unique ID is assigned by the LUS to all services, and in this case is self-assigned. Figure 4.10. The MulticastSniffer can be used to display announcement and request packets being sent by your Jini community. 71 At this point you only see announcement packets to indicate that the Jini network—the LUS—is available to service requests from client and service entities. Join and Lookup Now that you have the LUS in place and have been able to verify the heartbeat of reggie, let's discuss the join and lookup process. This is the process that enables you to provide and access services in the Jini community. As mentioned, the Join protocol makes use of the discovery protocols to provide a standard sequence of steps that services should perform when they are starting up and registering themselves with a LUS. The discovery protocol is used to discover LUSes, and for LUSes to return a proxy object that implements the ServiceRegistrar interface. Recall when you built the RMIShell, you had to specify where the RMI registry was running. If it was not specified, it would default to the local host. In contrast, the discovery process permits us to dynamically discover where the LUS is running and then allows us to register with it. Service Registration After the LUS has been discovered and returns the ServiceRegistrar proxy, you invoke the register method to register your service. You will do this with all LUSes that are of interest to you—that is, LUSes that support specific groups. ServiceRegistration register(ServiceItem item, long leaseDuration) throws RemoteException; The register method takes two parameters: • • A ServiceItem A lease duration The ServiceItem includes a service ID that must be globally unique and is initially generated by the LUS as explained previously. In addition, a ServiceItem contains a set of attributes that are used to augment the service definition and a proxy object used to communicate with the service. 72 The lease parameter is used to indicate a time-based duration to keep the service active. We will discuss leasing in general later in this chapter. For now, just realize that every service has a lease associated with it that controls how long the service will remain active in the LUS. The lease duration is expressed in milliseconds. Listing 4.4 ServiceRegistrar Interface public interface ServiceRegistrar { ServiceRegistration register(ServiceItem item, long leaseDuration) throws RemoteException; ServiceMatches lookup(ServiceTemplate tmpl, int maxMatches) throws RemoteException; int TRANSITION_MATCH_NOMATCH = 1 << 0; int TRANSITION_NOMATCH_MATCH = 1 << 1; int TRANSITION_MATCH_MATCH = 1 << 2; EventRegistration notify(ServiceTemplate tmpl, int transitions, RemoteEventListener listener, MarshalledObject handback, long leaseDuration) throws RemoteException; Class[] getEntryClasses(ServiceTemplate tmpl) throws RemoteException; Object[] getFieldValues(ServiceTemplate tmpl, int setIndex, String field) throws NoSuchFieldException, RemoteException; Class[] getServiceTypes(ServiceTemplate tmpl, String prefix) throws RemoteException; ServiceID getServiceID(); LookupLocator getLocator() throws RemoteException; String[] getGroups() throws RemoteException; } The register method of the ServiceRegistrar interface returns an object that implements the ServiceRegistration interface. This interface enables the service to retrieve its service ID and lookup registration and to manage its service attributes. You will need the service ID to register with other LUSes to ensure your identification is consistent and unique across the network. Each implementation of the LUS exports proxy objects that implement the ServiceRegistration interface. Listing 4.5 ServiceRegistration Interface public interface ServiceRegistration { ServiceID getServiceID(); Lease getLease(); void addAttributes(Entry[] attrSets) throws UnknownLeaseException, RemoteException; void modifyAttributes(Entry[] attrSetTemplates, Entry[] attrSets) throws UnknownLeaseException, RemoteException; void setAttributes(Entry[] attrSets) 73 throws UnknownLeaseException, RemoteException; } After a service has registered with the LUS, it has joined the community. Clients can now find the service by performing a lookup on any LUS that the service has registered with. The JoinManager Services will often use the net.jini.lookup.JoinManager class to discover and join a Jini community. This helper class implements the discovery and join protocols, making it easier for services to manage the "good Jini citizen" process requirements. There is more said about being a good Jini citizen later in this chapter. There are five parameters required to construct a JoinManager: • • • • • java.lang.Object is the object/service to be registered. This is the proxy object that implements the service interface. For example, the Jini LUS registers an object that implements the ServiceRegistrar interface. net.jini.core.entry.Entry[] is an array of attributes associated with the service. Entry objects are used to augment the service definition so users can perform specific searches for services. For example, an Entry might be used to specify a location of a service, or a specific vendor's implementation. In that case, only services matching that criteria would be returned from lookup requests. net.jini.lookup.ServiceIDListener or net.jini.core.lookup.ServiceID is the unique service identifier assigned to the service. If the service has already received the identifier from the LUS, then it uses the ID as a parameter; otherwise, it supplies an ID listener to receive the ID. The ID listener stores the ID for subsequent registration(s). Where and how the ID is stored, is service-dependent. Typically, the service will log this information to safe-storage upon receipt and retrieve the ID during subsequent starts from a known location. net.jini.discovery.DiscoveryManagement is an interface that defines the discovery operations as outlined in the previous section. The LookupDiscovery class implements this interface. In addition, the LookupLocatorDiscovery class implements this interface using unicast, rather than the multicast protocol. net.jini.lease.LeaseRenewalManager is the lease renewal manager responsible for renewing your lease with the LUS. This is how resources are managed across the Jini network, based on time allocation management. Leasing is often referred to as providing the "self-healing" properties of a Jini network. The JiniShell Example Let's convert the RMIShell service developed in the last chapter into a Jini Service to demonstrate the changes required. This will serve to reinforce our discussion and highlight the extensions Jini provides on top of RMI. Extend the UnicastRemoteObject to create a non-activatable Jini service: Listing 4.6 JiniShell Class // These classes form the basis of the RMI support import java.rmi.RemoteException; import java.rmi.RMISecurityManager; import java.rmi.server.UnicastRemoteObject; import java.io.IOException; 74 // We will require a number of Jini classes to support our Jini service import net.jini.core.discovery.LookupLocator; import net.jini.core.lookup.ServiceID; import net.jini.discovery.DiscoveryGroupManagement; import net.jini.discovery.LookupDiscoveryManager; import net.jini.lookup.JoinManager; import net.jini.lookup.ServiceIDListener; public class JiniShell extends UnicastRemoteObject implements Shell { // The LookupDiscoveryManager you are using to find (request) LUSes private LookupDiscoveryManager lookupDiscMgr; // The lookup locator array that contains specific LUSs private LookupLocator locators[]; // The groups array that contains specific groups, no groups, or all groups private String groups[] = DiscoveryGroupManagement.ALL_GROUPS; // The manager for joining LUSs private JoinManager joiner = null; // ServiceID returned from the lookup registration process private ServiceID serviceID = null; Add an init method to your constructor to do initialization specific to Jini. public JiniShell() throws IOException { // call the UnicastRemoteObject to export the object super(); // Initialize the Jini service init(); } Create a discovery manager by passing parameters to discover all LUSes within the multicast radius. Do this by specifying ALL_GROUPS and null parameters: private void init() throws IOException { try { lookupDiscMgr = new LookupDiscoveryManager(groups, locators, null); } catch (IOException e) { System.err.println("Problem starting discovery"); e.printStackTrace(); throw new IOException("Problem starting discovery:" + e.getLocalizedMessage()); } /* Register this service with if (serviceID == null) { joiner = new JoinManager( this, null, new SrvcIDListener(), lookupDiscMgr, null); } else { joiner = new JoinManager( this, any configured LUSes */ // First instance ... need service id // service object // service attributes – none at this time // ServiceIDListener - internal helper class // DiscoveryManagement - default // LeaseRenewalManager - default // Rejoin with (recovered) state information // service object 75 null, serviceID, lookupDiscMgr, null); // // // // service attributes – none at this time Service ID – already have an ID DiscoveryManagement - default LeaseRenewalManager - default } } // implementation of the Shell interface public Object executeCommand(Command cmd) { return cmd.execute(); } The main method does not differ significantly from prior examples; however, you no longer need to bind to the RMI registry because you now will use the LUS (ServiceRegistrar) to return a reference to the remote object: public static void main(String[] args) { // set the security manager if (System.getSecurityManager() == null) { System.setSecurityManager(new RMISecurityManager()); } try { Shell shell = new JiniShell(); System.out.println("JiniShell bound"); Thread.currentThread().join(); } catch (Exception e) { System.err.println("JiniShell exception: " + e.printStackTrace(); } e.getMessage()); } // Utility method for setting the service's ID obtained from // the lookup registration process. private void setServiceID(ServiceID id) { serviceID = id; } The SrvcIDListener class handles the callback of the service ID assignment from the JoinManager. You will continue to evolve this process to persist the service ID to a log file: private class SrvcIDListener implements ServiceIDListener { public SrvcIDListener() { super(); } /** * The JoinManager will invoke this method when it receives a * valid ServiceID from a LUS. */ public void serviceIDNotify(ServiceID id) { // Set the ID setServiceID(id); System.out.println("Received service id"); // Log this event } } } 76 There are a number of important aspects of being a Jini service that have been omitted for the moment. The preceding service only demonstrates the core requirements to registration. For example, you did not persist your service ID, so restarts of the service would pass a listener, and thus the LUS would generate a new unique ID as opposed to reusing the existing ID. Any client who had saved the original ID would not be able to find the service again. The shortcomings of this example will be corrected as additional Jini concepts are introduced to the service framework. Building a Simple Client Let's now build a simple client to test the service. Listing 4.7 JiniCommandLine Class import java.rmi.*; import java.io.IOException; import java.util.Vector; // Jini classes used to find and bind to a Jini service import net.jini.core.lookup.ServiceItem; import net.jini.core.lookup.ServiceTemplate; import net.jini.discovery.DiscoveryEvent; import net.jini.discovery.DiscoveryListener; import net.jini.discovery.DiscoveryManagement; import net.jini.discovery.LookupDiscovery; import net.jini.core.lookup.ServiceMatches; import net.jini.core.lookup.ServiceRegistrar; import org.jworkplace.command.*; public class JiniCommandLine { public JiniCommandLine() { } // method to find a service given a service template private Object lookup(ServiceTemplate template) throws IOException { // Your internal class ServiceListener does the actual work ServiceListener ss = new ServiceListener(template); return ss.lookup(); } public static void main(String args[]) { // Set a default directory to search String directory = "."; if(args.length == 1) { directory = args[0]; } // set security manager if not set if (System.getSecurityManager() == null) { System.setSecurityManager(new RMISecurityManager()); } try { JiniCommandLine cmdLine = new JiniCommandLine(); // create a service template to represent the required service ServiceTemplate template; // We pass a class that identifies an interface rather than a name Class[] types = { Class.forName("Shell") } ; // We do not constrain our service search other than interface type // null indicates match anything on service ID and attributes 77 template = new ServiceTemplate(null, types, null); } // call the lookup passing our template Shell shell = (Shell)cmdLine.lookup(template); FileList command = new FileList(directory); String[] files = (String[]) (shell.executeCommand(command)); if(files == null) { System.out.println("Invalid directory"); } else { for(int i=0; i<files.length; i++) System.out.println(files[i]); } catch (Exception e) { System.err.println("FileList exception: " + e.getMessage()); e.printStackTrace(); } } You create a ServiceTemplate to indicate the service you want your discovery process to find. A ServiceTemplate is used to match service items that are registered in the LUS. The template contains the service ID if known, an array of java.lang.Class objects of supported interfaces, and the attributes to limit the returned services. Null values are supplied for the service ID and attribute parameters, which instructs the LUS to match anything. You ask for any object that implements the Shell interface by setting the java.lang.Class array parameter to your Shell class name. Your ServiceListener implements the DiscoveryListener interface. The internal class ServiceListener is a helper class for your client. It is passed the template that your client wants to find. The DiscoveryListener interface defines two methods, discovered and discarded, as defined in Listing 4.8. Listing 4.8 DiscoveryListener Interface public interface DiscoveryListener extends java.util.EventListener // Called when one or more LUS registrars has been discarded. void discarded(DiscoveryEvent e); // Called when one or more LUS registrars has been // discovered. void discovered(DiscoveryEvent e); } Inner class of the Jini command line: private class ServiceListener implements DiscoveryListener { // Our discovery management support private DiscoveryManagement mgt; // A vector to hold all services discovered private Vector services = new Vector(); // A template used to indicate the service requested by the client private ServiceTemplate template; public ServiceListener(ServiceTemplate template) throws IOException { super(); 78 this.template = template; // sequence to ensure all lookups are heard mgt = new LookupDiscovery(LookupDiscovery.NO_GROUPS); // we implement the listener interface mgt.addDiscoveryListener(this); // now set all groups ((LookupDiscovery)mgt).setGroups(LookupDiscovery.ALL_GROUPS); } // client will wait until matching service is discovered public synchronized Object lookup() { while(services.size() == 0) { try { wait(); } catch (InterruptedException ex) { } } // just return the first match found return ((ServiceItem)services.elementAt(0)).service; } The discovered method receives a DiscoveryEvent, which is passed by the LookupDiscovery object when it discovers one or more LUSes. The getRegistrars method of the DiscoveryEvent returns an array containing the ServiceRegistrar for each newly discovered LUS. Display the discovered LUSes to the console to demonstrate the type of information available: public synchronized void discovered(DiscoveryEvent de) { System.out.println("Discovered LUS: "); ServiceRegistrar[] registrars = de.getRegistrars(); for (int i=0; i < registrars.length; i++) { try { System.out.println("URL: " + registrars[i].getLocator(). toString()); System.out.println("ID: " + registrars[i].getServiceID()); String groups[] = registrars[i].getGroups(); // simply display the first group returned System.out.println("GROUPS: " + groups[0]); The lookup method of the ServiceRegistrar is used to return an array of ServiceMatches objects. The service template containing the service Shell interface is used as a parameter to request that only services implementing the Shell interface be returned. The ServiceMatches object returned by the LUS will contain an array of ServiceItem objects and a count of the number returned. The ServiceItem is added to Vector of services, which in this simple example is used to trigger the notification to the suspended client that an object implementing the Shell interface has been found: ServiceMatches sm = registrars[i].lookup(template, Integer. MAX_VALUE); System.out.println("Matching services found ------: " + sm. totalMatches); System.out.println(""); for(int j=0; j < sm.items.length; j++) { // Process each ServiceItem 79 if(sm.items[j].service != null) { services.addElement(sm.items[j]); } else { System.out.println("Service item null" + sm.items[j]. service); } } } // notify the client a matching service has been found System.out.println("Notifying..."); notifyAll(); catch(Exception e) { e.printStackTrace(); } } } // we ignore discarded LUS objects in this example public void discarded(DiscoveryEvent de) { } } } The following code fragment in main is executed upon notification; it is the familiar FileList command. The results are displayed to the console: Shell shell = (Shell)cmdLine.lookup(template); FileList command = new FileList(directory); String[] files = (String[]) (shell.executeCommand(command)); Running the Example To begin, ensure that you have HTTP servers running on the client and server. In addition, rmid and reggie should be running on the server. Now it is time to build and test your JiniShell Service. The compilation and stub generation process should be familiar to you now. You just need to ensure you add the necessary Jini jar files to the process. For compilation, this usually amounts to adding the jini-core.jar file and the jini- ext.jar file to the classpath. Compile the Files Change to the directory where you copied the examples for this book. For example, in C:>cd c:\JiniJavaSpaces\Chapter4\example2, you should see the following files: JiniCommandLine.java FileList.java Command.java Shell.java JiniShell.java 80 Invoke the java compiler using the build script provided: C:\JiniJavaSpaces\Chapter4\example2>build Listing 4.9 build.cmd Script File @echo off echo deleting *.class files del *.class echo echo echo echo echo ------------------------------------------Jini and JavaSpaces Application Development Chapter 4 - Example 2 compiling ------------------------------------------- if not "%JINI_HOME%" == "" goto staticClasspath echo You must set JINI_HOME to point to your Jini installation echo For example set JINI_HOME=c:\files\jini1_2 goto exit echo compiling files :staticClasspath echo Setting your CLASSPATH statically. set CP= if exist "%JINI_HOME%\lib\jini-core.jar" set CP=%CP%;%JINI_HOME%\lib\jini-core.jar if exist "%JINI_HOME%\lib\jini-ext.jar" set CP=%CP%;%JINI_HOME%\lib\jini-ext.jar if exist "%JINI_HOME%\lib\sun-util.jar" set CP=%CP%;%JINI_HOME%\lib\sun-util.jar echo Using CLASSPATH: %CP% javac -d . -classpath %CP%;.; Command.java javac -d . -classpath %CP%;.; JiniCommandLine.java javac -d . -classpath %CP%;.; FileList.java javac -d . -classpath %CP%;.; Shell.java javac -d . -classpath %CP%;.; JiniShell.java echo generating the JiniShell_Stub class rmic -v1.2 -classpath .\ -d . JiniShell echo Creating the service download jar file jar -cf service-dl.jar JiniShell_Stub.class Command.class Shell.class echo Creating the service jar file jar -cmf service.txt service\service.jar JiniShell.class JiniShell$SrvcIDListener .class JiniShell_Stub.class Command.class Shell.class echo Creating the client download jar file jar -cf client-dl.jar Command.class Shell.class FileList.class echo Creating the client jar file jar -cmf client.txt client\client.jar JiniCommandLine.class 81 JiniCommandLine$ServiceListener.class Shell.class FileList.class Command.class :exit Create the Required Stub Interfaces This will compile the files and invoke the rmic compiler to generate the stub file for the JiniShell class. Create Jar Files (Client-Side and Server-Side) It also creates the client-side and server-side jar files. Copy the client-dl.jar file to the client HTTP directory. Copy the service-dl.jar file to the server HTTP directory. Define the Security Policy Files for Client and Server Create the policy.all file for both the server and the client or use the one provided with the Jini distribution: grant { permission java.security.AllPermission "", ""; } ; Start the JiniShell Server Go to the service directory. Start the JiniShell command processor by invoking the service script. C:\JiniJavaSpaces\Chapter4\example2\service>startService Listing 4.10 startService.cmd Script File @echo off if not "%HTTP_ADDRESS%" == "" goto gotServerHTTP echo You must set HTTP_ADDRESS to point at your service HTTP server echo For example set HTTP_ADDRESS=[hostname][:port] goto exit :gotServerHTTP echo Setting your CLASSPATH statically. set CP= if exist "%JINI_HOME%\lib\jini-core.jar" set CP=%CP%;%JINI_HOME%\lib\jini-core.jar if exist "%JINI_HOME%\lib\jini-ext.jar" set CP=%CP%;%JINI_HOME%\lib\jini-ext.jar echo Using CLASSPATH: %CP% java -Djava.security.policy=%JINI_HOME%\policy\policy.all 82 -Djava.rmi.server.codebase=http://%HTTP_ADDRESS%/service-dl.jar -cp %CP%;..\; JiniShell :exit Test Client To test the client, move to the directory and machine where you deployed the client software. Run the following client command: C:\JiniJavaSpaces\Chapter4\example2\client>startClient Listing 4.11 startClient.cmd Script File @echo off if not "%HTTP_ADDRESS%" == "" goto gotClientHTTP echo You must set HTTP_ADDRESS to point at your service HTTP server echo For example set HTTP_ADDRESS=[hostname][:port] goto exit :gotClientHTTP java -jar -Djava.security.policy=%JINI_HOME%\policy\policy.all -Djava.rmi.server.codebase=http://%HTTP_ADDRESS%/client-dl.jar client.jar %1 :exit Where %1 is the remote directory to search and display files. Again, you should see the contents of the remote directory displayed on the console. Your Shell interface is now a Jini service, and is available on the Jini network. Any client within the multicast radius can discover the service using multicast discovery. In addition, with a minor change, such as using a specific LookupLocator address, a remote client over a wide area network or the Internet can also interface with the command service. You can change the discovery process to use unicast discovery by changing from LookupDiscovery to LookupLocatorDiscovery. Instead of specifying groups, specify an array of URLs of the form jini://hostname/port/ where: • • • The protocol is jini; The hostname defines the name of the host on which to find the LUS; The network port is optional and defaults to 4160. The LookupLocatorDiscovery class is used to participate in unicast discovery. The class takes an array of locators to find specific LUSes. Add a LookupLocator array to your class definition: // The lookup locator array that contains specific LUSs 83 private LookupLocator locators[]; public ServiceListener(ServiceTemplate template) throws IOException { super(); this.template = template; // change to LookupLocatorDiscovery mgt = new LookupLocatorDiscovery(locators); // you still implement the listener interface mgt.addDiscoveryListener(this); // now set specific locators locators[0] = new LookupLocator("jini://hostname:port/"); ((LookupDiscovery)mgt).setLocators(locators); } So, you have just demonstrated three different approaches to the discovery process: • • • The server used the LookupDiscoveryManager class, which combines unicast and multicast discovery The first client example used multicast by using the LookupDiscovery class The modifications outlined use unicast discovery by creating an instance of LookupLocatorDiscovery The discovery technique you use will depend on the security and visibility requirements of your service. Security is discussed in depth in Chapter 9"Security in Jini.", Service Attributes The Jini LUS provides a framework that supports service discovery by interface type. In addition, LUSes support service attributes. Service attributes are used to further define the service and permit using entities to refine their discovery parameters. In the JiniShell example, you used the JoinManager class to help with the discovery and join process. JoinManager joiner = new JoinManager( this, // service object null, // service attributes – none at this time new SrvcIDListener(), // ServiceIDListener - internal helper class lookupDiscMgr, // DiscoveryManagement - default null); // LeaseRenewalManager - default The second parameter to creating a JoinManager is an array of net.jini.core.entry.Entry attributes. This class is the supertype of all entries that can be stored in a Jini LUS. It is an interface that serves as a marker interface—there are no methods defined. An object that implements this interface must adhere to the Entry specification. It must have a default, no argument constructor, and should contain only public instance variables. All instance variables must be serializable. Each field is serialized separately, so references between two fields of an entry will not be reconstituted to be shared references, but instead to separate copies of the original object. 84 AbstractEntry is a basic implementation of the Entry interface. This class is often used as the base class for defining service attributes, because it adheres to the Entry specification. The Pre-Defined Attribute Definitions Jini supplies the following pre-defined attributes: • • Address— An Entry object used to represent an address. This address is used to represent a mailing address; for example, it contains a postalCode defined as a String. Comment— An Entry object that contains a String to attach a comment to a service. • Location— An Entry object that refines the address attribute to contain the floor or • Name— An Entry object that represents a name for the service. This is often used to room in a building. • • • display a readable value for the service. ServiceInfo— An Entry object that contains implementation details, such as vendor, manufacturer, version, and model of the service. ServiceType— An Entry object that provides service information to a human user, such as service name, an icon representation, and a description. Status— An Entry object used to represent the status of a service. In addition the net.jini.lookup.entry package contains a matching bean definition for each entry type. For example, Address has an associated AddressBean. The bean class has the necessary get and set methods to manipulate the content of the entry definition. EntryBeans is an Entry object that defines a utility class for handling JavaBeans components that relate to Entry classes in the LUS. Developing a Taxonomy of Attributes You can use Entry objects to augment service definitions. It is possible to use these object definitions to create ordered groups or categories. For example, you can use Address objects to classify according to geographic relationships, or Location objects to partition a building or campus. It is also possible to create new Entry object definitions that represent information taxonomies or hierarchical categories similar to those found on Web portals. For example, a health portal can offer a Jini hospital referral service that recommends hospitals based on an attribute such as "cardiac care" or "respiratory disease" treatment. Searching, Matching, and Filtering Services Based on Attributes You can easily create Entry object definitions. For example, to augment the JiniShell service, you can define the following Entry objects: private Entry[] entries = { new Name("JiniShell"), // providen lookup by name capabilities new ServiceInfo("JWorkPlace", //provide specific service information "Robert Flenner", "Sams", "1.0","", "") }; 85 The new JoinManager construction would simply change to: JoinManager joiner = new JoinManager( this, // service object entries, // service attributes new SrvcIDListener(), // ServiceIDListener - internal helper class lookupDiscMgr, // DiscoveryManagement - default null); // LeaseRenewalManager - default To find a specific service, you can change your template definition: // create a service template to represent the required service ServiceTemplate template; // pass a class that identifies an interface rather than a name Class[] types = { Class.forName("org.jworkplace.command.Shell") } ; // Create the entries to match on private Entry[] entries = { new Name("JiniShell"), // provide lookup by name capabilities new ServiceInfo("JWorkPlace", // provide specific service information "Robert Flenner", "Sams", "1.0","", "") } ; // Construct the template using the Entry objects template = new ServiceTemplate(null, types, entries); Then, when you pass the template to the LUS, only those services matching the template will be returned: ServiceMatches sm = registrars[i].lookup(template, Integer.MAX_VALUE); Entry objects provide a powerful mechanism to service discovery. You will continue to work through service attribute definitions as you develop Jini applications. Distributed Events Jini supports a distributed event model to enable loosely-coupled notifications from remote sources. The net.jini.core.event.RemoteEvent class is the superclass of all remote events in the Jini framework. The state contained in a RemoteEvent object includes: • • • • A reference to the object in which the event occurred An ID that indicates the kind of event relative to the object in which the event occurred A long, which indicates the sequence number of this instance of the event kind A MarshalledObject that is handed back to the client when the notification occurs The combination of the event identifier and the object reference obtained from the RemoteEvent object should uniquely identify the event type. 86 The sequence number obtained from the RemoteEvent object is an increasing value that can act as a hint to the number of occurrences of this event relative to some earlier sequence number. Any object that generates a RemoteEvent must ensure a unique sequence number is assigned to each event it generates. Notifications The ServiceRegistrar interface implemented by the LUS contains a notify method: EventRegistration notify(ServiceTemplate tmpl, int transitions, RemoteEventListener listener, MarshalledObject handback, long leaseDuration) throws RemoteException; Applications can use this notify method by passing a RemoteEventListener object to request notification when registered services change. ServiceEvent objects are returned to the listener when services in the Jini network change. The applications specify the events of interest by specifying a service template and a transition parameter. • • • TRANSITION_MATCH_NOMATCH— A service item that previously matched the service template no longer matches the template. TRANSITION_NOMATCH_MATCH— A service item was added that matches the template or was changed to match the template. TRANSITION_MATCH_MATCH— A service item changed but still matches the template. A ServiceEvent extends the RemoteEvent object and includes the service ID, the service item, and the type of transition that took place. This class is used for remote events sent by the LUS. Sequence numbers for a given event ID are strictly increasing. If there is no gap between two sequence numbers, no events have been missed; if there is a gap, events might (but might not) have been missed. For example, a gap might occur if the LUS crashes, even if no events are lost because of the crash. The net.jini.core.event.RemoteEventListener is an interface that defines the notify method. The RemoteEventListener interface is implemented by any object that wants to receive a notification of a remote event from some other object. public void notify(RemoteEvent theEvent) throws UnknownEventException, java.rmi.RemoteException The call to notify is synchronous to enable the party making the call to know whether the call succeeded. However, the recipient of the call should return from the notify method as quickly as possible. UnknownEventException is thrown when the recipient does not recognize the combination of the event identifier and the event source as something in which it is interested. Throwing this exception has the effect of asking the sender not to send further notifications of this kind of event from this source in the future. A MarshalledObject can be supplied to an event registration and will be returned to the RemoteEventListener during notification. The listener can retrieve the MarshalledObject by invoking the getRegistrationObject method of the RemoteEvent. 87 The Self-Healing Network If you've integrated systems across a network, you know that the resiliency of the network and the capability to cope with network failures separates good applications from primitive applications. Jini assumes at its foundation that the network might be unstable. Jini introduces the idea of a lease to service management. If you are a user of a service, you never own the service, you simply lease it—nothing is permanent. All resources must have some management process that periodically checks to see whether everything is OK, and to determine whether the resource is still required by the system or user. Imagine that I am a service provider publishing services offered to B2B exchanges. My system crashes or I have system problems. How does the exchange maintain my status in a timely fashion? There's not likely to be a consistent approach across exchanges. However, leasing can provide consistency and time-based resource management flexibility. Leasing is an important part of Jini infrastructure robustness. Leasing permits the LUS to delete services that have failed to renew their lease. If you provide or are using a service, you must periodically renew your lease. The renewal period depends on the application. You request a finite renewal period on your lease, and the service provider determines the actual lease commitment. The service provider is in the best position to determine resource availability. The provider, if designed correctly, can become more intelligent about resource commitments than most software is today. It's analogous to apartment leasing. I can ask the apartment complex for a 6-month lease. They inform me they can only commit to a 1-month lease period. After one month, I must renew my lease, or I am evicted! When you use the Web, you often find invalid URL references or slow sites. Web services are being built on this same infrastructure and architecture. System and network instability will be an issue. Web services will require the self-healing properties of a Jini network. Leasing When a service registers with the LUS, it receives a lease from the LUS. More specifically, the ServiceRegistrar interface returns a ServiceRegistration object when the register method is invoked. The ServiceRegistration contains a getLease method that returns a Lease object. The Lease interface defines a type of object that is returned to the leaseholder and issued by the lease grantor. Particular instances of the Lease type will be created by the grantors of a lease and returned to the holder of the lease as part of the return value from a call that allocates a leased resource, such as the register method. The call that requests a leased resource will typically include a requested duration for the lease. If the request is for a particular duration, the lease grantor is required to grant a lease of no more than the requested period of time. A lease can also be granted for a period of time shorter than that requested. It is the responsibility of the service to ensure that the lease does not expire. In order for a service to maintain its residency in the LUSes it has joined, the service must provide for the coordination, systematic renewal, and overall management of all leases on that residency. If the service fails to renew the lease when it expires, the LUS will remove the service from its list of available services. The LUS assumes the service is having a problem that keeps it from performing satisfactorily. This is why the Jini network is often referred to as "self-healing." It proactively grooms the service environment without the need for human intervention. 88 The net.jini.lease package introduced with Jini 1.1 contains the classes related to lease management. The LeaseRenewalManager is a utility class that can be used to ease the development required to manage leases. The class creates a thread that sleeps until it is time to renew the leases under its management. The LeaseRenewalManager is active as long as the virtual machine that activated the manager is active. The LeaseRenewalService is a Jini service that enables leases to be managed in a separate virtual machine process. We will discuss the LeaseRenewalService in greater detail when we cover helper services in Chapter 7, "The Helper Services." Recall that we have already used a LeaseRenewalManager (LRM), although we did not explicitly provide one when you instantiated a JoinManager. JoinManager joiner = new JoinManager( this, // service object entries, // service attributes new SrvcIDListener(), // ServiceIDListener - internal helper class lookupDiscMgr, // DiscoveryManagement - default null); // LeaseRenewalManager - default The last parameter to the JoinManager takes a renewal manager. If one is not supplied, the JoinManager instantiates one on your behalf. You can gain access to the LRM through the JoinManager's getLeaseRenewalManager method. public LeaseRenewalManager getLeaseRenewalManager() Clients of the LRM simply give their leases to the manager, and the manager renews each lease as necessary. Failures encountered while renewing a lease can optionally be reflected to the client via LeaseRenewalEvents by providing a LeaseListener. Entities can also explicitly create an instance of this class in their own virtual machine to locally manage the leases granted to them. public class LeaseRenewalManager LeaseRenewalManager() LeaseRenewalManager(Lease lease, long desiredExpiration, LeaseListener listener) public void cancel(Lease lease) public void clear() public long getExpiration(Lease lease) public void remove(Lease lease) public void renewFor(Lease lease, long desiredDuration, LeaseListener listener) public void renewFor(Lease lease, long desiredDuration, long renewDuration, LeaseListener listener) public void renewUntil(Lease lease, long desiredExpiration, LeaseListener listener) public void renewUntil(Lease lease, long desiredExpiration, long renewDuration, LeaseListener listener) public void setExpiration(Lease lease, long expiration) } 89 Time-Based Resource Allocation The LeaseRenewalManager distinguishes between two time values associated with lease expiration: the desired expiration time for the lease, and the actual expiration time granted when the lease is created or was last renewed. The desired expiration represents when the client would like the lease to expire. The actual expiration represents when the lease is going to expire if it is not renewed. Both time values are absolute times, not relative time periods. Each lease in the managed set also has two other associated attributes: a desired renewal duration, and a remaining desired duration. The desired renewal duration is specified (directly or indirectly) when the lease is added to the set. This duration must normally be a positive number; however, it might be Lease.ANY if the lease's desired expiration is Lease.FOREVER. The remaining desired duration is always the desired expiration minus the current time. The methods of this class are appropriately synchronized for concurrent operation. Additionally, this class makes certain guarantees with respect to concurrency. When this class makes a remote call (for example, when requesting the renewal of a lease), any invocations made on the methods of this class will not be blocked. Similarly, this class makes a re-entrance guarantee with respect to the listener objects registered with this class. Should this class invoke a method on a registered listener (a local call), calls from that method to any other method of this class are guaranteed not to result in a deadlock condition. Summary The most important concept within the Jini architecture is that of a service. Services are found and resolved by a lookup service (LUS). The LUS is the central bootstrapping mechanism for the system and provides the major point of contact between the system and users of the system. The service discovery protocols require support for multicast or restricted-scope broadcast, along with support for reliable unicast delivery in the transport layer. The discovery protocols make use of the Java platform's object serialization capabilities to exchange information in a platformindependent manner. The join and lookup process enables us to provide and access services in the Jini community. The Join protocol makes use of the discovery protocols to provide a standard sequence of steps that services should perform when they are starting up and registering themselves with a LUS. The Jini LUS provides a framework that supports service discovery by interface type. In addition, LUSes support service attributes. Service attributes are used to further define the service and allow using entities to refine their discovery parameters. Jini supports a distributed event model to enable loosely coupled notifications from remote sources. The distributed event model is simple to implement with a single notify method defined. Event frameworks can be quickly assembled to provide support for remote notification, filtering, and message delivery. Leasing is an important part of Jini infrastructure robustness. Services must maintain their leases in all registered LUSes. The LUS will remove any service that does not renew its lease in a timely manner. This is why the Jini network is often referred to as "self-healing." The LUS proactively grooms the service environment without the need for human intervention. 90 Part II: Designing Applications Using the Jini Framework IN THIS PART 5 JavaSpaces Service 6 Transaction Service 7 The Helper Services 8 Service Administration 9 Security in Jini 91 Chapter 5. JavaSpaces Service IN THIS CHAPTER • • • • • • • • • The JavaSpaces Service The Architecture of JavaSpaces Entries Revisited The Simple API Distributed Data Structures Application Basics Deploying JavaSpaces A Sample Application—Building the MessagePad Summary This chapter provides a brief overview of the JavaSpaces service. It expands the notion of entries defining service attributes and entries defining models of communication and control in JavaSpaces, such as messaging systems and workflow control systems. The JavaSpaces Service JavaSpaces is a Jini service that supports distributed persistence and the design of distributed algorithms. JavaSpaces is closely tied to the Jini architecture; in fact, it is one of the few services that comes bundled with Jini. JavaSpaces was heavily influenced by the concept of the tuple space, first described in 1982 in a programming language called Linda that was developed at Yale University. Linda was designed as a coordination language for ensemble (distributed and parallel) computing. Like Linda, JavaSpaces is designed to significantly ease the development of distributed and parallel processing systems. The basic idea of ensemble programming is that you can have many active programs distributed over physically dispersed machines, unaware of each other's existence, that are still able to communicate. They communicate to each other by releasing data (a tuple) into tuple space. Programs read, write, and take tuples that are of interest to them from the tuple space. JavaSpaces, however, extends this model of communication by defining the data as objects. So, rather than simply passing data between processes, you are able to pass objects, and thus behavior. JavaSpaces uses RMI and object serialization from the Java programming language to provide these features. In addition, JavaSpaces leverages the Jini infrastructure that includes distributed events, leasing, and lightweight transactions. The Architecture of JavaSpaces A JavaSpaces server is used to mediate the communications between networked systems. It provides much of the common functionality required for distributed systems, which simplifies the development task, particularly when you can model your task as a flow of objects between systems distributed over a network as seen in Figure 5.1. 92 Figure 5.1. The JavaSpace API provides a simple programming interface for distributed processes. It allows processes to read and write objects and to notify processes when an object of interest arrives in the space. With JavaSpaces, distributed processes communicate by reading and writing entries into space. The JavaSpace API provides an event notification mechanism, which enables processes to register for notification when a specific object is written to space. A space is defined by JavaSpaces as a shared, network-accessible repository for objects. This shared repository can persist objects written to a space beyond the lifetime of the process that created them. Spaces provide reliable storage for objects and support leases. Objects in a space are located via associative lookup, as opposed to more traditional keys or identifiers. More is said about associative lookup when entries are discussed later in this chapter. When you read or take an object from a space, a local copy of the object is created. As with any other local object, you can modify its public fields and invoke its methods. In addition, JavaSpaces supports transactions for single operations, such as writing an object to a single space, as well as multiple operations over one or more spaces. JavaSpaces Versus Traditional Database Technology JavaSpaces is not a database, although it does have database characteristics. For instance, the capability to persist objects would lead to comparing it to object database technology. However, you will find that the API to JavaSpaces is so simple that it could not possibly cover all the functions and features normally associated with databases. The differences include the following: • No defined query language—objects are retrieved with templates, not a specifically designed query language such as SQL or OQL. 93 • • No transparent access provided to objects—rather than blurring the distinction between in-memory and storage access of objects, JavaSpaces works on serialized copies of entries. A general lack of database tools and utilities—JavaSpaces provides a simple API and defines a minimal and lightweight persistence engine. JavaSpaces' strength lies in its capability to simplify the programming required to coordinate distributed communication and parallel processing. JavaSpaces Versus MOM (Message Oriented Middleware) There might also be a tendency to compare JavaSpaces to message oriented middleware (MOM). Again, the comparisons are justified, but miss the mark. MOM products support two fundamental models of communication; point-to-point and publishsubscribe. Point-to-Point In the point-to-point model, requests are targeted to a specific queue or receiver. Queues are named as network-accessible resources. A client sends a request to a specific receiver. The receiver then reads the message from the queue, which is managed by the messaging middleware. The messaging can be asynchronous or synchronous. Asynchronous messaging permits the initiating process to continue processing and avoid blocking until the message is retrieved. Publish-Subscribe In the publish-subscribe model, requests are targeted to a queue that is accessible by one or more receivers. The requestor does not know the identity of the receiver as in the point-to-point model. Subscribers register interest in a specific queue, and the messaging software ensures that messages are delivered to the subscribers. Both models have become popular because of their capability to loosely couple existing systems. Although JavaSpaces also provides loose coupling for distributed processes and MOM models of communication, there are a number of important differences. The differences include the following: • • • • • MOM supports messages; JavaSpaces supports objects. Most messaging products support the exchange of data. JavaSpaces supports the exchange of objects, and therefore active processes or behavior. MOM products often involve extensive administrative overhead. JavaSpaces are simple to set up and administer. Messaging products have proprietary interfaces, which means that two different messaging products might not be able to communicate without some gateway or bridging technology. JavaSpaces supports persistent messaging. A JavaSpaces message is an object in its own right, and can persist and manage its lifecycle independent of the originating process. JavaSpaces supports transactions and leases inherently. The concept of leases has no direct counterpart in MOM products. Although comparisons to database technology and message oriented middleware are valid, JavaSpaces still provides significant differences. These differences point to a new environment supporting networked applications as the norm, rather than the exception. 94 Entries Revisited Entries were introduced when templates built to define and find services were discussed in Chapter 4. Recall that when you register a service, you can associate attributes with the service, such as address- or service-specific information. These attributes were defined by the net.jini.core.entry.Entry interface. package net.jini.core.entry; public interface Entry extends java.io.Serializable { } The net.jini.core.entry.Entry interface is also used to identify entries that are capable of being used in JavaSpaces. You will implement this interface or extend the AbstractEntry class to create objects for use in a space. Let's review the rules associated with Entry objects: • • • It must have a default no argument constructor, which is required for serialization. It should contain only public instance variables, which are required for associative lookups. All instance variables must be serializable so that no primitive types are written or read from space. In addition, each public field is serialized separately. So, two fields referencing the same object prior to serialization will reference two separate but equal objects after being written to space. AbstractEntry is a basic implementation of the Entry interface. This class is often used as the base class for defining entries because it adheres to the Entry specification. For instance, it implements the proper semantics for equality and comparisons. Now that you have reviewed entries and their role in JavaSpaces, let's define a CommandEntry to read and write objects to a space that contains objects that implement the Command interface. public class CommandEntry extends AbstractEntry { public String name; public Command cmd; public CommandEntry() { this(null); } public CommandEntry(String name) { this(name,null); } public CommandEntry(String name, Command cmd) { this.name = name; this.cmd = cmd; } } The CommandEntry extends AbstractEntry and contains a name field used to name the associated command and a reference to the Command object. Also provided is the required no argument constructor. The fields of the CommandEntry are also serializable, so all the 95 requirements of the Entry specification have been met. What follows is a demonstration of how easy it is to write entries to space. Retrieving entries from JavaSpaces requires a template of the type or subtype of the entry to be built. Entries are compared, public field by public field, based on the template. If two fields have the same value, they are considered equal. When you want to retrieve an Entry from JavaSpace, you need to create a template for the system to compare for equality. For example: CommandEntry template = new CommandEntry("FileList"); The preceding fragment constructs a CommandEntry template that can be used to match and return any command that has been named "FileList" in the JavaSpaces server. CommandEntry template = new CommandEntry(); This second template can be used to match and return any command of any name. The null value acts as a wildcard in matching fields in an Entry. The null value will therefore match any and all values. There are a couple of implications as a result of this idiom. First, you cannot use null to match on an uninitialized value. You need to introduce another field, such as a Boolean, to perform such a comparison for each field in your Entry definition. That is because, as mentioned, null matches on anything. In addition, matching on null or for that matter any field, makes no guarantee as to which object will be returned as a result of the match. Suppose you have 10 commands that have been named "FileList," all with slightly different implementations in the space. If you fetch the command 10 different times using just the "FileList" argument, you might get: • • • The same command object returned 10 times A different command object returned each time Some combination of repetitive and unique command objects JavaSpaces makes no guarantee on which object will be returned. This has enormous implications on the design of your entries and the applicability of JavaSpaces as a solution. If you normally deal with traditional database technologies, this requirement might catch you off guard the first time you find yourself looking for a collection or set of objects with a specific value. In the relational world you do this type of programming all the time. For example, "Bring me back all rows where column x equals y." The set returned permits us to iterate through each record. Converting this table/data structure to an Entry will almost always fail. Entries are designed to be used when exact-match lookup semantics are useful. In other words, you supply the value you are looking for in the template you provide to the lookup. However, this should not be construed to mean "I need to know the entire value of the entry in order to find the one that matches." The wildcard value (null) provides the "I don't care" or "any will do" semantics for a specific field as previously stated. JavaSpaces also provides us with the powerful capability to match on subtypes. All fields added by a subtype are considered to be wildcards. This enables a template to match entries of any of its subtypes. This capability, combined with dynamic downloading of code, provides a natural solution to the evolution of service functionality. New class structures can be defined without affecting current applications using old interface semantics. Or, new functionality can be dynamically invoked by downloading new class definitions on demand. 96 You will be building a number of sample applications in Part III of this book. These examples will further refine and define the use of entries and distributed data structures in JavaSpaces. The Simple API The JavaSpaces API is very simple. All operations are invoked on an object that implements the JavaSpace interface. For example, the following code fragment will write an entry of type CommandEntry into the JavaSpaces service referred to by the identifier space: // Gain access to an instance of JavaSpace JavaSpace space = getSpace(); // Create an entry CommandEntry entry = new CommandEntry(); // initialize the public fields entry.name = "FileList"; entry.cmd = new FileList("/usr/files/jini1_1"); // write the entry to space and allow it to exist for 1 minute space.write(entry, null, 60 * 1000); The JavaSpace interface is defined as: Listing 5.1 The JavaSpace Interface package net.jini.space; import import import import java.rmi.*; net.jini.core.event.*; net.jini.core.transaction.*; net.jini.core.lease.*; public interface JavaSpace { public final long NO_WAIT = 0; // don't wait at all Lease write(Entry e, Transaction txn, long lease) throws RemoteException, TransactionException; Entry read(Entry tmpl, Transaction txn, long timeout) throws TransactionException, UnusableEntryException, RemoteException, InterruptedException; Entry readIfExists(Entry tmpl, Transaction txn, long timeout) throws TransactionException, UnusableEntryException, RemoteException, InterruptedException; Entry take(Entry tmpl, Transaction txn, long timeout) throws TransactionException, UnusableEntryException, RemoteException, InterruptedException; Entry takeIfExists(Entry tmpl, Transaction txn, long timeout) throws TransactionException, UnusableEntryException, 97 RemoteException, InterruptedException; EventRegistration notify(Entry tmpl, Transaction txn, RemoteEventListener listener, long lease, MarshalledObject handback) throws RemoteException, TransactionException; Entry snapshot(Entry e) throws RemoteException; } The Transaction and TransactionException types in the preceding signatures are imported from net.jini.core.transaction. The Lease type is imported from net.jini.core.lease. The RemoteEventListener and EventRegistration types are imported from net.jini.core.event. The Transaction parameter may be null, which means that no Transaction object is managing the operation. Transactions will be discussed in the next chapter. The JavaSpace interface is not a remote interface. Each implementation of a JavaSpaces service exports proxy objects that implement the JavaSpace interface locally on the client, talking to the actual JavaSpaces service through an implementation-specific interface. An implementation of any JavaSpace method can communicate with a remote JavaSpaces service. Each method defined throws RemoteException to allow for possible failures. You should expect RemoteExceptions on method calls in the same cases in which you would expect them for methods invoked directly on an RMI remote reference. The details of each JavaSpace method are given in the sections that follow. write A write statement copies an Entry object to a JavaSpaces service. long lease_requested = 60 * 60 * 1000; Lease lease = space.write(template, null, lease_requested); Each write operation places a new entry into the specified space, even if the same Entry object is used in more than one write. Each write invocation returns a Lease object. If the requested time is longer than the space is willing to grant, you will get a lease with a reduced time. When the lease expires, the entry is removed from the space. The following code fragment demonstrates how to determine whether the lease granted is different from the lease requested. long expires = lease.getExpiration(); if(expires < lease_requested) { // code to handle case where lease is less than requested } If a write returns without throwing an exception, that entry is committed to the space. If a RemoteException is thrown, the write might or might not have been successful. If any other exception is thrown, the entry was not written into the space. Writing an entry into a space can also generate notifications to registered objects (notify will be discussed a little later). 98 read and readIfExists The two forms of the read request—read and readIfExists— will search the JavaSpaces service for an entry that matches the template provided as an Entry. // create a template setting the name field but leaving the Command null String name = "FileList" CommandEntry template = new CommandEntry(name); CommandEntry cmdEntry = (CommandEntry)space.read(template, null, TIMEOUT_VALUE); If a match is found, a reference to a copy of the matching entry is returned. If no match is found, null is returned. Passing a null reference for the template will match any entry. A readIfExists request will return a matching entry, or null if there is currently no matching entry in the space. CommandEntry template = new CommandEntry(name); CommandEntry cmdEntry = (CommandEntry)space.readIfExists(template, null, TIMEOUT_VALUE); If the only possible matches for the template are involved in transactions that have conflicting locks, the timeout value specifies how long the client is willing to wait for interfering transactions to commit before returning a value. At the end of that time, if no value can be returned that would not interfere with transactional state, null is returned. A read request acts like a readIfExists, except that it will wait until a matching entry is found or until transactions commit, whichever is longer, up to the timeout period. In both read methods, a timeout of NO_WAIT means to return immediately, with no waiting, which is equivalent to using a zero timeout. take and takeIfExists The take and the takeIfExists requests perform exactly like the corresponding read and readIfExists requests, except that the matching entry is removed from the space. If a take or takeIfExists returns a non-null value, the entry has been removed from the space. notify A notify request registers interest in future incoming entries that match the specified template to the JavaSpaces service. Matching is done as it is for read. When matching entries are written, the specified RemoteEventListener will be notified. When you invoke notify, you provide an upper bound on the lease time, which is how long you want the registration to be remembered by the JavaSpaces service. Each notify returns a net.jini.core.event.EventRegistration object. When an object is written that matches the template supplied in the notify invocation, the listener's notify method is invoked with a RemoteEvent object. 99 snapshot The process of writing an object to space involves serialization. Serializing an entry for transmission to a JavaSpaces service will be identical if the same entry is used twice. This is most likely to be an issue with templates that are used repeatedly to search for entries with read or take. The snapshot method gives the JavaSpaces service implementor a way to reduce the impact of repeated use of the same entry. Invoking snapshot with an Entry will return another Entry object that contains a snapshot of the original entry. Using the returned snapshot entry is equivalent to using the unmodified original entry in all operations on the same JavaSpaces service. So read, write, take, notify, and snapshot comprise the entire JavaSpace API. Simple, but extremely powerful in developing distributed applications. Distributed Data Structures JavaSpaces is built using distributed data structures. Distributed data structures enable multiple processes to access and manipulate the content of a structure in parallel. Distributed data structures require a different approach to data access and control. In a typical database system, processes are defined that act as barriers or locks at a level in the structure that inhibits concurrent access. This is done to ensure that the database remains in a consistent state, often referred to as supporting the ACID properties—atomicity, concurrency, isolation, and durability. Other programming techniques include defining manager objects that provide a barrier around data or files that ensure data is accessed serially. Distributed data structures offer a number of advantages to traditional database processing by enabling multiple processes to access and change information concurrently. You might want to build a distributed database structure whenever the following requirements must be met: • • • • Scale to large, rapidly growing user populations High availability over an unreliable network, even during partial failure Maintain user data consistency across a large user base Improve or ease operational management of large data structures In the book JavaSpaces Principles, Patterns, and Practice by Freeman, et al. (Addison Wesley), there is a significant amount of detail provided on building and using distributed data structures in JavaSpaces. The book classifies distributed data structures according to common usage patterns. Shared Variables A shared variable enables multiple processes to change the shared value in an atomic manner. JavaSpaces provides an easy approach to support such changes. For instance, the following entry definition represents an index into a data structure. Listing 5.2 The Index Class public class Index implements AbstractEntry { public Integer value; public String name; public Index() { } 100 public increment() { value = new Integer(value.intValue()+1); } public decrement() { value = new Integer(value.intValue()-1); } } The following code fragment takes the index entry and increments it by using the take and write semantics of the JavaSpace API. Index template = new Index(); Index index = (Index)space.take(template, null, Long.MAX_VALUE); index.increment(); space.write(index, null, Lease.FOREVER); The take method removes the entry from space. Any process trying to read the entry will be blocked until the entry is returned with the write method. Therefore, serialization of updates across distributed processes is guaranteed. You can use the index entry to iterate through ordered structures, such as distributed arrays (discussed in the following section). By following the take, then write protocols for updating the structure, the ACID properties for data reliability are assured. This is all under the control of JavaSpaces and simplifies the development task for the programmer. The astute reader will note that the transaction parameter is missing in the previous calls. This could present problems unless the appropriate error recovery is implemented. More is said in regard to transactions in the next chapter. Ordered Structures Ordered structures are collections that have an index or position field defined in each entry of the collection. If you are defining a process that might need to iterate through the entries of a space or require a certain sequencing for the entries, then ordered structures provide one solution. Distributed Arrays Distributed arrays are the most popular example of an ordered structure. They are built from space entries that include an index and name reference. The distributed array enables you to access individual elements within the array by going directly to an element via the position index. No locking is required at a start or root element. This permits and promotes concurrent access, and thus reduces wait and blocking issues as shown in Figure 5.2. Figure 5.2. Traditional databases use barriers to restrict concurrent access. Distributed data structures promote concurrent access. 101 In addition to the position field, you need to define a name field. The name field provides a mechanism to reuse the index entry across multiple data structures as opposed to subtyping the index entry for each structure. However, either approach—associating a named-index with a data structure, or subtyping per data structure—is valid. The naming approach reduces the amount of object types that result in the space. Unordered Structures Unordered structures are used when sequence or order is not important as in Figure 5.3. For instance, workflow applications are often based on a Task entry that is written to space. Worker processes do not care which Task they take from space, as they can operate or use any task that they retrieve. The idea is that as the number of tasks grows, so can the number of worker processes that take and process task objects. The system can expand and contract based on demand. Workers will often write a Result object to space on completion, which is read or taken by a Boss or TaskManager object. The manager is responsible for controlling or aggregating the results from many tasks. Figure 5.3. Unordered structures are used when sequence is not important. In effect, the structure implements "any object will do" semantics. 102 Bags Unordered structures are referred to as bags because you can throw any type of object into them without concern for the sequencing of the structure. Application Basics Developing applications with JavaSpaces requires both of the following: • • Experience with synchronization techniques in distributed processing Experience defining data structures supporting loosely coupled communication JavaSpaces goes a long way toward minimizing the learning curve required to gain this experience. Synchronization Techniques The basics of space-based programming synchronization can be quickly summarized thusly: Multiple processes can read an entry in a space at any time. But when a process wants to update an entry, it first has to remove it (take) from the space and thereby gain sole access to it. In other words, the read, take, and write operations enforce coordinated access to entries. Entries and their operations give you everything you need to build more complex synchronization and object coordination. Loosely Coupled Communication JavaSpaces provides a simple foundation for the exchange of information between loosely coupled processes. For example, by writing a message to a space, any process can read or take the message from that space. Combined with the Jini discovery process, it is easy to recognize the power and flexibility of dynamically discovering a space and interfacing with it. In addition, coupled with RMI, the options for exchanging data and objects become almost limitless. 103 Deploying JavaSpaces Let's now deploy JavaSpaces into the environment to demonstrate the simplicity and power of the technology through the following examples. JavaSpaces requires the use of a transaction service like the Jini-supplied mahalo service. The transaction service provides support for ACID properties through the two-phase commit protocol. You must start the mahalo transaction service prior to starting JavaSpaces. We will discuss transactions in the next chapter. For now, simply start the mahalo service supplied with Jini. The following script starts the mahalo transaction service. It can be found in the services\scripts directory. Simply invoke the startMahalo.cmd. You can use the setup.cmd file to ensure your environment can run the scripts. If you prefer you can use the GUI-supplied service starter that you used in Chapter 3, "Supporting Technologies." C:\JiniJavaSpaces\services\scripts\startMahalo Listing 5.3 startMahalo.cmd Script @echo off @call setup.cmd set GROUPS=public echo echo echo echo echo echo echo echo Jini and JavaSpaces Application Development ------------------------------------------Jini install directory %JINI_HOME% Examples install directory %JWORK_HOME% Web server %HTTP_ADDRESS% Default group %GROUPS% ------------------------------------------Starting the mahalo transaction service... java -jar Djava.security.policy=%JINI_HOME%\example\lookup\policy.all -Dcom.sun.jini.mahalo.managerName=TransactionManager %JINI_HOME%\lib\mahalo.jar http://%HTTP_ADDRESS%/mahalo-dl.jar %JINI_HOME%\example\txn\policy.all %JWORK_HOME%\services\logs\txn_log %GROUPS% Sun provides a reference implementation of the JavaSpaces Service called outrigger. You can run outrigger as an activatable service or as a transient service. Which version you use will depend on your requirements for persistence. In other words, will you require the persistence service state to survive restarts or system crashes? The transient space does not provide persistence across system restarts. So, if the system crashes or you need to restart the service, the information that is stored in the transient space will not be available. The following script starts the outrigger persistent service: C:\JiniJavaSpaces\services\scripts\startOutrigger 104 Listing 5.4 The startOutrigger.cmd Script @echo off @call setup.cmd set GROUPS=public set SPACENAME=JavaSpaces echo echo echo echo echo echo echo echo echo Jini and JavaSpaces Application Development ------------------------------------------Jini install directory %JINI_HOME% Examples install directory %JWORK_HOME% Web server %HTTP_ADDRESS% Default group %GROUPS% Default SpaceName property %SPACENAME% ------------------------------------------Starting the outrigger JavaSpaces service... java -jar -Djava.security.policy=%JINI_HOME%\policy\policy.all -Dcom.sun.jini.outrigger.spaceName=%SPACENAME% %JINI_HOME%\lib\outrigger.jar http:// %HTTP_ADDRESS%/outrigger-dl.jar %JINI_HOME%\policy\policy.all %JWORK_HOME%\services\logs\js_log GROUPS Notice the -Dcom.sun.jini.outrigger.spaceName property, which is used to name this instance of outrigger. In order to start outrigger, your Jini environment will need the following: • • • • An HTTP server running with a public directory available for downloading files. An RMI daemon (rmid) running to activate services An LUS (reggie) running to register and lookup services The transaction service (mahalo) required for JavaSpace transaction support One thing to note is that after your environment is in place, subsequent startups only require the first two services—an HTTP server for downloading code, and the RMI daemon for activating services. Because reggie, mahalo, and outrigger can all be started as activatable services, they will be restarted by RMI automatically when rmid starts up. Figure 5.4 shows one of many possible configurations for testing the next example. If you have access to multiple machines (which is highly recommended), you should experiment with different deployment alternatives. Of course, you can get this example to work on a single machine, but once you do, try running the services using different configurations. Figure 5.4. An sample configuration for testing the JavaSpaces applications in this chapter. Although the examples can be run on a single machine, configuring and developing on the network is recommended. 105 Figure 5.4 depicts separate machines running on different network segments. You have deployed your HTTP server, rmid, and reggie on a single server, while JavaSpaces and the transaction service are running on a different server and segment. Then you test your message producing pad (discussed later in this chapter) and message consuming shell on different workstations. The process to access the JavaSpaces service is like accessing any other Jini service; you perform the lookup process with an LUS that has the JavaSpaces service registered. The net.jini.space package provides the service interface and related classes for the Sun/Jini outrigger implementation. Listing 5.5 The SpaceAccessor Class package org.jworkplace.util; import java.rmi.*; import java.util.*; import net.jini.space.JavaSpace; import net.jini.core.entry.*; import net.jini.core.lookup.*; import net.jini.lookup.entry.*; import net.jini.core.discovery.*; /* * The SpaceAccessor class is a utility class used to resolve the reference * to the outrigger JavaSpace service */ public class SpaceAccessor { // host name running LUS and name of JavaSpace instance public synchronized static JavaSpace getSpace(String hostname, String name) { 106 try { if (System.getSecurityManager() == null) { System.setSecurityManager( new RMISecurityManager()); } // unicast discovery LookupLocator lookup = new LookupLocator("jini://" + hostname); System.out.println("SpaceAccessor using locator: " + lookup); ServiceRegistrar registrar = lookup.getRegistrar(); // create Name entry to match on Entry entries[] = { new Name(name) } ; // lookup the service in the LUS JavaSpace space = (JavaSpace)registrar.lookup(new ServiceTemplate(null,null,entries)); } // return the proxy return space; catch (Exception e) { System.err.println(e); } return null; } } This first iteration of the utility only supports unicast discovery. So, to use this utility, you must know the location of the LUS that has the JavaSpaces instance running as referenced by the hostname parameter. The name argument refers to the -Dcom.sun.jini.outrigger. spaceName=JavaSpaces property that was used to launch JavaSpaces. A Sample Application—Building the MessagePad You can quickly build an example that permits a message to be sent through space. In the following example, a take operation is used to retrieve and delete messages as they arrive into space. The simple message definition extends AbstractEntry and adds a public field to store text. Listing 5.6 The Message Class package org.jworkplace; import net.jini.entry.AbstractEntry; /** * The Message class is used to create messages for a JavaSpace. */ public class Message extends AbstractEntry { public String text; 107 public Message() { } public Message(String text) { super(); this.text = text; } } The MessagePad class can be used to write single-line messages to a JavaSpaces service. The first argument is the hostname where a lookup service is running that has a registered instance of JavaSpaces. The second argument is the name of the JavaSpaces instance. It must be a JavaSpaces service that has been registered with the appropriate Name entry, such as JavaSpaces. Listing 5.7 The MessagePad Class package org.jworkplace; import java.io.*; import net.jini.space.JavaSpace; import org.jworkplace.util.SpaceAccessor; public class MessagePad { public static void main(String[] args) { String spacename = null, hostname = null; if(args.length < 2) { System.out.println("Usage java org.jworkplace.MessagePad [hostname] [spacename]"); } System.exit(1); else { hostname = args[0]; spacename = args[1]; } try { // get the space proxy JavaSpace space = SpaceAccessor.getSpace(hostname, spacename); if(space != null) { // read input from the console String line; BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); // create a message entry Message msg = new Message(); // while console input available while ((line = reader.readLine()).length() > 0) { // set the message text msg.text = line; // write a message per line entered space.write(msg, null, Long.MAX_VALUE); } } else { System.out.println("Unable to find: " + spacename); 108 } System.exit(1); } catch (Exception e) { e.printStackTrace(); } } } The MessageShell class is used to take the messages written by the MessagePad to the JavaSpaces service and displays them on the console. Again, the first argument is the hostname where JavaSpaces has registered with an LUS, and the second argument is the name of the JavaSpaces instance. Listing 5.8 The MessageShell Class package org.jworkplace; import net.jini.space.JavaSpace; import org.jworkplace.util.SpaceAccessor; public class MessageShell { public static void main(String[] args) { String spacename = null, hostname = null; if(args.length < 2) { System.out.println("Usage java org.jworkplace.MessageShell [hostname] [spacename]"); } System.exit(1); else { hostname = args[0]; spacename = args[1]; } try { JavaSpace space = SpaceAccessor.getSpace(hostname, spacename); if(space != null) { // Retrieve any Message in the space Message template = new Message(); for(;;) { // take each message from the space // the call will block until a message arrives Message received = (Message)space.take(template, null , Long.MAX_VALUE); } System.out.println(received.text); } else { // we were unable to find the JavaSpaces service specified System.out.println("Unable to find: " + hostname + " " + spacename); System.exit(1); } 109 } catch (Exception e) { e.printStackTrace(); } } } Figure 5.5. Messages can be easily exchanged by using the write and take operations of the JavaSpace API. Step-By-Step Compile the Files To build the example, change to the Chapter 5 Example 1 directory and invoke the compile command. C:\JiniJavaSpaces\chapter5\example1\compile Start the MessagePad To test the message producer, move to the directory and machine where you deployed the client software (MessagePad). Run the following command: C:\JiniJavaSpaces\chapter5\example1\>start messageSender hostname spacename where: • • hostname is the host running the LUS where the space is registered spacename is the name of the JavaSpaces instance, such as JavaSpaces Start the MessageShell To test the message receiver, move to the directory and machine where you deployed the MessageShell. Run the following command: 110 C:\JiniJavaSpaces\Chapter5\example1>start messageReceiver hostname spacename where: • • hostname is the host running the LUS where the space is registered spacename is the name of the JavaSpaces instance, such as JavaSpaces You will be able to type messages into the console of the MessagePad and see them displayed on the console of the MessageShell. A null line terminates message production. Using Distributed Events with Spaces JavaSpaces uses the distributed event model of Jini to deliver remote events to remote listeners. JavaSpaces acts as the source of events when an entry is written to space that matches a template that has been registered by or for a RemoteEventListener. JavaSpaces compares each entry written to space to determine whether a RemoteEventListener should be notified of the existence of a new entry in space that matches a template associated with the registration. The space sends a RemoteEvent object to the listener by invoking the notify method of the RemoteEventListener. You can extend your Message entry to support point-to-point communication using distributed events. Extend your Message class to include the from-and-to system identification. This will enable a client to supply the destination and source of the system exchange. Unlike the MessageShell, which simply took any message from space, the new RequestListener will be more selective. In addition, it receives notification of a matching entry using distributed events. The Request class extends Message and adds the from-and-to system identification. Listing 5.9 The Request Class package org.jworkplace; import net.jini.entry.AbstractEntry; public class Request extends Message { public String toSystem; public String fromSystem; public Request() { } } The RequestListener implements the RemoteEventListener interface and uses the static UnicastRemoteObject.exportObject method to export itself to the RMI runtime. When JavaSpaces produces the RemoteEvent to signal the arrival of a matching Entry, the space using the RMI runtime will invoke the notify method of RequestListener. Listing 5.10 The RequestListener Class package org.jworkplace; 111 import import import import import import java.rmi.*; java.rmi.server.UnicastRemoteObject; net.jini.space.JavaSpace; net.jini.core.event.RemoteEvent; net.jini.core.event.RemoteEventListener; org.jworkplace.util.SpaceAccessor; public class RequestListener implements RemoteEventListener { private JavaSpace space; // Source system identification private String toSystem; public RequestListener(JavaSpace space, String toSystem) throws RemoteException { this.space = space; this.toSystem = toSystem; // export to RMI UnicastRemoteObject.exportObject(this); } // Receive notification when entry arrives in space public void notify(RemoteEvent event) { // create the template using the source system id Request template = new Request(); template.toSystem = toSystem; Request request = template; // loop to take all entries from space while(request != null) { try { request = (Request)space.take(template, null, Long.MAX_VALUE); System.out.println(request.text); } catch (Exception e) { e.printStackTrace(); } } } You have modified the prior version of the MessagePad to include the registration of a RemoteEventListener with JavaSpaces. Recall that the notify method of the JavaSpace API is: EventRegistration notify(Entry tmpl, Transaction txn, RemoteEventListener listener, long lease, MarshalledObject handback) throws RemoteException, TransactionException; Create the registration with the following code fragment: // create the space listener passing the source system id // we use this in the listener to take matching requests listener = new RequestListener(space, fromSystem); // register for Requests from destination system 112 Request template = new Request(); template.toSystem = toSystem; registration = space.notify(template,null,listener,Lease.FOREVER, null); In addition, it now requires the command line to include the source and destination system (fromand-to) identification. Listing 5.11 The MessagePad Class Updated package org.jworkplace; import import import import import java.io.*; net.jini.space.JavaSpace; net.jini.core.event.EventRegistration; net.jini.core.lease.Lease; org.jworkplace.util.SpaceAccessor; public class MessagePad { public static void main(String[] args) { // Registration for space notification EventRegistration registration; // RemoteEventListener that will receive notification RequestListener listener; // Host running the LUS String hostname = null; // The String // The String name of the JavaSpaces instance spacename = null; request source system identification fromSystem = null; // The request destination system identification String toSystem = null; if(args.length < 4) { System.out.println("Usage [hostname] [spacename] [from] [to]"); System.exit(1); } else { hostname = args[0]; spacename = args[1]; fromSystem = args[2]; toSystem = args[3]; } // unicast discovery setup JavaSpace space = SpaceAccessor.getSpace(hostname, spacename); if(space != null) { try { // create the space listener passing the id listener = new RequestListener(space, fromSystem); // register for Requests from destination system Request template = new Request(); template.toSystem = toSystem; 113 registration = space.notify(template, null, listener, Lease.FOREVER, null); } catch (Exception e) { e.printStackTrace(); System.exit(1); } // Create messages from console input to destination system String line; try { BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); while ((line = reader.readLine()).length() > 0) { Request request = new Request(); request.toSystem = toSystem; request.fromSystem = fromSystem; request.text = line; space.write(request, null, Long.MAX_VALUE); } } catch (Exception e) { e.printStackTrace(); } } else { System.out.println("Unable to find: " + spacename); System.exit(1); } } } Compile the Files To build the example, change to the Chapter 5 Example 2 directory and invoke the compile command. C:\JiniJavaSpaces\chapter5\example2\compile Create Jar File Now invoke the deploy.cmd script to generate the jar file with the RequestListener_Stub.class. The script also copies the jar file to the Web server directory. C:\JiniJavaSpaces\Chapter5\example2>deploy Figure 5.6. The MessagePad writes messages to JavaSpace. The RequestListener objects are notified of the arrival of messages that match a template. The template is initialized with a system identifier, such as a uri. 114 Listing 5.12 The deploy.cmd Script @echo off @call ..\..\services\scripts\setup.cmd echo Jini and JavaSpaces Application Development echo Chapter 5 Example 2 echo ------------------------------------------echo Jini install directory %JINI_HOME% echo Examples install directory %JWORK_HOME% echo Web server %HTTP_ADDRESS% echo ------------------------------------------echo Creating jar file chap5ex2-dl.jar in directory %JWORK_HOME%\services\lib echo ... jar cvf %JWORK_HOME%\services\lib\chap5ex2-dl.jar -C org/jworkplace/ / RequestListener_Stub.class Start the MessagePad To test the request listener, run the following command: C:\JiniJavaSpaces\Chapter5\example2>start messageSender hostname spacename fromname toname where: • • • • hostname is the host running the LUS where the space is registered spacename is the name of the JavaSpaces instance, such as JavaSpaces fromname is the name of the source system, such as www.jworkplace.org toname is the name of the destination system, such as www.sams.com Now start another instance of the MessagePad: 115 C:\JiniJavaSpaces\Chapter5\example2>start messageSender hostname spacename fromname toname where: • • • • hostname is the host running the LUS where the space is registered spacename is the name of the JavaSpaces instance, such as JavaSpaces fromname is the name of the source system, such as www.sams.com toname is the name of the destination system, such as www.jworkplace.org You should be able to type messages into the console of one MessagePad and see the messages displayed on the console of the other MessagePad. You now have a simple, point-topoint framework for exchanging requests and responses through JavaSpaces. You can start any number of MessagePads using different from-and-to address identifications and be able to communicate through the space service. The space will queue the messages until a requesting MessagePad takes the requests from space. Channel Data Structures for Communication The last example in this chapter demonstrates a chat program. In this example, you will build on the concept of a channel. A channel is an ordered set of messages—an implementation of a distributed data structure that has an index or position field defined in each entry of the collection. This allows you to sequence the messages in the collection and to iterate through the entries. The new ChatMessage class extends Message by providing two new fields: a channel name and a position. The name is used to uniquely identify the channel in a specific instance of JavaSpace. The position is used to indicate where in the channel sequence the message resides. The text field contains the actual message data. Listing 5.13 The ChatMessage Class package org.jworkplace; import net.jini.entry.AbstractEntry; /* * The Message class is used to create messages for a chat channel. */ public class ChatMessage extends Message { public String channel; public Integer position; public ChatMessage() { } public ChatMessage(String channel, Integer position, String text) { this.channel = channel; this.position = position; this.text = text; } } The combination of the channel name and the position uniquely identifies a chat message in space. In addition, because you sequentially increment the position for each message written to a channel, 116 you can determine the number of messages in a channel by starting at the first message and incrementing the position with each read until a null is returned. You also create a Tail entry to indicate the last entry in the channel. If you have worked with queues before, then the heads and tails technique to indicate the start and end of a queue structure should not be new. However, what might be new is having a named tail that equals the named queue (channel). Listing 5.14 The Tail Class package org.jworkplace; import net.jini.entry.AbstractEntry; public class Tail extends AbstractEntry { public String name; public Integer position; public Tail() { super(); this.name = null; this.position = null; } public int increment() { position = new Integer(position.intValue() + 1); return position.intValue(); } public int decrement() { position = new Integer(position.intValue() - 1); return position.intValue(); } } The ChatController class uses ChatMessage and Tail entries to enable space chat. If a Tail entry for the designated channel does not exist, create a new channel. Listing 5.15 The ChatController Class package org.jworkplace.viewers; import java.awt.*; import java.awt.event.*; import java.util.*; import javax.swing.*; import net.jini.space.JavaSpace; import net.jini.core.lease.*; import org.jworkplace.*; /** * The ChatController class is used to create the ChatView and control the * reading and writing of messages to the chat channel */ public class ChatController implements ActionListener { // The name of the channel private String channel = null; 117 // name of user in chat session private String userName = null; // Simple Swing JPanel private ChatViewer viewer = null; // Thread to read new messages private ChannelListener cl = null; private volatile Thread listener; // Messages exist for 1 hour public long CHAT_TIME = 60 * 60 * 1000; // 1 hour private JavaSpace space; private JFrame frame; public ChatController(JFrame frame, JavaSpace space, String channel, String user) { this.frame = frame; this.space = space; this.userName = user; this.channel = channel; viewer = new ChatViewer(this,channel); // start the channel listener thread cl = new ChannelListener(); cl.start(); // determine if there is already a session open if(!activeChannel()) createChannel(); } // Does Tail entry exists for this channel? synchronized boolean activeChannel() { Tail template = new Tail(); template.name = channel; try { Tail tail = (Tail)space.readIfExists(template, null, JavaSpace.NO_WAIT); if(tail == null) return false; } catch (Exception e) { } return true; } // if no active channel create and initialize new Tail private void createChannel() { Tail tail = new Tail(); tail.name = channel; showStatus("Creating channel "+ channel); tail.position = new Integer(0); try { Lease lease = space.write(tail, null, CHAT_TIME); } catch (Exception e) { e.printStackTrace(); return; } } 118 When you press the send button, an ActionEvent is triggered and delivered to the actionPerformed method. The message is displayed in a JTextArea and appended to the channel, meaning it is written to space. // Action to display message and append to channel public void actionPerformed(ActionEvent event) { Object object = event.getSource(); if (object == viewer.chatControl) { String message = userName+"> "+viewer.getMessage(); if (message.equals("")) { JOptionPane.showMessageDialog(frame, "Enter message"); return; } viewer.setMessage(""); append(channel, message); } } // append message to channel private void append(String channel, String msg) { // get the next available position Integer messageNum = getMessageNumber(); // create a new message using the new position ChatMessage message = new ChatMessage(channel, messageNum, msg); try { // write to space Lease lease = space.write(message, null, CHAT_TIME); } catch (Exception e) { e.printStackTrace(); return; } } The getMessageNumber method determines whether a Tail exists. If a Tail entry exists, you take the Tail from space, increment the position, and write it back. While you have taken the Tail entry, no other process will be able to read the Tail. Here again, JavaSpaces provides an easy technique for serializing distributed processes. If the Tail does not exist, create a new channel. // get the current tail and increment private Integer getMessageNumber() { try { Tail template = new Tail(); template.name = channel; Tail tail = (Tail)space.take(template, null, 10 * 1000); // If no tail exists create a new channel if (tail == null) { createChannel(); tail = (Tail) space.take(template, null, Long.MAX_VALUE); } // increment the tail position tail.increment(); // write the tail to space Lease lease = space.write(tail, null, CHAT_TIME); // return the next position return tail.position; } catch (Exception e) { e.printStackTrace(); return null; } 119 } public JPanel getChatView() { return viewer; } On a WindowClosingEvent you interrupt and kill the channel listener thread. // set the listener to null and interrupt the thread public void windowClosing() { listener = null; cl.interrupt(); } The ChannelListener thread is an internal class. It reads messages on startup that already exist in the channel if you are joining an active session. The run method simply blocks on read requests, incrementing the position after each read, then waits for the next message to arrive or until it's interrupted. Listing 5.16 The ChannelListener Class // The channel listener public class ChannelListener extends Thread int position = 1; String newline; { public ChannelListener() { newline = System.getProperty("line.separator"); // If joining an existing chat display chat history if(activeChannel()) { try { ChatMessage template = new ChatMessage(); template.channel = channel; ChatMessage msg = template; // loop through all messages starting at 2 while(msg != null) { template.position = new Integer(position++); msg = (ChatMessage)space.readIfExists(template, null, JavaSpace.NO_WAIT); if(msg != null) { viewer.append(msg.text + newline); } else { position--; } } } catch (Exception e) { e.printStackTrace(); } } } // run until interrupted public void run() { listener = Thread.currentThread(); while(listener != null) { ChatMessage template = new ChatMessage(); template.channel = channel; ChatMessage msg = null; // increment the current position template.position = new Integer(position++); 120 try { // wait till message arrives msg = (ChatMessage)space.read(template, null, Long.MAX_VALUE); } // display new message viewer.append(msg.text + newline); catch (Exception e) { } } } } } The ChatFrame class contains the main method that accepts the command line arguments, resolves the JavaSpace reference using the SpaceAccessor utility, and creates the view controller. Listing 5.17 The ChannelFrame Class package org.jworkplace.viewers; import java.awt.*; import java.awt.event.*; import java.io.*; import java.rmi.*; import java.util.*; import javax.swing.*; import javax.swing.event.*; import net.jini.space.JavaSpace; import org.jworkplace.*; import org.jworkplace.util.SpaceAccessor; public class ChatFrame extends JFrame { public static void main(String[] args) { if(args.length < 4) { System.out.println("Usage [hostname] [spacename] [channel] [user]"); System.exit(1); } ChatFrame frame = new ChatFrame(args[0], args[1], args[2], args[3]); } public ChatFrame(String hostname, String spacename, String channel, String user) { super("JWorkPlace"); JavaSpace space = SpaceAccessor.getSpace(hostname, spacename); addWindowListener(new WindowEventHandler()); getContentPane().setBackground(Color.black); getContentPane().add(new ChatController(this, space, channel,user).getChatView()); setSize(480,640); setVisible(true); } 121 class WindowEventHandler extends WindowAdapter { public void windowClosing(WindowEvent evt) { System.exit(0); } } } Figure 5.7 depicts a chat channel with four active messages. The chat controller would append the next message using position five. Each participant (ChannelListener) in the chat is blocking on a read request using a template with position five. When the entry is written, each participant will read the entry and increment the template position number to block on position six. Figure 5.7. A distributed array structure can be used to implement a message channel. The channel is used to sequence messages and control access to message content. Now let's build your chat program. The chat program will provide the capability to send messages on a named channel. Any number of participants can share the channel. The user sending the message is identified on the display panel. Compile the Files To build the example, change to the Chapter 5 example 3 directory and invoke the compile command. C:\JiniJavaSpaces\chapter5\example3\compile Start the Chat Control You specify the channel and your user identification when you start the chat program. C:\JiniJavaSpaces\Chapter5\example3>start chat hostname spacename channel user where: 122 • • • • hostname is the host running the LUS where the space is registered spacename is the name of the JavaSpaces instance, such as JavaSpaces channel is the name of the chat session to join, such as movies user is your name displayed in the session Now start another instance of the chat controller (see Figure 5.8). Use the same channel name, but change the user identification. Type your message and send! Figure 5.8. The Chat Program can be used as a simple Instant Messenger using JavaSpaces. Summary JavaSpaces is a Jini service that supports distributed persistence and the design of distributed algorithms. The JavaSpace API is simple, yet provides much of the common functionality required for distributed systems. It simplifies the development task, particularly when you can model your task as a flow of objects between systems distributed over a network. JavaSpaces is built from distributed data structures, such as shared variables, unordered collections, and distributed arrays. Sun provides a reference implementation of the JavaSpaces Service called outrigger. JavaSpaces provides a simple foundation for the exchange of information between loosely coupled processes. Combined with the Jini discovery process, it is easy to recognize the power and flexibility of dynamically discovering a space and interfacing with it. In addition, coupled with RMI, the options for exchanging data and objects become almost limitless. 123 Chapter 6. Transaction Service IN THIS CHAPTER • • • • • Distributed Transactions The Jini Transaction Framework The Mahalo Implementation Transactional Shell Example Summary In the last chapter we introduced the JavaSpaces service. In order to run the Jini-supplied JavaSpaces service (outrigger), you needed to start a transaction service. In this chapter, we will discuss the Jini Transaction API and the Transaction Service (mahalo) supplied with Jini. In general, transactional systems let you group together a set of operations so that either all the operations complete, or none of them do. Without this transactional capability, the states of systems can easily become inconsistent. Distributed systems in particular can suffer because participants can crash the network or leave the network before an operation has completed. By using transactions, you can ensure that the operations do complete, or if they don't, that the state of the participating resources remains unchanged. Distributed Transactions Distributed transactions have traditionally been tightly coupled with database management systems and transaction processing monitors (TP Monitor). Transactions and transactional integrity are not new issues in distributed computing. However, traditional solutions have tended to be heavyweight and complex. Most distributed transaction processing has standardized on the two-phase commit protocol. The two-phase commit protocol defines the communication exchange that enables distributed systems and resources to wrap a set of operations in such a way that they appear to be a single operation. The protocol requires a coordinator or transaction manager that guarantees all transactional participants will eventually know whether they should commit the operations (roll forward), or abort them (roll backward). A participant can be any resource that supports the two-phase commit contract by implementing the appropriate interface. Participants are not limited to databases or other persistent storage services. A transaction is bracketed by one of these two actions: • • Begin—Commit Begin—Abort In phase one (see Figure 6.1), the transaction manager asks all participants whether they are ready and able to commit a transaction. If a participant responds negatively (vote abort), the participant will automatically roll back any work it performed on behalf of the transaction and discard any knowledge it had of the transaction. Figure 6.1. The two-phase commit protocol is used by the Jini transaction manager to coordinate participants in a distributed transaction. 124 In phase two, the transaction manager determines whether there are any negative replies, and if so, instructs all participants to roll back. If all replies are positive (vote commit), it will instruct the participants to commit. The two-phase commit protocol attempts to ensure the ACID properties of transactions. The ACID properties are as follows: • • • • Atomicity—All the operations grouped under a transaction occur or none of them do. Atomicity is defined from the perspective of the consumer of the transaction. Consistency—The completion of a transaction must leave the system in a consistent state. If the transaction cannot achieve a stable end state, it must return the system to its initial state. Isolation—Transactions should not be affected by other transactions that execute concurrently. Participants in a transaction should see only intermediate states resulting from the operations of their own transaction, not the intermediate states of other transactions. Durability—The results of a transaction should be as persistent as the entity on which the transaction commits. Database management systems go to great lengths to ensure the ACID properties are enforced. As a result, the system overhead is generally proportional to the degree of transactional integrity. In other words, more system resources are required to ensure that reading, writing, and updating data are accurate in light of failures that might occur. Jini takes a more lightweight approach to transactions. Jini provides only the interface to transactions, and this interface closely resembles the two-phase commit protocol. By providing 125 only an object interface to transactions, Jini does not dictate the implementation. The notion of data consistency with regard to partial failure is promoted through the two-phase commit-based interface. But the key is the degree to which the implementation will try to ensure ACID properties despite a partial or catastrophic failure. What the Jini transaction service primarily provides is the coordination mechanism necessary for objects to agree on the transaction state. Per the Jini specification, the goal of the system is to provide the minimum set of protocols and interfaces that will enable objects to implement transaction semantics. As you expand your notion of distributed transactions to include a broader array of client devices, like cell phones, Palm Pilots, and mobile agents, you will require support for a more flexible and lightweight implementation. Jini provides an API that provides implementation flexibility. Lightweight Jini implementations will reduce the overhead and time required to deploy transactional systems. Perhaps like message-oriented middleware (MOM), which provides various levels of message assurance, the new breed of transactional systems will provide various levels of transactional integrity. Transaction Types There are two primary types of transactions—flat and nested. Flat transactions perform their work within the boundaries of a single begin-end work bracket. They tend to be relatively simple and short-lived. Flat transactions provide an excellent fit for modeling short activities, such as banking deposits and withdrawals. The work might be performed by many different servers and may be distributed across many different platforms. Jini comes with a reference implementation (mahalo) that supports flat transactions. Nested transactions occur at many levels, and involve a root transaction that contains leaves that also contain transactions, and thus form a tree of transactions. The transaction at the root of the tree is called the top-level transaction. Leaf transactions only commit if their parent transactions commit. The leaf transaction is able to see the uncommitted data changes in its parent transaction. Nested transactions can quickly become complex and require significant coordination. The Jini API supports nested transactions; however, the mahalo implementation does not, as seen in Figure 6.2. Figure 6.2. The Jini transaction service supports the notion of flat and nested transactions. The reference implementation mahalo, however, only provides support for flat transactions. 126 The Jini Transaction Framework The classes and interfaces defined by the Jini Transaction Specification are in the packages net.jini.core.transaction and net.jini.core.transaction.server. There are three primary roles defined in the framework: • • • Transaction client—Starts and commits or aborts the transaction. Transaction manager—Provides the overall transaction coordination and provides the leased transaction object that is passed to the participants of the transaction. Transaction participants—Any service which receives the transaction object from the client and implements the TransactionParticipant interface. The transaction object is defined as a parameter in any method the service exposes that requires transaction support. A transaction is created and overseen by a transaction manager. Each manager implements the interface TransactionManager. Each transaction is represented by a long identifier that is unique with respect to the transaction: • net.jini.core.transaction.server.TransactionManager— • net.jini.core.transaction.TransactionFactory— The factory • that creates a transaction object when supplied with a transaction manager. The transaction object represents the current transaction. net.jini.core.transaction.Transaction— The interface to the transaction object returned by the transaction factory. The interface supported by the transaction manager. Listing 6.1 TransactionManager Interface package net.jini.core.transaction.server; public interface TransactionManager extends Remote, TransactionConstants { public static class Created implements Serializable { public final long id; public final Lease lease; public Created(long id, Lease lease) { ...} } Created create(long leaseFor) throws LeaseDeniedException, RemoteException; void join(long id, TransactionParticipant part, long crashCount) throws UnknownTransactionException, CannotJoinException, CrashCountException, RemoteException; int getState(long id) throws UnknownTransactionException, RemoteException; void commit(long id) throws UnknownTransactionException, CannotCommitException, RemoteException; void commit(long id, long waitFor) throws UnknownTransactionException, 127 CannotCommitException, TimeoutExpiredException, RemoteException; void abort(long id) throws UnknownTransactionException, CannotAbortException, RemoteException; void abort(long id, long waitFor) throws UnknownTransactionException, CannotAbortException, TimeoutExpiredException, RemoteException; } The transaction client first finds a transaction manager. The following code fragment uses the supplied service registrar to look up the transaction manager on the network. public static TransactionManager findTM(ServiceRegistrar registrar) { TransactionManager tm = null; ServiceTemplate template; Class[] tmInterface = { TransactionManager.class } ; template = new ServiceTemplate(null, tmInterface, null); try { tm = (TransactionManager)registrar.lookup(template); } catch (Exception e) { e.printStackTrace(); } return tm; } The client asks the manager to create a transaction, typically by using a factory class such as TransactionFactory to create a transaction object. The transaction object created is then passed as a parameter when performing operations on a service. TransactionManager manager = findTM(registrar); Transaction.Created leasedTxn = TransactionFactory.create(manager, Lease.FOREVER); The returned Transaction.Created (an inner class) object holds the transaction object and the lease associated with the transaction. Transaction transaction = leasedTxn.transaction; Lease lease = leasedTxn.lease; The transaction interface is defined as follows: Listing 6.2 Transaction Interface package net.jini.core.transaction; public interface Transaction { public static class Created implements Serializable { public final Transaction transaction; public final Lease lease; Created(Transaction transaction, Lease lease) { ...} } void commit() throws UnknownTransactionException, CannotCommitException, RemoteException; void commit(long waitFor) throws UnknownTransactionException, CannotCommitException, 128 TimeoutExpiredException, RemoteException; void abort() throws UnknownTransactionException, CannotAbortException, RemoteException; void abort(long waitFor) throws UnknownTransactionException, CannotAbortException, TimeoutExpiredException, RemoteException; } The TransactionConstants interface defines constants used in the communication between managers and participants. Listing 6.3 TransactionConstants Interface package net.jini.core.transaction.server; public interface TransactionConstants { int ACTIVE = 1; int VOTING = 2; int PREPARED = 3; int NOTCHANGED = 4; int COMMITTED = 5; int ABORTED = 6; } These correspond to the states and votes that participants and managers go through during the lifecycle of a given transaction. For the client, the transaction starts out ACTIVE as soon as create returns. The client drives the transaction to completion by invoking commit or abort on the transaction manager; or by canceling the lease or letting the lease expire (both of which are equivalent to an abort). For the participant, the transaction starts out ACTIVE as soon as join returns. Any operations attempted under a transaction are valid only if the participant has the transaction in the ACTIVE state. In any other state, a request to perform an operation under the transaction should fail, signaling the invoker appropriately. A transaction completes when any entity either commits or aborts the transaction. If a transaction commits successfully, then all operations performed under that transaction will complete. Aborting a transaction means that all operations performed under that transaction will appear never to have happened. Committing a transaction requires each participant to vote; where a vote is either prepared (ready to commit), not changed (read-only), or aborted (the transaction should be aborted). If all participants vote prepared or not changed, the transaction manager will tell each prepared participant to roll forward, thus committing the changes. Participants that voted not changed need do nothing more. If the transaction is ever aborted, the participants are told to roll back any changes made under the transaction. In the manager's view, a transaction goes through the following states. When a transaction is created using create, the transaction is ACTIVE. This is the only state in which participants may join the transaction. Attempting to join the transaction in any other state throws a CannotJoinException. 129 Invoking the manager's commit method causes the manager to move to the VOTING state, in which it attempts to complete the transaction by rolling forward. Each participant that has joined the transaction has its prepare method invoked to vote on the outcome of the transaction. The participant may return one of three votes: NOTCHANGED, ABORTED, or COMMITTED. A timeout can also be specified in the abort and commit methods. If the transaction manager reaches a decision, but is unable to notify all participants of that decision before the specified timeout expires, then TimeoutExpiredException is thrown. If the specified timeout expires before the transaction manager reaches a decision, then TimeoutExpiredException is not thrown until the manager reaches a decision. It is the responsibility of the client to maintain the lease to the transaction object. If the lease is not maintained, the transaction manager will abort the transaction when the lease expires. The client passes the transaction object to services, which require transactions in method calls, such as the JavaSpaces API write method seen here: space.write(msg, transaction, 1000*10); The first time a client tells a participant to perform an operation under a given transaction, the participant must invoke the transaction manager's join method with an object that implements the TransactionParticipant interface. This object will be used by the manager to communicate with the participant about the transaction. public void someMethod(Transaction txn) throws RemoteException, CannotJoinException, UnknownTransactionException, CrashCountException { // Cast to ServerTransaction (flat transaction) ServerTransaction serverTxn = (ServerTransaction)txn; // join the transaction providing a TransactionParticipant and crash count serverTxn.join(this, crashCount); // continue processing If the participant's invocation of the join method throws RemoteException, the participant should not perform the operation requested by the client, and should either re-throw the exception or signal failure to the client. The TransactionParticipant interface must be implemented by the object referenced in the first parameter of the join method. Listing 6.4 TransactionParticipant Interface package net.jini.core.transaction.server; public interface TransactionParticipant extends Remote, TransactionConstants { int prepare(TransactionManager mgr, long id) throws UnknownTransactionException, RemoteException; 130 void commit(TransactionManager mgr, long id) throws UnknownTransactionException, RemoteException; void abort(TransactionManager mgr, long id) throws UnknownTransactionException, RemoteException; int prepareAndCommit(TransactionManager mgr, long id) throws UnknownTransactionException, RemoteException; } The TransactionParticipant interface provides the methods that support the twophase commit protocol. The join method's second parameter is a crash count that uniquely defines the version of the participant's storage that holds the state of the transaction. Each time the participant loses the state of that storage (because of a system crash if the storage is volatile, for example) it must increase the crash count by one. For example, the participant could store the crash count in stable storage. When the participant is restarted, retrieve the crash count and increase it by one. When a manager receives a join request, it checks to see whether the participant has already joined the transaction. If it has, and the crash count is the same as the one specified in the original join, the join is accepted but is otherwise ignored. If the crash count is different, the manager throws CrashCountException and forces the transaction to abort. The Mahalo Implementation Sun provides a reference implementation of the Jini Transaction Service called mahalo. You run mahalo as an activatable service. An activatable service is one that registers with rmid and restarts when rmid is activated. The startMahalo.cmd script can be used to start the mahalo service. Of course, you can also use the service starter GUI that was introduced in Chapter 3, "Supporting Technologies." See Appendix B "Tools," for more information on starting services. Transactions in JavaSpace In the last chapter we developed applications using JavaSpaces. We always used a null transaction as the second parameter to the methods defined, which assumes the operation consists of one indivisible action. As soon as the operation completes, the entry is visible to all clients of the space. On the other hand, when we write an entry under a non-null transaction, the entry is not accessible to operations outside of the transaction until the transaction commits. If the transaction commits, then all the entries written under the transaction become visible to the entire space. However, if the transaction aborts, the entries written under the transaction are removed. In effect, after a transaction aborts, the space reflects that the operations never occurred. When using distributed data structures such as distributed arrays, it is important that transactional support is enabled. Recall that you use an index and a tail entry to provide sequence support for distributed data structures. If you take the tail entry, you remove it from space. What happens if you increment the index value and then fail on the write method? In effect you have lost the data structure. Another process would not find the tail, and would create a new index and data structure. You would lose your current entries or, depending on the application, logic could retrieve the entries in erroneous situations. 131 Transactions enable you to group the take and write methods under a single operation. If the transaction aborts, any entries taken under the transaction are returned to the space (and of course, any entries written under it are removed), leaving the space as if the operations never occurred. The following code fragment demonstrates writing entries to JavaSpaces using a transaction manager. TransactionManager manager = findTM(registrar); Transaction.Created leasedTxn = TransactionFactory.create(manager, Lease.FOREVER); Lease lease = leasedTxn.lease; Transaction transaction = leasedTxn.transaction; Tail template = new Tail(); template.name = "MessageArray"; // perform space operations under a single transaction // if error occurs all operations reversed try { Tail tail = space.takeIfExists(template, transaction, JavaSpace.NO_WAIT); if(tail == null) tail = template; Message msg = new Message(); msg.text = "Testing transactions in space"; for(int i=0; i<100; i++) { System.out.println("Writing message "+i); space.write(msg,transaction,Lease.FOREVER); tail.increment(); } System.out.println("Writing tail "); space.write(tail, transaction, Lease.FOREVER); System.out.println("Commit transaction"); txn.commit(); } catch (...) JavaSpace Notifications If you register for a notify under a transaction, you receive notifications of entries that are written within the transaction and to the space. When the transaction completes (whether it commits or aborts), all notification registrations under the transaction are withdrawn. If the transaction commits, the entries remaining in the transaction might result in notifications as a response to registrations in the space. The entries also become eligible for read and take operations from the space. JavaSpaces Transaction Example Let's begin this example by building a utility class for service lookup. I'll expand on this utility in the next chapter when we discuss helper services. For now, you have two services that you need to find and access: JavaSpaces (outrigger), and the transaction service (mahalo). Our ServiceFinder utility takes an LUS as a parameter to the methods findSpace and findTM. Next you create a template that will match any service supporting the interface— JavaSpace and TransactionManager. Listing 6.5 ServiceFinder Class 132 package org.jworkplace.util; import net.jini.core.lookup.ServiceTemplate; import net.jini.core.lookup.ServiceRegistrar; import net.jini.core.transaction.server.TransactionManager; import net.jini.space.JavaSpace; public class ServiceFinder { public static JavaSpace findSpace(ServiceRegistrar registrar) { JavaSpace space = null; ServiceTemplate template; Class[] spaceInterface = { JavaSpace.class } ; template = new ServiceTemplate(null, spaceInterface, null); try { space = (JavaSpace)registrar.lookup(template); } catch (Exception e) { e.printStackTrace(); } return space; } public static TransactionManager findTM(ServiceRegistrar registrar) { TransactionManager txm = null; ServiceTemplate template; Class[] transactionManagerInterface = { TransactionManager.class } ; template = new ServiceTemplate(null, transactionManagerInterface, null); try { txm = (TransactionManager)registrar.lookup(template); } catch (Exception e) { e.printStackTrace(); } return txm; } } The TxnClient class demonstrates how to create a transaction and pass it as a parameter to space operations. Listing 6.6 The TxnClient Class package org.jworkplace.client; import java.io.IOException; import java.io.Serializable; import java.util.*; import import import import import import net.jini.core.lease.*; net.jini.core.lookup.ServiceItem; net.jini.core.lookup.ServiceTemplate; net.jini.core.lookup.ServiceMatches; net.jini.core.lookup.ServiceRegistrar; net.jini.core.discovery.LookupLocator; import java.rmi.RemoteException; import java.rmi.RMISecurityManager; import java.rmi.server.UnicastRemoteObject; 133 import import import import import import import import import import net.jini.core.transaction.Transaction; net.jini.core.transaction.TransactionFactory; net.jini.core.transaction.Transaction.Created; net.jini.core.transaction.server.ServerTransaction; net.jini.core.transaction.server.TransactionConstants; net.jini.core.transaction.server.TransactionManager.Created; net.jini.core.transaction.server.TransactionManager; net.jini.core.transaction.server.TransactionParticipant; net.jini.core.transaction.UnknownTransactionException; net.jini.core.transaction.CannotAbortException; import import import import net.jini.discovery.DiscoveryEvent; net.jini.discovery.DiscoveryListener; net.jini.discovery.DiscoveryManagement; net.jini.discovery.LookupDiscovery; import net.jini.space.JavaSpace; import org.jworkplace.util.ServiceFinder; import org.jworkplace.Message; public class TxnClient { private private private private private JavaSpace space; TransactionManager manager; Transaction.Created leasedTxn; Transaction txn; Lease lease; public TxnClient(ServiceRegistrar registrar){ // use our ServiceFinder to access outrigger and mahalo manager = ServiceFinder.findTM(registrar); space = ServiceFinder.findSpace(registrar); try { // pass the transaction manager to the factory to create a // leased Transaction.Created leasedTxn = TransactionFactory.create(manager, Lease.FOREVER); // get the lease and transaction object lease = leasedTxn.lease; txn = leasedTxn.transaction; // set the lease period for 10 seconds lease.renew(1000*10); } } } catch (LeaseDeniedException leaseDenied) { leaseDenied.printStackTrace(); System.exit(1); catch (UnknownLeaseException unknownLease) { unknownLease.printStackTrace(); System.exit(1); catch (RemoteException remoteException) { remoteException.printStackTrace(); System.exit(1); } // create a Message entry with some text 134 Message msg = new Message(); msg.text = "Testing transactions in space"; try { // write the entry 100 times passing the transaction object for(int i=0; i<100; i++) { System.out.println("Writing to space message " + i); space.write(msg,txn,1000*10); System.out.println( "Lease remaining (millis): " + (lease.getExpiration() System.currentTimeMillis())); } // commit the transaction to make it visible to other processes System.out.println("Commit transaction"); txn.commit(); } catch (Exception e) { System.err.println(e.getMessage()); e.printStackTrace(); try { txn.abort(); } catch (UnknownTransactionException unknown) { unknown.printStackTrace(); } catch (CannotAbortException cannotAbort) { cannotAbort.printStackTrace(); } catch (RemoteException remoteException) { remoteException.printStackTrace(); } } } public static void main(String args[]) throws Exception { if(args.length < 1) { System.out.println("usage jini://hostname:port"); System.exit(1); } // set the security manager if (System.getSecurityManager() == null) { System.setSecurityManager(new RMISecurityManager()); } // create a LookupLocator for unicast discovery LookupLocator lookup = new LookupLocator(args[0]); // find the LUS ServiceRegistrar registrar = lookup.getRegistrar(); if(registrar != null) new TxnClient(registrar); } } Compile the Files 135 To build the example, change to the Chapter 6 Example 1 directory and invoke the compile command: C:\JiniJavaSpaces\chapter6\example1\compile The mahalo.jar file contains the transaction support classes for Jini. The compile.cmd script includes this jar file on the classpath. Start the TxnClient Ensure that both the mahalo transaction service and the outrigger JavaSpaces service are running. Then invoke the startClient.cmd from the Chapter 6 Example 1 directory. You must set the first parameter on the command line to the hostname and port of the running lookup service. C:\JiniJavaSpaces\chapter6\example1\startClient jini://hostname:port Here you simply write 100 messages within a single transaction. You might want to change the lease time to a low value on the transaction. For instance, the line lease.renew(1000*10); is changed to lease.renew(1000*2); Unless the transaction commits in less than 2 seconds, an exception is thrown when JavaSpaces tries to join the transaction. The transaction object does not have a lease manager renewing its lease, and therefore the transaction manager aborts the transaction. All entries written to the space are removed. Transactional Shell Example The JavaSpaces service is a transaction participant, because it implements the TransactionParticipant interface and takes a Transaction object as a parameter in methods exposed by its interface. In Chapter 4, "Building on Jini Foundation Concepts," we built a JiniShell that enabled us to enter commands and have them run remotely. We can extend that concept to create a transactional shell. In other words, create a shell that is capable of participating in transactions, and therefore affect the outcome of the command's success or failure by voting to commit or abort the transaction. Figure 6.3 depicts the message exchange required to establish a transaction and participate in the two-phase commit protocol. Figure 6.3. This scenario diagram outlines the messages exchanged between participants within the transactional shell example. 136 You extend the Shell interface to support an executeCommand method that takes a Transaction object as a parameter. Listing 6.7 TxnShell Interface package org.jworkplace.command; import java.rmi.RemoteException; import net.jini.core.transaction.Transaction; import net.jini.core.transaction.TransactionException; public interface TxnShell extends Shell { public Object executeCommand(Command cmd, Transaction txn) throws RemoteException, TransactionException; } We now define a ParticipantShell service that extends UnicastRemoteObject and implements TxnShell and TransactionParticipant. Listing 6.8 ParticipantShell Class package org.jworkplace.client; import java.io.IOException; import java.io.Serializable; import java.util.Vector; import import import import import import import java.rmi.RemoteException; java.rmi.RMISecurityManager; java.rmi.server.UnicastRemoteObject; net.jini.core.transaction.Transaction; net.jini.core.transaction.TransactionFactory; net.jini.core.transaction.Transaction.Created; net.jini.core.transaction.TransactionException; import import import import net.jini.core.transaction.server.ServerTransaction; net.jini.core.transaction.server.TransactionConstants; net.jini.core.transaction.server.TransactionManager.Created; net.jini.core.transaction.server.TransactionManager; 137 import import import import import net.jini.core.transaction.server.TransactionParticipant; net.jini.core.transaction.server.CrashCountException; net.jini.core.transaction.UnknownTransactionException; net.jini.core.transaction.CannotAbortException; net.jini.core.transaction.CannotJoinException; import org.jworkplace.command.*; public class ParticipantShell extends UnicastRemoteObject implements TxnShell, TransactionParticipant { private int crashCount = 0; public ParticipantShell() throws RemoteException { super(); // recover log file (crash count and any state needed to recover) // take snapshot which would log our crash count to safe storage } public Object executeCommand(Command cmd) throws RemoteException { return cmd.execute(); } public Object executeCommand(Command cmd, Transaction txn) throws RemoteException, CannotJoinException, UnknownTransactionException, CrashCountException { // We join the transaction in this method. This assumes every // request creates a new transaction. if(txn != null) { ServerTransaction serverTxn = (ServerTransaction)txn; serverTxn.join(this, crashCount); } return cmd.execute(); } // Implementation of the TransactionParticipant interface public void abort(TransactionManager mgr, long transactionID) throws UnknownTransactionException, RemoteException { System.out.println( "abort" ); // rollback any actions here // write log } public void commit(TransactionManager mgr, long transactionID) throws UnknownTransactionException, RemoteException { System.out.println( "commit" ); // roll forward any actions here // write log } public int prepare(TransactionManager mgr, long transactionID) 138 throws UnknownTransactionException, RemoteException { System.out.println( "prepare" ); return PREPARED; // take snapshot here } public int prepareAndCommit(TransactionManager mgr, long transactionID) throws UnknownTransactionException, RemoteException { System.out.println( "prepare & commit" ); // take snapshot here int result = prepare( mgr, transactionID ); if ( result == PREPARED ) { commit( mgr, transactionID ); result = COMMITTED; // roll forward any actions here // write log } return result; } } The Participant class is used as a service helper to provide general Jini housekeeping, such as discovery and join processing for the TxnShell. Listing 6.9 Participant Class package org.jworkplace.client; import java.io.IOException; import java.io.Serializable; import java.util.Vector; import java.rmi.RemoteException; import java.rmi.RMISecurityManager; import import import import import net.jini.core.lease.*; net.jini.core.lookup.ServiceItem; net.jini.core.lookup.ServiceTemplate; net.jini.core.lookup.ServiceMatches; net.jini.core.lookup.ServiceRegistrar; import import import import import import import import import import import net.jini.discovery.DiscoveryEvent; net.jini.discovery.DiscoveryListener; net.jini.discovery.DiscoveryManagement; net.jini.discovery.LookupDiscovery; net.jini.discovery.DiscoveryGroupManagement; net.jini.discovery.LookupDiscoveryManager; net.jini.core.discovery.LookupLocator; net.jini.core.lookup.ServiceID; net.jini.lookup.JoinManager; net.jini.lookup.ServiceIDListener; net.jini.space.JavaSpace; import org.jworkplace.util.SpaceAccessor; import org.jworkplace.util.ServiceFinder; import org.jworkplace.command.*; 139 public class Participant { // The LookupDiscoveryManager we are using to find lookups private LookupDiscoveryManager lookupDiscMgr; private LookupLocator locators[]; private String groups[] = DiscoveryGroupManagement.ALL_GROUPS; /** Manager for joining lookup services */ private JoinManager joiner = null; /** ServiceID returned from the lookup registration process */ private ServiceID serviceID = null; public Participant(String[] args) throws IOException { init(); } private void init() throws IOException { try { lookupDiscMgr = new LookupDiscoveryManager(groups, locators, null); } catch (IOException e) { System.err.println("JoinState:Problem starting discovery"); e.printStackTrace(); throw new IOException("JoinState: Problem starting discovery:" + e.getLocalizedMessage()); } // This is the object we register with the lookup service TxnShell shell = new ParticipantShell(); /* Register this service with any configured lookup services */ if (serviceID == null) { // First instance ... need service id joiner = new JoinManager( shell, null, new SrvcIDListener(), lookupDiscMgr, // service object // service attributes // ServiceIDListener // DiscoveryManagement - null); // LeaseRenewalManager - default default } else { // Rejoin with (recovered) state information joiner = new JoinManager( shell, // service object null, // service attributes serviceID, // Service ID lookupDiscMgr, // DiscoveryManagement default null); // LeaseRenewalManager - default } } 140 public static void main(String args[]) throws Exception { if (System.getSecurityManager() == null) { System.setSecurityManager(new RMISecurityManager()); } try { new Participant(args); } catch(IOException e) { e.printStackTrace(); System.exit(1); } try { Thread.sleep(100000L); } catch(InterruptedException e) { } System.exit(0); } /** * Utility method for setting the service's ID obtained from * the lookup registration process. */ private void setServiceID(ServiceID id) { serviceID = id; } /** * Class which handles the callback of a service ID assignment * by the JoinManager */ private class SrvcIDListener implements ServiceIDListener { public SrvcIDListener() { super(); } /** * The JoinManager will invoke this method when it receives a * valid ServiceID from a lookup service. */ public void serviceIDNotify(ServiceID id) { // Set the ID setServiceID(id); System.out.println("Received service id"); // Log this event } } //end class SrvcIDListener } The transaction client can also be a participant by implementing the TransactionParticipant interface, and joining the transaction. Listing 6.10 TxnClient Class package org.jworkplace.client; import java.io.IOException; 141 import java.io.Serializable; import java.util.Vector; import import import import import import net.jini.core.lease.*; net.jini.core.lookup.ServiceItem; net.jini.core.lookup.ServiceTemplate; net.jini.core.lookup.ServiceMatches; net.jini.core.lookup.ServiceRegistrar; net.jini.core.discovery.LookupLocator; import java.rmi.RemoteException; import java.rmi.RMISecurityManager; import java.rmi.server.UnicastRemoteObject; import import import import import import import import import import net.jini.core.transaction.Transaction; net.jini.core.transaction.TransactionFactory; net.jini.core.transaction.Transaction.Created; net.jini.core.transaction.server.ServerTransaction; net.jini.core.transaction.server.TransactionConstants; net.jini.core.transaction.server.TransactionManager.Created; net.jini.core.transaction.server.TransactionManager; net.jini.core.transaction.server.TransactionParticipant; net.jini.core.transaction.UnknownTransactionException; net.jini.core.transaction.CannotAbortException; import import import import import net.jini.space.JavaSpace; org.jworkplace.util.ServiceFinder; org.jworkplace.Message; org.jworkplace.command.TxnShell; org.jworkplace.client.FileList; public class TxnClient extends Thread implements TransactionParticipant { private private private private private private private TransactionManager manager; Transaction.Created leasedTxn; Transaction txn; Lease lease; static String directory; ServiceRegistrar registrar; JavaSpace space; public TxnClient(ServiceRegistrar registrar) { this.registrar = registrar; } public static void main(String args[]) throws Exception { if (System.getSecurityManager() == null) { System.setSecurityManager(new RMISecurityManager()); } LookupLocator lookup = new LookupLocator(args[0]); ServiceRegistrar registrar = lookup.getRegistrar(); directory = args[1]; new TxnClient(registrar).start(); } public void run() { 142 manager = ServiceFinder.findTM(registrar); space = ServiceFinder.findSpace(registrar); try { leasedTxn = TransactionFactory.create(manager, Lease.FOREVER); lease = leasedTxn.lease; txn = leasedTxn.transaction; System.out.println( "Lease duration (millis): " + (lease.getExpiration() System.currentTimeMillis())); lease.renew(1000*10); System.out.println( "Lease duration (millis): " + (lease.getExpiration() System.currentTimeMillis())); } } } catch (LeaseDeniedException leaseDenied) { leaseDenied.printStackTrace(); System.exit(1); catch (UnknownLeaseException unknownLease) { unknownLease.printStackTrace(); System.exit(1); catch (RemoteException remoteException) { remoteException.printStackTrace(); System.exit(1); } // Export our client to participate in the transaction try { UnicastRemoteObject.exportObject(this); } catch (RemoteException remoteException) { remoteException.printStackTrace(); System.exit(1); } try { // cast the transaction object to ServerTransaction ServerTransaction st = (ServerTransaction)txn; // join the transaction st.join( this, 0 ); TxnShell shell = null; Class[] shelltypes = { TxnShell.class } ; ServiceTemplate template = new ServiceTemplate(null, shelltypes, null); // Find the TxnShell shell = (TxnShell)registrar.lookup(template); // invoke the command passing the transaction object String[] files = (String [])shell.executeCommand( new FileList(directory), txn); if(files == null) { System.out.println("Invalid directory"); } else { for(int i=0; i<files.length; i++) System.out.println(files[i]); } // write a message to space using the transaction 143 Message msg = new Message(); msg.text = directory; space.write(msg,txn,1000*10); // now commit it all txn.commit(); } catch (Exception e) { System.err.println(e.getMessage()); e.printStackTrace(); try { txn.abort(); } catch (UnknownTransactionException unknown) { unknown.printStackTrace(); } catch (CannotAbortException cannotAbort) { cannotAbort.printStackTrace(); } catch (RemoteException remoteException) { remoteException.printStackTrace(); } } } // if an error occurs this method is called public void abort(TransactionManager mgr, long transactionID) throws UnknownTransactionException, RemoteException { System.out.println( "-------abort--------" ); } // now roll forward the transaction public void commit(TransactionManager mgr, long transactionID) throws UnknownTransactionException, RemoteException { System.out.println( "---------commit-------" ); } // save any state information required public int prepare(TransactionManager mgr, long transactionID) throws UnknownTransactionException, RemoteException { System.out.println( "--------prepare--------" ); return PREPARED; } // if the only participant in the transaction public int prepareAndCommit(TransactionManager mgr, long transactionID) throws UnknownTransactionException, RemoteException { System.out.println( "---------prepare & commit---------" ); int result = prepare( mgr, transactionID ); if ( result == PREPARED ) { commit( mgr, transactionID ); result = COMMITTED; } return result; } } Compile the Files To build the second example in this chapter, change to the Chapter 6 Example 2 directory and invoke the build command: 144 C:\JiniJavaSpaces\chapter6\example2\build Create the Required Stub Interfaces This will compile the files and invoke the rmic compiler to generate the stub files for the TxnShell class and the ParticipantShell class. Create Jar Files (Client-Side and Server-Side) It also creates the client-side and server-side jar files. Copy the client -dl.jar file to the client HTTP directory. Copy the service -dl.jar file to the server HTTP directory. Start the ParticipantShell Go to the service directory. Start the ParticipantShell command processor by invoking the participant script. C:\JiniJavaSpaces\Chapter6\example2\service>startParticipant Start the TxnClient To test the transaction client, move to the directory and machine where you deployed the client software. Run the following client command: C:\JiniJavaSpaces\Chapter6\example2\client>startClient jini://hostname directory where hostname is the host and port (default to 4160) running the LUS where the space is registered; and directory is the remote directory location of the files to list. Figure 6.4 demonstrates the ParticipantShell and TxnClient invoking a FileList command. Figure 6.4. You should see the prepare and commit messages in both windows. 145 Both the ParticipantShell and TxnClient receive the appropriate prepare and commit messages in support of the two-phase commit protocol. It is in these methods that services would store and restore the necessary application state to recover from failures or roll forward changes to provide the appropriate ACID property guarantees. As you build applications in Part III, you will log the necessary state information such as crash count and service ID to make the applications more resilient and production-ready. Summary Transactional systems let you group together a set of operations so that either all the operations complete or none of them do. Jini supports the two-phase commit protocol, which is a standard protocol for coordinating participants in a distributed transaction. While Jini defines a standard transaction interface, it leaves the implementation to the service provider. There is, however, a sample implementation called mahalo provided by Sun Microsystems. The JavaSpaces service requires the transaction service. The first example in this chapter demonstrated how to coordinate JavaSpace operations under the control of a transaction. The next example expounded on the concepts of a remote Shell that were introduced in Chapter 4. It demonstrates the two-phase commit protocol, by implementing the Participant interface. Jini's support for transactions provides a key component to building more robust applications and services. 146 Chapter 7. The Helper Services IN THIS CHAPTER • • • • • • Overview of Helper Services The Disconnected Client or Service Entity The LDS Interface Explained The LRS Interface Explained The EMS Interface Explained Summary This chapter provides an overview of the Jini 1.1 Helper services and how they can be used to complement core and user-defined services. Overview of Helper Services To help simplify the process of developing clients and services for the Jini environment, several helper services have been defined. The Jini specifications define two categories of helper entities: helper utilities and helper services. These utilities and services provide a basis for building applications that demonstrate desirable behavior in a Jini community. Helper utilities are programming components that can be used during the construction of Jini services and/or clients. Helper utilities are not remote services and do not register with a lookup service (LUS). Helper utilities are instantiated locally by entities wishing to employ them. Helper utilities include the following: • • • • LookupDiscovery LeaseRenewalManager JoinManager ServiceDiscoveryManager A helper service is a Jini service that can be registered with any number of lookup services and whose methods can execute on remote hosts. In general, a helper service should be of use to more than one type of entity participating in the Jini application environment, and should provide a significant reduction in development complexity for developers of such entities. A helper service consists of an interface or set of interfaces and an associated implementation encapsulating behavior that is either required or highly desirable in services that adhere to the Jini technology programming model. Helper services include the following: • • • Lookup Discovery Service Lease Renewal Service Event Mailbox Service Lookup Discovery Service The Lookup Discovery Service (LDS) is a helper service that uses the Jini discovery protocols to find LUSes in which an entity (a client or service) has expressed interest, and to notify the entity when a previously unavailable LUS becomes available (see Figure 7.1). 147 Figure 7.1. The LDS monitors LUS changes in the Jini community and notifies entities that have registered for notifications. Why would an entity want to delegate this responsibility? An activatable service that deactivates may want to employ the LDS to perform discovery duties on the entity's behalf. Such an entity may want to deactivate for various reasons, one being to conserve resources. While the entity is deactivated, the LDS running on the same or a separate host would employ the discovery protocols to find LUSes in which the entity has expressed interest, and would notify the entity when a previously unavailable LUS becomes available, when the entity has reactivated again. Lease Renewal Service The Lease Renewal Service (LRS) is a helper service that can be employed by both Jini clients and services to perform all lease renewal duties on their behalf. An entity would delegate this responsibility so that services wishing to remain inactive until they are needed can request that the lease renewal service take on the responsibility of renewing the leases granted to the service (see Figure 7.2). This enables the service to be dormant without risking the loss of access to the resources corresponding to the leases being renewed. Figure 7.2. The LRS can provide lease management for disconnected clients. 148 Entities that have continuous access to a network but cannot be continuously connected to that network (for example, a cell phone), might also find this service useful. By enabling an LRS (which can be continuously connected) to renew the leases on the resources acquired by the entity, the entity may remain disconnected until needed. This lease renewal service removes the need to perform the discovery and lookup process each time the entity reconnects to the network, potentially resulting in a significant increase in efficiency. Event Mailbox Service The Event Mailbox Service (EMS) is a helper service that can be employed by entities to store event notifications on their behalf. When an entity registers with the event mailbox service, that service will collect events intended for the registered entity until the entity initiates delivery of the events (see Figure 7.3). Figure 7.3. The EMS can ensure that events targeted at a disconnected client are available when the client reactivates. The EMS can provide store-and-forward semantics. A service such as the EMS can be useful to entities that desire more control over the delivery of the events sent to them. Some entities operating in a distributed system may find it undesirable or 149 inefficient to be contacted solely for the purpose of having an event delivered, preferring to defer the delivery to a time that is more convenient as determined by the entity itself. For example, an entity wishing to deactivate or detach from a network may wish to have its events stored until it is available to retrieve them. Additionally, some entities may want to batch process event notifications for efficiency. The Disconnected Client or Service Entity The general theme in the usage patterns mentioned previously is support for disconnected entities, and entities or environments where computational conservation is of paramount importance. Delegating Responsibilities By using helper services, entities can disconnect from the network and still have their Jini "good citizenship" behavior managed. For clients, this might involve renewing leases to services or storing events for later consumption. For services, the responsibilities also include LUS residency to maintain a persistent state across service restarts and crashes. This persistent state consists of the following: • • • • The service must use the service ID obtained from the first LUS it registered with across all subsequent registrations. Attributes associated with the service in LUSes. These attributes must be consistent for all LUS entries. Groups the service wishes to participate in. Specific LUSes to register with. When a service deactivates, this information needs to be saved to persistent storage. Likewise, when a service activates, it must be retrieved to initialize the service. Other pieces of information specific to a service implementation might also need to be saved. Resource Conservation The helper services also provide entities with an environment that can reduce the overall resource requirements. If many fine-grained services must run concurrently, sharing the services offered by the helper services can be advantageous. Also, if the physical limitations of the device, such as memory, constrain the supporting environment, delegating responsibility to helper services on the network can reduce the demands on the resource-constrained device. Let's look at the helper services in more detail. The LDS Interface Explained The LookupDiscoveryService interface defines the lookup discovery helper service. Through that interface, Jini services and Jini clients may request that the discovery process be performed on their behalf. Listing 7.1 The LookupDiscoveryService Interface 150 public interface LookupDiscoveryService { public abstract LookupDiscoveryRegistration register(String[] groups, LookupLocator[] locators, RemoteEventListener listener, MarshalledObject handback, long leaseDuration) throws RemoteException; } Clients find the discovery service by creating a ServiceTemplate using the LookupDiscovery Service interface as a parameter. public LookupDiscoveryService findLDS(ServiceRegistrar registrar) { LookupDiscoveryService lookupService; ServiceTemplate template; // set the interface class Class[] lookupDiscoveryServiceInterface = { LookupDiscoveryService.class } ; // create the template template = new ServiceTemplate(null, lookupDiscoveryServiceInterface, null); // use the already found LUS to find the LDS try { lookupService = (LookupDiscoveryService)registrar.lookup(template); } catch (Exception e) { e.printStackTrace(); } return lookupService; } The LookupDiscoveryService interface defines a single register method. An entity uses the interface to define the LUSes of interest to the entity. // The lookup registration returned by the LDS LookupDiscoveryRegistration lookupRegistration; // The registration handback information MarshalledObject data = handBackData; // The lease on the LDS registration long leaseTime = 1000*60*5; // 5 minutes // Discover all groups using no specific locators lookupRegistration = lookupService.register(LookupDiscovery.ALL_GROUPS, new LookupLocator[0], new LDSListener(), data, leaseTime); 151 The register method also requires: • • • A RemoteEventListener that will be notified when the discovery service adds or removes an LUS of interest, for example LDSListener. A MarshalledObject that will be available to the using entity through event notification, for example data. A requested lease time to indicate how long the LDS should remain active for the using entity, for example leaseTime. The using entity receives a LookupDiscoveryRegistration interface to manage the registration with LookupDiscoveryService. With the LookupDiscoveryRegistration interface, an entity can configure its registration requirements with the discovery service. For instance, you can change and remove the groups and locators of interest in your LUS view: Listing 7.2 The LookupDiscoveryRegistration Interface package net.jini.discovery; public interface LookupDiscoveryRegistration { public EventRegistration getEventRegistration(); public Lease getLease(); public ServiceRegistrar[] getRegistrars() throws LookupUnmarshalException, RemoteException; public String[] getGroups() throws RemoteException; public LookupLocator[] getLocators() throws RemoteException; public void addGroups(String[] groups) throws RemoteException; public void setGroups(String[] groups) throws RemoteException; public void removeGroups(String[] groups) throws RemoteException; public void addLocators(LookupLocator[] locators) throws RemoteException; public void setLocators(LookupLocator[] locators) throws RemoteException; public void removeLocators(LookupLocator[] locators) throws RemoteException; public void discard(ServiceRegistrar registrar) throws RemoteException; } When the LDS discovers or discards an LUS matching the criteria established through one of its registrations, the LDS sends an instance of the RemoteDiscoveryEvent class to the RemoteEventListener implemented by the entity and registered with the LDS. Listing 7.3 The RemoteDiscoveryEvent Class package net.jini.discovery; public class RemoteDiscoveryEvent extends RemoteEvent { public RemoteDiscoveryEvent(Object source, long eventID, long seqNum, MarshalledObject handback, 152 boolean discarded, Map groups) throws IOException { ...} public boolean isDiscarded() { ...} public ServiceRegistrar[] getRegistrars() throws LookupUnmarshalException { ...} public Map getGroups() { ...} } The RemoteDiscoveryEvent class provides an encapsulation of event information that the LDS uses to notify a registration of the occurrence of an event involving one or more Service Registrar objects (LUSes) in which the entity has registered interest. The LDS passes an instance of this class to the registration's discovery listener when one of the following events occurs: • Each LUS referenced in the event has been discovered for the first time, or has been rediscovered after having been discarded. Each LUS referenced in the event has been either actively or passively discarded. • RemoteDiscoveryEvent is a subclass of RemoteEvent, adding the following additional items: • A boolean indicating whether the LUSes referenced by the event have been discovered or discarded. A set of marshalled instances of the ServiceRegistrar interface representing proxies of the discovered or discarded LUSes referenced by the event. A Map instance in which the elements of the map's key set are the instances of ServiceID that correspond to each LUS reference returned in the event; and in which the map's value set contains the corresponding member groups of each LUS reference. • • Methods are defined through which this additional state can be retrieved upon receipt of an instance of this class. The following example provides a template LDS listener. Listing 7.4 The LDSListener Class package org.jworkplace.utils; import import import import import import import import import java.rmi.MarshalledObject; java.rmi.server.UnicastRemoteObject; java.rmi.RemoteException; net.jini.core.lookup.ServiceRegistrar; net.jini.core.event.RemoteEventListener; net.jini.core.event.RemoteEvent; net.jini.core.event.UnknownEventException; net.jini.discovery.RemoteDiscoveryEvent; net.jini.discovery.LookupUnmarshalException; public class LDSListener extends UnicastRemoteObject implements RemoteEventListener { public LDSListener() throws RemoteException { super(); } public void notify(RemoteEvent event)throws UnknownEventException, 153 RemoteException { // The LDS generates a RemoteDiscoveryEvent RemoteDiscoveryEvent evt = (RemoteDiscoveryEvent)event; // Appropriate logic could be inserted here // Just dump some event info to the console try { // The ID is unique per instance of LDS System.out.println("ID: " + evt.getID()); // The sequence number can be used to determine gaps // or missing notifications, the sequence number increments System.out.println("Sequence #: " + evt.getSequenceNumber()); // a handback object can be provided at registration // that will be returned unmodified MarshalledObject object = event.getRegistrationObject(); if(object != null) { System.out.println("LDS registration object: " + object.get()); } else { System.out.println("No LDS registration object set"); } // isDiscarded can by used to determine if this is a // discovered or discarded LUS notification System.out.println("Discovered : "+ evt.isDiscarded()); if (! evt.isDiscarded()) { ServiceRegistrar[] registrars = null; try { // get the registrars that have been discovered registrars = evt.getRegistrars(); } catch(LookupUnmarshalException e) { e.printStackTrace(); return; } // Simply list the discovered LUSs for (int i = 0; i < registrars.length; i++) { System.out.println(registrars[i]); } } } catch (Exception e) { e.printStackTrace(); } } } This listener extends UnicastRemoteObject, which means that the listener must be manually started and remain active to receive remote event notification. This might not provide the most robust alternative for listener notification. You will develop a framework for service management later in this chapter that will build on activatable services. The benefit of the framework is that services can rely on the activation mechanism of RMI and only be active when required, for example when a notification needs to be processed. Building the environment in this manner will provide a more robust environment, and help to reduce the overall resource requirements. 154 The Fiddler Implementation Sun Microsystems provides a reference implementation of the Jini LDS called fiddler. You can run fiddler as an activatable service or as a transient service. The version you use will depend on your requirements for persistence. In other words, will you require discovery service state to survive restarts or system crashes? Of course, the persistent or activatable service version will also require more system resources—nothing is free! Starting Fiddler The following script can be used to start the activatable version of fiddler: Listing 7.5 The startFidler.cmd Script @echo off @call setup.cmd set GROUPS=public echo echo echo echo echo echo echo echo Jini and JavaSpaces Application Development ------------------------------------------Jini install directory %JINI_HOME% Examples install directory %JWORK_HOME% Web server %HTTP_ADDRESS% Default group %GROUPS% ------------------------------------------Starting the fiddler lookup discovery service... java -jar Djava.security.policy=%JINI_HOME%\example\lookup\policy.all %JINI_HOME%\lib\fiddler.jar http://%HTTP_ADDRESS%/fiddler-dl.jar %JINI_HOME%\example\lookup\policy.all %JWORK_HOME%\services\logs\fiddler_log %GROUPS% And the following command can be used to start the transient version of fiddler: java -cp %JINI_HOME%\lib\fiddler.jar Djava.rmi.server.codebase=http:\\hostname:8080/ fiddler-dl.jar -Djava.security.policy=%JINI_HOME%\policy\policy.all com.sun.jini.fiddler.TransientFiddler %JWORK_HOME%\services\logs\transient-fiddler_log public The discovery facility of the LDS, together with its event mechanism, are the resources that entities can register to use. Because the resources of the LDS are leased, access is granted for only a limited period of time, unless the lease is extended . So, the astute reader will determine that if an entity disconnects, it will still need to renew its lease with the LDS. The LRS Interface Explained According to the Jini Lease Renewal Service Specification (see Appendix C), 155 The client of a leased service may run into difficulties if that service deactivates. Unless the client ensures that some other process renews the client's leases while it is inactive, or that the client is activated before its lease begins to expire, the client will lose access to the resources it has acquired. This loss can be particularly dramatic in the case of lookup service registrations. A service's registration with a lookup service is leased—if the service deactivates… and it does not take appropriate steps, its registrations with lookup services will expire, and before long it will be inaccessible. If that service only becomes active when clients invoke its methods, it may never become active again, because at this point new clients may not be able to find it. The Lease Renewal Service provides the interface to enable using entities to delegate their lease management responsibilities to a third party. The LRS is defined by the LeaseRenewalService interface. Listing 7.6 The LeaseRenewalService Interface public interface LeaseRenewalService { public LeaseRenewalSet createLeaseRenewalSet (long leaseDuration) throws RemoteException; } A client entity invokes the createLeaseRenewalSet method to create a set to which the entity will add leases that the renewal service should renew. The client entity leases a renewal set from the LRS for a specific amount of time (leaseDuration). When the lease expires, the LRS stops renewing the leases in that set. This is important because the set is also a leased resource. If you want the leases within the set to continue to be renewed, you must renew the container, for example the set. After a client entity becomes inactive, how can it renew its lease for the LeaseRenewalSet? The client entity can ask the LeaseRenewalSet implementation to send it a warning event before the lease expires; it can then activate and renew the set's lease. The setExpiration WarningListener method, of the LeaseRenewalSet class, registers the warning request. Listing 7.7 The LeaseRenewalSet Interface package net.jini.lease; public interface LeaseRenewalSet { final public static long RENEWAL_FAILURE_EVENT_ID = 0; final public static long EXPIRATION_WARNING_EVENT_ID = 1; public void renewFor(Lease leaseToRenew, long desiredDuration, long renewDuration) throws RemoteException; public void renewFor(Lease leaseToRenew, long desiredDuration) throws RemoteException; public EventRegistration setExpirationWarningListener( RemoteEventListener listener, long minWarning, MarshalledObject handback) throws RemoteException; 156 public void clearExpirationWarningListener() throws RemoteException; public EventRegistration setRenewalFailureListener( RemoteEventListener listener, MarshalledObject handback) throws RemoteException; public void clearRenewalFailureListener() throws RemoteException; public Lease remove(Lease leaseToRemove) throws RemoteException; public Lease[] getLeases() throws LeaseUnmarshalException, RemoteException; public Lease getRenewalSetLease(); } Clients find the renewal service by creating a ServiceTemplate, using the LRS interface as a parameter. public LeaseRenewalService findLRS(ServiceRegistrar registrar) { LeaseRenewalService leaseService; ServiceTemplate template; // set the interface class Class[] leaseRenewalServiceInterface = { LeaseRenewalService.class } ; // create the template template = new ServiceTemplate(null, leaseRenewalServiceInterface, null); // use the already discovered LUS to find the LRS try { leaseService = (LeaseRenewalService)registrar.lookup(template); } catch (Exception e) { e.printStackTrace(); } return leaseService; } The following code fragment creates a LeaseRenewalSet and requests a one-hour lease duration, with notification one minute prior to expiration. // lease the set for 1 hour long leaseDuration = 1000*60*60; // notify one minute before expiration long leaseNotification = 1000*60*1; leaseRenewalSet = leaseRenewalService.createLeaseRenewalSet(leaseDuration); leaseRenewalSet.setExpirationWarningListener(leaseNotification,null,n ull); This fragment supplies the notification: public void notify(RemoteEvent event)throws UnknownEventException, RemoteException 157 { ExpirationWarningEvent evt = (ExpirationWarningEvent)event; // get the set Lease lease = evt.getRenewalSetLease(); try { // extend for an additional hour // notification still set at one minute lease.renew(leaseDuration); } catch (Exception e) { e.printStackTrace(); } } And adding leases to be managed is accomplished with the following: Lease lease = someLeasedResource; leaseRenewalSet.renewFor(lease, someLeaseTime); You now have a service that can manage all the leases acquired by your application. All that is required is access to the LeaseRenewalSet. The Norm Implementation Sun Microsystems provides a reference implementation of the Jini Lease Renewal Service called norm. As supplied, you can run norm only as an activatable service. Starting Norm The following script can be used to start the activatable version of norm: Listing 7.8 The startNorm.cmd Script @echo off @call setup.cmd set GROUPS=public echo echo echo echo echo echo echo echo Jini and JavaSpaces Application Development ------------------------------------------Jini install directory %JINI_HOME% Examples install directory %JWORK_HOME% Web server %HTTP_ADDRESS% Default group %GROUPS% ------------------------------------------Starting the norm lease renewal service... java -jar -Djava.security.policy=%JINI_HOME%\policy\policy.all %JINI_HOME%\lib\norm.jar http://%HTTP_ADDRESS%/norm-dl.jar %JINI_HOME%\policy\policy.all %JWORK_HOME%\services\logs\norm_log %GROUPS% The LRS permits client entities to disconnect or become inactive and still have a process in place that can manage resource leasing requirements. As noted previously, this is especially important for activatable services that must maintain residency in the Jini community even while dormant. Often the RemoteEventListener that is supplied to the setExpirationWarning Listener method of the LeaseRenewalSet is also an activatable service. This allows 158 you to activate the listener only when leases are preparing to expire, and renew or cancel leases as processing dictates. Let's now look at the final helper service in the Jini distribution, the EventMailbox service, which is designed to provide delegated remote event notification. The EMS Interface Explained A Jini service may choose to delay handling of some remote events directed to it. A dormant activatable Jini service implementation might not want to activate every time it receives a remote event. The Event Mailbox Service (EMS) enables a service to delegate event listening to a third party process. The EMS can be used to intercept events, store them, and forward them when the mailbox's client deems it convenient. The EMS is defined by the EventMailbox interface. Listing 7.9 The EventMailbox Interface public interface EventMailbox { MailboxRegistration register (long leaseDuration) throws RemoteException, LeaseDeniedException; } Clients find the EMS by creating a ServiceTemplate, using the interface as a parameter. public EventMailbox findEMS(ServiceRegistrar registrar) { EventMailbox mailbox; ServiceTemplate template; // set the interface class Class[] eventMailboxInterface = { EventMailbox.class } ; // create the template template = new ServiceTemplate(null, eventMailboxInterface, null); try { // use an existing LUS to fine the EMS mailbox = (EventMailbox)registrar.lookup(template); } catch (Exception e) { e.printStackTrace(); } return mailbox; } Clients invoke the register method on the EventMailbox and receive a MailboxRegistration as a result. Listing 7.10 The MailboxRegistration Interface package net.jini.event; 159 public interface MailboxRegistration { Lease getLease(); RemoteEventListener getListener(); void enableDelivery(RemoteEventListener target) throws RemoteException; void disableDelivery() throws RemoteException; } With the MailboxRegistration interface, an entity can manage the time and location of remote event delivery. A client entity retrieves a RemoteEventListener from the registration object and uses this listener in any method that requires one. The client entity can then disconnect from the network and not worry about losing events. The events will be directed and stored by the mailbox listener. At a later point in time, the client entity can activate and invoke the enableDelivery method, specifying a RemoteEventListener that becomes the target for the saved events. The client entity can toggle between enabling and disabling delivery until it terminates or allows its lease to expire with the mailbox service. The Mercury Implementation Sun Microsystems provides a reference implementation of the Jini Event Mailbox Service called mercury. You can run mercury as an activatable service or transient service. The following script can be used to start the activatable version of mercury. Listing 7.11 The startMercury.cmd Script @echo off @call setup.cmd set GROUPS=public echo echo echo echo echo echo echo echo Jini and JavaSpaces Application Development ------------------------------------------Jini install directory %JINI_HOME% Examples install directory %JWORK_HOME% Web server %HTTP_ADDRESS% Default group %GROUPS% ------------------------------------------Starting the mercury event mailbox service... java -jar -Djava.security.policy=%JINI_HOME%\policy\policy.all %JINI_HOME%\lib\mercury.jar http://%HTTP_ADDRESS%/mercury-dl.jar %JINI_HOME%\policy\policy.all %JWORK_HOME%\services\logs\mercury_log %GROUPS% And the following command can be used to start the transient version of mercury: java -cp %JINI_HOME%\lib\mercury.jar Djava.rmi.server.codebase=http:\\hostname:8080/ mercury-dl.jar -Djava.security.policy=%JINI_HOME%\policy\policy.all com.sun.jini.mercury.TransientMercury %JWORK_HOME%\services\logs\transient-mercury_log public A Framework for Service Registration 160 Now that we have looked at the helper services, let's develop a framework for demonstrating their usage. You want to enable activatable services in your Jini community. Activatable services require the following steps: • • • Create an activation group description. Create an activation object description. Register with the activation system. Create an ActivationGroupDesc that will encapsulate the parameters necessary for the activation system to start your group of services. The ActivatableService class assists in defining an activatable service. Listing 7.12 The ActivatableService Class package org.jworkplace.util; import import import import import import import import import import import import import public java.rmi.activation.Activatable; java.rmi.activation.ActivationDesc; java.rmi.activation.ActivationGroupDesc; java.rmi.activation.ActivationGroupDesc.CommandEnvironment; java.rmi.activation.ActivationGroup; java.rmi.activation.ActivationGroupID; java.rmi.activation.ActivationException; java.rmi.activation.UnknownGroupException; java.rmi.MarshalledObject; java.rmi.RemoteException; java.rmi.RMISecurityManager; java.io.IOException; java.util.Properties; class ActivatableService { // Uniquely identifies the VM for the group private ActivationGroupID groupID = null; // Path and command-line arguments used to spawn VM protected ActivationGroupDesc groupDesc = null; // the service description required by rmid protected ActivationDesc objectDesc = null; // the service we are registering with rmid protected Object proxy = null; protected void createGroup(Properties props, CommandEnvironment env) { // create the startup group description groupDesc = new ActivationGroupDesc(props, env); try { // get the unique id for the group groupID = ActivationGroup.getSystem().registerGroup(groupDesc); } catch(RemoteException e) { e.printStackTrace(); System.exit(1); } catch(ActivationException e) { e.printStackTrace(); 161 System.exit(1); } // create the activation group try { ActivationGroup.createGroup(groupID, groupDesc, 0); } catch(ActivationException e) { e.printStackTrace(); System.exit(1); } } protected void registerObject(ActivationGroupID groupID, String className, String codebase, MarshalledObject data, boolean restart) { // create the startup service description try { // service does not belong to an existing group if(groupID == null) { objectDesc = new ActivationDesc(className, codebase, data, restart); // service belongs with an existing group } else { objectDesc = new ActivationDesc(groupID, className, codebase, data, restart); } } catch(ActivationException e) { e.printStackTrace(); System.exit(1); } // register with the activation system try { service = Activatable.register(objectDesc); } catch(UnknownGroupException e) { e.printStackTrace(); System.exit(1); } catch(ActivationException e) { e.printStackTrace(); System.exit(1); } catch(RemoteException e) { e.printStackTrace(); System.exit(1); } } } You can extend your ActivatableService class for each service you define as activatable. Your ServiceListener (see Listing 7.13), creates and registers an activatable 162 RemoteEvent Listener. It provides the necessary support to register the activatable listener with the LDS. So, when a new LUS is discovered, the LDS will wake up the listener with the appropriate notification. In addition, it finds and uses the LRS to provide an example of the lease renewal support required for activatable services. Listing 7.13 The ServiceListener Class package org.jworkplace.utils; import import import import import import import import import import import import import import import import import import import java.io.IOException; java.util.Properties; java.rmi.MarshalledObject; java.rmi.Naming; java.rmi.RemoteException; java.rmi.RMISecurityManager; net.jini.core.discovery.LookupLocator; net.jini.core.event.RemoteEventListener; net.jini.core.lookup.ServiceRegistrar; net.jini.core.lease.Lease; net.jini.discovery.DiscoveryEvent; net.jini.discovery.DiscoveryListener; net.jini.discovery.DiscoveryManagement; net.jini.discovery.LookupDiscovery; net.jini.discovery.LookupDiscoveryService; net.jini.discovery.LookupDiscoveryRegistration; net.jini.lease.LeaseRenewalService; net.jini.lease.LeaseRenewalSet; org.jworkplace.util.*; public class ServiceListener extends ActivatableService implements DiscoveryListener { // Discovers lookup services private DiscoveryManagement mgt = null; // The lookup discovery service private LookupDiscoveryService lookupService = null; // the lookup discovery registration protected LookupDiscoveryRegistration lookupRegistration = null; // The lease renewal service private LeaseRenewalService leaseRenewalService = null; // the lease renewal service lease set used to manage our leases protected LeaseRenewalSet leaseRenewalSet = null; // the lease on the lease renewal set private Lease lookupLease = null; protected long leaseDuration = 120000L; protected long leaseNotification = 60000L; // the codebase for the VM private String CODEBASE = null; // the service class name protected String classname = "org.jworkplace.service.ServiceListenerProxy"; 163 public ServiceListener(String[] args) { super(); Properties props = new Properties(); // codebase CODEBASE = args[0]; props.put("java.rmi.server.codebase", args[0]); // security policy props.put("java.security.policy", args[1]); // class path props.put("java.class.path", System.getProperty("java.class.path")); // now create the group description createGroup(props, null); } protected void registerWithActivation() throws IOException { // register our service listener // we are registering the ServiceListenerProxy registerObject(groupID, classname, CODEBASE, getActivationHandback(), false); // now start finding registrars (lookup services) mgt = new LookupDiscovery(LookupDiscovery.NO_GROUPS); mgt.addDiscoveryListener(this); ((LookupDiscovery)mgt).setGroups(LookupDiscovery.ALL_GROUPS); } // implementation of DiscoveryListener // called when LUS discovered public synchronized void discovered(DiscoveryEvent de) { // get the array of lookup services discovered ServiceRegistrar[] registrars = de.getRegistrars(); // register with LDS registerWithLDS(registrars, getDiscoveryEventListener()); // register with LRS // the lookupLease contains the lease on the LDS registerWithLRS(registrars, getWarningEventListener(), lookupLease); } public void discarded(DiscoveryEvent de) { } // used to register with LDS protected synchronized void registerWithLDS(ServiceRegistrar[] registrars, RemoteEventListener listener) { 164 // if already registered return if(lookupRegistration == null) { for(int i=0; i < registrars.length; i++) { try { lookupService = ServiceFinder.findLDS(registrars[i]); // if found register with it if(lookupService != null) { // register to discover all LUSs // set the expiration warning listener to our // ServiceListenerProxy lookupRegistration = lookupService.register(LookupDiscovery.ALL_GROUPS, new LookupLocator[0], listener, getListenerHandback(), leaseDuration); break; } } catch (Exception e) { e.printStackTrace(); System.exit(1); } } // get the LDS lease to renew using the LRS lookupLease = lookupRegistration.getLease(); } } protected synchronized void registerWithLRS(ServiceRegistrar[] registrars, RemoteEventListener listener, Lease lease) { if(leaseRenewalSet == null) { for(int i=0; i < registrars.length; i++) { try { leaseRenewalService = ServiceFinder.findLRS(registrars[i]); if(leaseRenewalService != null) { System.out.println("Found a lease renewal service"); leaseRenewalSet = leaseRenewalService.createLeaseRenewalSet(leaseDuration); leaseRenewalSet.renewFor(lease, Lease.FOREVER); leaseRenewalSet.setExpirationWarningListener(listener, leaseNotification, getWarningHandback()); break; } } catch(Exception e) { e.printStackTrace(); } } } } // overrides protected MarshalledObject getActivationHandback() { return null; } protected MarshalledObject getListenerHandback() { return null; } 165 protected MarshalledObject getWarningHandback() { return null; } protected RemoteEventListener getDiscoveryEventListener() { return (RemoteEventListener)proxy; } protected RemoteEventListener getWarningEventListener() { return (RemoteEventListener)proxy; } public static void main(String[] args) { if(args.length < 2) { System.out.println("Invalid usage: set codebase and security policy "); System.exit(1); } if (System.getSecurityManager() == null) { System.setSecurityManager(new RMISecurityManager()); } try { new ServiceListener(args).registerWithActivation(); } catch(IOException e) { e.printStackTrace(); System.exit(1); } try { Thread.sleep(10000L); } catch(InterruptedException e) { } System.exit(0); } } The activatable RemoteEventListener (see Listing 7.14), receives notification of both LDS events (RemoteDiscoveryEvent) and LRS events (ExpirationWarningEvent). Listing 7.14 The ServiceListenerProxy Class public class ServiceListenerProxy extends Activatable implements RemoteEventListener { private MarshalledObject data; public ServiceListenerProxy(ActivationID id, MarshalledObject data) throws java.rmi.RemoteException { super(id,0); 166 this.data = data; System.out.println("ServiceListenerProxy active..."); } public void notify(RemoteEvent event)throws UnknownEventException, RemoteException { if(event instanceof RemoteDiscoveryEvent) { RemoteDiscoveryEvent evt = (RemoteDiscoveryEvent)event; try { System.out.println("ID: " + evt.getID()); System.out.println("Sequence #: " + evt.getSequenceNumber()); MarshalledObject object = event.getRegistrationObject(); if(object != null) { System.out.println("LDS registration object: " + object.get()); } else { System.out.println("No LDS registration object set"); } System.out.println("Discovered : "+ evt.isDiscarded()); if (! evt.isDiscarded()) { ServiceRegistrar[] registrars = null; try { registrars = evt.getRegistrars(); } catch(LookupUnmarshalException e) { e.printStackTrace(); return; } for (int i = 0; i < registrars.length; i++) { System.out.println(registrars[i]); } } } catch (Exception e) { e.printStackTrace(); } } else if(event instanceof ExpirationWarningEvent) { ExpirationWarningEvent evt = (ExpirationWarningEvent)event; Lease lease = evt.getRenewalSetLease(); try { lease.renew(1000*60*2); } catch (Exception e) { e.printStackTrace(); } System.out.println("Lease renewed for " + (lease.getExpiration() System.currentTimeMillis())); } } } ServiceFinder is the helper class for finding services, which was defined in the last chapter. You pass a ServiceRegistrar (lookup service) to each method. A null can be returned if the LUS does not contain the desired service. This version does not try to constrain the lookup with groups or entries. Note the null ServiceTemplate parameters. package org.jworkplace.util; import java.rmi.*; import net.jini.core.lookup.ServiceTemplate; import net.jini.core.lookup.ServiceRegistrar; 167 import net.jini.discovery.LookupDiscoveryService; import net.jini.event.EventMailbox; import net.jini.lease.LeaseRenewalService; public class ServiceFinder { public static LookupDiscoveryService findLDS(ServiceRegistrar registrar) { LookupDiscoveryService lookupService = null; ServiceTemplate template; Class[] lookupDiscoveryServiceInterface = LookupDiscoveryService.class } ; template = new ServiceTemplate(null, lookupDiscoveryServiceInterface, null); try { lookupService = (LookupDiscoveryService)registrar.lookup(template); } catch (Exception e) { e.printStackTrace(); } return lookupService; } { public static LeaseRenewalService findLRS(ServiceRegistrar registrar) { LeaseRenewalService leaseService = null; ServiceTemplate template; Class[] leaseRenewalServiceInterface = { LeaseRenewalService.class } ; template = new ServiceTemplate(null, leaseRenewalServiceInterface, null); try { leaseService = (LeaseRenewalService)registrar.lookup(template); } catch (Exception e) { e.printStackTrace(); } return leaseService; } public static EventMailbox findEMS(ServiceRegistrar registrar) { EventMailbox mailbox = null; ServiceTemplate template; Class[] eventMailboxInterface = { EventMailbox.class } ; template = new ServiceTemplate(null, eventMailboxInterface, null); try { mailbox = (EventMailbox)registrar.lookup(template); } catch (Exception e) { e.printStackTrace(); } return mailbox; } } Building Activatable Services and Service Monitors Now that you have classes defined to create activatable services and perform the necessary lease management, let's provide an example to demonstrate their usage. To begin, make sure that you have the necessary services running to support the test environment. You will need the following as minimum requirements: 168 • • • • • HTTP server Lookup service (reggie) Lookup discovery service (fiddler) Lease renewal service (norm) Rmid on all machines that support activation Figure 7.4 depicts one possible test configuration. Figure 7.4. One possible test configuration is illustrated in this diagram. Of course, Jini provides you with great flexibility in deploying and activating services. If you have not yet started the necessary services, you should start the following: • • • • • Rmid wherever services will be activated HTTP server where the LUS is running LUS, such as reggie LDS, such as fiddler (see Listing 7.5 or use the Start Service GUI) LRS, such as norm (see Listing 7.8 or use the Start Service GUI) Compile the Files To build the example, change to the Chapter 7 example 1 directory and invoke the build command. C:\JiniJavaSpaces\chapter7\example1\build Listing 7.15 The build.cmd Script @echo off echo ------------------------------------------echo Jini and JavaSpaces Application Development echo Chapter 7 - Example 1 echo compiling echo ------------------------------------------if not "%JINI_HOME%" == "" goto staticClasspath echo You must set JINI_HOME to point to your Jini installation 169 echo For example set JINI_HOME=c:\files\jini1_2 goto exit echo compiling files :staticClasspath echo Setting your CLASSPATH statically. set CP= if exist "%JINI_HOME%\lib\jini-core.jar" set CP=%CP%;%JINI_HOME%\lib\jini-core.jar if exist "%JINI_HOME%\lib\jini-ext.jar" set CP=%CP%;%JINI_HOME%\lib\jini-ext.jar if exist "%JINI_HOME%\lib\sun-util.jar" set CP=%CP%;%JINI_HOME%\lib\sun-util.jar echo Using CLASSPATH: %CP% javac -classpath %CP%;.; *.java -d . rmic -v1.2 -classpath %CP% org.jworkplace.service.ServiceListenerProxy -d . Create Client-Side and Server-Side Jar Files Now invoke the deploy command to create the jar files. Note that the deploy script adds the RemoteEvent, RemoteEventListener, and UnknownEventException classes to the downloaded jar file. This is to ensure that the necessary class files are available to the rmid activation daemon. C:\JiniJavaSpaces\chapter7\example1\deploy Listing 7.16 The deploy.cmd Script @echo off @call ..\..\services\scripts\setup.cmd echo Jini and JavaSpaces Application Development echo Chapter 7 Example 1 echo ------------------------------------------echo Jini install directory %JINI_HOME% echo Examples install directory %JWORK_HOME% echo Web server %HTTP_ADDRESS% echo ------------------------------------------echo Creating jar file chap7ex1.jar in directory %JWORK_HOME%\services\lib echo ... jar cvf service\chap7ex1.jar org/jworkplace/service/\*.class org/jworkplace/ util\*.class copy service\chap7ex1.jar %JWORK_HOME%\services\lib\chap7ex1.jar echo Creating jar file chap7ex1-dl.jar in directory %JWORK_HOME%\services\lib echo ... jar cvf service\chap7ex1-dl.jar -C org/jworkplace/service/ / ServiceListenerProxy_Stub.class jar xvf %JINI_HOME%\lib\jini-core.jar net/jini/core/event/RemoteEventListener 170 jar uf service\chap7ex1-dl.jar net/jini/core/event/RemoteEventListener.class jar xvf %JINI_HOME%\lib\jini-core.jar net/jini/core/event/UnknownEventException jar uf service\chap7ex1-dl.jar net/jini/core/event/UnknownEventException.class jar xvf %JINI_HOME%\lib\jini-core.jar net/jini/core/event/RemoteEvent jar uf service\chap7ex1-dl.jar net/jini/core/event/RemoteEvent.class copy service\chap7ex1-dl.jar %JWORK_HOME%\services\lib\chap7ex1dl.jar Start the ServiceListener Server Change to the service directory and invoke the startServiceListener.cmd. The ServiceListener registers your ServiceListenerProxy with rmid. Notice that you pass the codebase and the security policy as arguments to ServiceListener. When the expiration warning notification is sent from the LRS, your ServiceListenerProxy is activated and renews the lease for an additional two minutes (see Figure 7.5). You will see the expiration warning event from the LRS sent every minute. In addition, if you start or stop a LUS in your Jini community, you should see discovery event notifications sent from the LDS and received by the ServiceListenerProxy. Figure 7.5. The ServiceListener registers the ServiceListenerProxy with the activation daemon and then exits. Every minute, the LRS generates a lease expiration warning event. You should see the ServiceListenerProxy wake-up and renew the lease for an additional two minutes. This continues until you shut down rmid and delete the log file. 171 You did not register your ServiceListenerProxy with any LUS. Instead, you made it an RMI activatable service only. If you want the capability to find the ServiceListenerProxy using an LUS, you can add the JoinManager and DiscoveryManagement to the startup code, and register the proxy with any LUSes discovered. Summary An activatable service must initiate when it receives a RemoteDiscoveryEvent. At that point, it must join the located lookup services, or discard lookup services no longer available. In addition, it must initiate when it receives an ExpirationWarningEvent, at which time it must renew its lease for the LeaseRenewalSet that generated the event. Helper services can be employed to lessen the development effort required, and assist clients and services with maintaining desirable behavior in a Jini community. This is especially important with activatable services. The defined framework can be used as a starting point for building the necessary controls to ensure good Jini citizenship. 172 Chapter 8. Service Administration IN THIS CHAPTER • • • • • • Service Administration Overview Client Administration The Administrative Interfaces Building on Your Service Framework Client-Side Implementation Summary This chapter explains how Jini clients administer Jini services. It explains the management of groups, locators, and entries in service administration. In addition, you will define a Jini service to provide administrative functionality. Service Administration Overview One of the primary goals of Jini is to reduce the amount of administration that is required to establish and maintain a Jini community. Services have a broad definition, so trying to define the administrative requirements and a standard implementation for all services is impossible. However, Jini has defined a standard set of interfaces that services can implement and extend to satisfy specific requirements. These interfaces can be used to provide a standard for accessing and controlling service behavior. Service behavior is controlled by managing the following: • • • The capture and maintenance of all LUS registrations The capture and propagation of the unique service ID across all LUS registrations The consistent capture and maintenance of service attributes and groups across all LUS registrations When a service deactivates, this information needs to be saved to persistent storage. Likewise, when the service activates, it must be retrieved to initialize the service. Other information specific to a service implementation might also need to be saved. The Jini service administration interfaces provide the necessary methods to manage these properties of a Jini service. Additionally, this information provides control to processes that have traditionally been administratively intensive, such as configuring, deploying, and monitoring services on a network. The process of discovery and joining is about building service relationships. It is about finding and binding to services that meet specific criteria. These processes, in addition to lease management, provide the substrate that allows services to be self-deterministic and to recover from failure without human intervention. It is somewhat ironic, but understanding administration in an environment that is relatively administration-free is a key to understanding Jini. Client Administration 173 Client administration enables a using entity to configure and control the properties of a serviceproviding entity. This will typically be in the form of a client that is given the appropriate authorization to perform administrative functions. All services need some form of administration, even if it is only the initial launch in a Jini network. But, practically speaking, at a minimum services need a mechanism to control the parameters for the following: • • • • Group membership Service attributes Discovery policy Join policy The net.jini.admin.Administrable interface is intended to provide a standard method for services to deliver an administrative object to using entities, so you know whether or not the service supports administration. Listing 8.1 The Administrable Interface public interface Administrable { public Object getAdmin() throws RemoteException; } A client can determine whether a service supports administration by checking whether the service supports the Administrable interface. It can then access the administrative object by invoking the getAdmin method. if(someService instanceof Administrable) { Object admin = ((Administrable)someService).getAdmin(); The using entity can then determine which administrative interfaces the service supports. Managing Groups, Service Attributes, Locators You control service participation in a Jini community by managing the set of groups, attributes, and locators that a service supports (see Figure 8.1). Figure 8.1. Jini provides three sets of elements to control visibility and participation in a Jini community. 174 Group Membership Jini lookup services are organized into groups. You can create groups with any name that you desire. There is also a default group called the "public" group. When you start a LUS, you specify the groups that the LUS will support. In addition, when you discover a LUS, you specify the groups you require it to support in order to register with the service. In the examples, you have been setting the %GROUPS% environment variable to public. Figure 8.2 demonstrates the results when services are registered along departmental boundaries. Figure 8.2. When the Human Resources service attempts to discover registration points (LUS), it will only hear back from lookup services that support the groups requested. 175 Group objects are merely represented as a set of java.lang.String[] objects. Services can change their group membership at any time, and using entities can optionally receive notification that membership has changed. Service administration provides the necessary methods to support setting, modifying, and adding groups to any service definition that supports join or discovery administration. Service Attributes Recall that service attributes are defined as a set of net.jini.core.entry.Entry[] entries. Entry objects are used to augment the service definition so using entities can perform specific searches for matching services. Jini supplies a number of predefined attributes; for instance, Address and Location entries, and Name and ServiceInfo entries. It is also possible to define attributes that are unique to a particular service. Attributes can be added and removed at any time during the lifecycle of a service. Attributes that do not permit clients to administer their fields should implement the ServiceControlled interface. public interface net.jini.lookup.entry.ServiceControlled { } This is simply a marker interface that can be used to indicate to administrative services that this entry (attribute) is not client-modifiable. Lookup Locators Lookup locators provide the third critical data element to Jini services. Locators determine where on the network to discover lookup services and services in general. As discussed in Chapter 4, "Building on Jini Foundation Concepts," unicast discovery uses lookup locators to define specific addresses (hostnames or IP addresses) to search for services. Lookup locators are defined as a set of net.jini.core.discovery.LookupLocator[] objects. Service administration provides methods to manage the set of lookup locators that a service will use, and this directly affects where on the network the service will reside. Up to this point in the book, services have been defined using static configurations. For instance, we did not alter group membership, service attributes, or service location information. Despite this, Jini demonstrated its flexibility through the discovery and join process. Now let's look at the interfaces that support the administration and control of services in the Jini community. The Administrative Interfaces There are three administrative interfaces that comprise the set of interfaces used to control and manage Jini services. They are as follows: • The net.jini.admin.JoinAdmin interface is used to control a service's participation in the join protocol. 176 • • The com.sun.jini.admin.StorageLocationAdmin interface permits a client to specify where a service keeps its persistent data. The com.sun.jini.admin.DestroyAdmin interface permits a client to destroy a service (that is, delete persistent storage, unregister the service, and exit the VM). A common convention is to extend these interfaces and add service-specific interfaces: public interface MyServiceAdmin extends JoinAdmin, StorageLocationAdmin, DestroyAdmin { public myServiceAdminMethod() throws RemoteException; // other methods } You then implement the methods that comprise the set of administrative services supported. In addition, there are two LUS-specific interfaces. • • The net.jini.lookup.DiscoveryAdmin interface is used to control which groups a LUS is a member of; and for controlling which TCP port a LUS uses for its lookup locator. The com.sun.jini.reggie.RegistrarAdmin interface is an administrative interface for the Sun Microsystems-supplied reggie implementation of the LUS. Now let's look at the services in detail. The JoinAdmin Interface After you have determined that a service supports administration—it implements the Administrable interface—you can retrieve the administration object. Using a similar technique, you can determine the interfaces supported by the administrative object. JoinAdmin joinAdmin = null; try { if(someService instanceof Administrable) { Object admin = ((Administrable)someService).getAdmin(); if(admin instanceof JoinAdmin) joinAdmin = (JoinAdmin) admin; } } catch(Exception e) { } // invoke joinAdmin methods Most services will implement the JoinAdmin interface because it provides methods to control the three fundamental elements of Jini service composition and behavior—groups, attributes, and locators. Listing 8.2 The JoinAdmin Interface public interface JoinAdmin { public void addLookupAttributes(Entry[] attrSets); public void addLookupGroups(java.lang.String[] groups); public void addLookupLocators(LookupLocator[] locators); public Entry[] getLookupAttributes(); public java.lang.String[] getLookupGroups(); 177 public LookupLocator[] getLookupLocators(); public void modifyLookupAttributes(Entry[] attrSetTemplates, Entry[] attrSets); public void removeLookupGroups(java.lang.String[] groups); public void removeLookupLocators(LookupLocator[] locators); public void setLookupGroups(java.lang.String[] groups); public void setLookupLocators(LookupLocator[] locators); } The JoinAdmin interface enables using entities to control how the service joins the Jini community. The StorageLocationAdmin Interface The StorageLocationAdmin interface enables a client to specify where a service keeps its persistent data. For instance, good Jini behavior mandates that services use a consistent service ID across registrations and that they "remember" the configuration (groups, attributes, and locators) used when the service is restarted. The StorageLocationAdmin interface specifies where that information resides. Listing 8.3 The StorageLocationAdmin Interface public interface StorageLocationAdmin { public abstract String getStorageLocation(); public abstract void setStorageLocation(String location); } The setStorageLocation method should specify the name of a directory or storage access point that can be identified by a String. This method will create a log file that captures configurations and service-specific information. The process does the following: 1. Grabs a write lock to ensure exclusive access to the file. 2. Creates the log file, or copies the current log file to a new location. 3. Takes a new snapshot of the current service state. 4. If it's an activatable service, it passes the file location to the ActivationSystem. 5. Releases the write lock. Some thought must go into implementing this method if an existing service is running while the log file is being moved. Separating service administration information from service snapshots, for instance ongoing incremental updates, can help to minimize the impact and avoid excessive queuing and loss of data. The DestroyAdmin Interface Using entities that want to destroy a service (delete persistent storage, unregister the service, and then exit the VM) should use the DestroyAdmin interface. Listing 8.4 The DestroyAdmin Interface 178 public interface DestroyAdmin { public void destroy(); } The DiscoveryAdmin Interface This interface is specific to the lookup service. It enables clients to administer the discovery parameters—primarily group membership of a LUS. Listing 8.5 The DiscoveryAdmin Interface public interface DiscoveryAdmin { void addMemberGroups(String[] groups); String[] getMemberGroups(); int getUnicastPort(); void removeMemberGroups(String[] groups); void setMemberGroups(String[] groups); void setUnicastPort(int port); } Often a LUS will be defined to support a single group or department. For instance, the sales group would be supported by one or more dedicated lookup services. The groups are administered through this interface. The RegistrarAdmin interface is unique to the reggie implementation of the ServiceRegistrar. It uses interface composition to provide standard service administration and extends that definition with reggie-specific methods. These methods include setting lease duration values and managing the heuristics associated with logging. Listing 8.6 The RegistrarAdmin Interface public interface RegistrarAdmin extends DiscoveryAdmin, StorageLocationAdmin, DestroyAdmin, JoinAdmin { /** * Change the lower bound for the maximum value allowed by the * lookup service for any service lease, in milliseconds. */ void setMinMaxServiceLease(long leaseDuration) throws RemoteException; /** * Retrieve the lower bound for the maximum value allowed by the * lookup service for any service lease, in milliseconds. */ long getMinMaxServiceLease() throws RemoteException; /** * Change the lower bound for the maximum value allowed by the * lookup service for any event lease, in milliseconds. */ void setMinMaxEventLease(long leaseDuration) throws RemoteException; /** * Retrieve the lower bound for the maximum value allowed by the 179 * lookup service for any event lease, in milliseconds. */ long getMinMaxEventLease() throws RemoteException; /** * Change the minimum average interval between lease renewals, in * milliseconds. */ void setMinRenewalInterval(long interval) throws RemoteException; /** * Retrieve the minimum average interval between lease renewals, in * milliseconds. */ long getMinRenewalInterval() throws RemoteException; /** * Change the weight factor applied by the lookup service to the * snapshot size */ void setSnapshotWeight(float weight) throws RemoteException; /** * Retrieve the weight factor applied by the lookup service to the * snapshot size */ float getSnapshotWeight() throws RemoteException; /** * Change the value of the size threshold of the snapshot; */ void setLogToSnapshotThreshold(int threshold) throws RemoteException; /** * Retrieve the value of the size threshold of the snapshot */ int getLogToSnapshotThreshold() throws RemoteException; } Building on Your Service Framework Now let's build on the service framework to include administrative capabilities. The ServiceAdmin Interface A typical service interface will combine the JoinAdmin, DestroyAdmin, and StorageLocationAdmin interfaces. As stated, these interfaces define the methods required to manage Jini services. The ServiceAdmin interface is an example. Listing 8.7 The ServiceAdmin Interface package org.jworkplace.service; import net.jini.admin.JoinAdmin; 180 import com.sun.jini.admin.DestroyAdmin; import com.sun.jini.admin.StorageLocationAdmin; public interface ServiceAdmin extends JoinAdmin, DestroyAdmin, StorageLocationAdmin { } Here the common set of administrative services is extended by using an interface composition technique. Next you define a backend interface that services will implement by adding the Remote interface. The ServiceBackEnd interface extends Administrable, ServiceAdmin, and Remote. This will be the interface that all your remote services will implement. Listing 8.8 The ServiceBackEnd Interface package org.jworkplace.service; import java.rmi.Remote; import net.jini.admin.Administrable; import org.jworkplace.service.ServiceAdmin; public interface ServiceBackEnd extends Administrable, ServiceAdmin, Remote { } The ServiceAdmin Implementation Now let's define and implement the methods necessary to manage a service in the Jini community. This will serve as an abstract base class that you can extend to provide specific-service functionality. All the services defined to this point require participation in lookup and discovery. This is fundamental to becoming a citizen in the Jini community. You can capture the primary elements that assist and control this process in your base class. They include the following: • • • • • • • Join Manager—Manages the join process Lookup Discovery Manager—Manages the discovery process Lookup Locators—Controls the unicast discovery process Groups—Organize services Attributes—Augment service definition Service Proxy—Implements the using entity (client) Service ID—Uniquely identifies the service ServiceImpl Abstract Class The ServiceImpl class contains the necessary mechanisms to control and administer a Jini service, as shown in Figure 8.3. Figure 8.3. The abstract ServiceImpl class provides a base class for services to extend. 181 Listing 8.9 The ServiceImpl Class public abstract class ServiceImpl implements ServiceBackEnd { private private private private private private private JoinManager joiner = null; LookupDiscoveryManager lookupDiscMgr; LookupLocator locators[]; String groups[] = DiscoveryGroupManagement.ALL_GROUPS; Entry[] attributes; Object proxy; ServiceID serviceID = null; There are two abstract methods defined in ServiceImpl: public abstract Entry[] getAttributes(); public abstract Object getProxy() throws IOException; The getAttributes method must be implemented by concrete classes to provide the service attribute information used to register with an LUS. The getProxy method must be implemented to provide the using entity (client) implementation. This is the object that is downloaded to the client if it is a remote interface. ServiceImpl provides a null reference to the getAdmin method of the Administrable interface. Classes that extend ServiceImpl should override this method and provide an administrative object of the appropriate type. /* Administrable Interface */ public Object getAdmin() throws RemoteException { return null; } ServiceImpl performs the following steps: 1. Exports the service (itself) to RMI. 2. Creates a snapshot thread to take snapshots of the service as it is running. 3. Creates a reliable log handler that is used to store the log and snapshot information. 4. Recovers any existing service log information. 182 5. Initializes the service by discovering and joining the Jini community with the state retrieved from the service logs. 6. Starts the snapshot thread. /** our persistent store used to save state */ private PersistentStore store; /** The name of the log directory where state will reside*/ private String logDir; /** Thread that performs snapshots when signaled */ private SnapshotThread snapshotter; public ServiceImpl(String[] args) throws IOException { // export to RMI UnicastRemoteObject.exportObject(this); // Get Log Directory Name from the command line logDir = args[0]; System.out.println("Using log directory " + logDir); // start a thread to perform service specific updates snapshotter = new SnapshotThread(); try { // create a logging subsystem store = new PersistentStore(logDir, new ServiceLogHandler(), this); } catch (IOException e) { e.printStackTrace(); } // get required attributes and proxy and start join process init(); // current baseline store.snapshot(); // start thread snapshotter.start(); } The init method is used to create the LookupDiscoveryManager and register the service using a JoinManager. This is where the calls to the getProxy and getAttributes are made into subclasses. protected void init() throws IOException { try { lookupDiscMgr = new LookupDiscoveryManager(groups, locators, null); } catch (IOException e) { System.err.println("JoinState:Problem starting discovery"); e.printStackTrace(); throw new IOException("JoinState: Problem starting discovery:" + e.getLocalizedMessage()); } // subclasses will supply the proxy object proxy = getProxy(); 183 // subclasses will supply the service attributes attributes = getAttributes(); /* Register this service with any configured lookup services */ if (serviceID == null) { // First instance ... need service id joiner = new JoinManager( proxy, // service object attributes, // service attributes new SrvcIDListener(), // ServiceIDListener lookupDiscMgr, // DiscoveryManagement - default null); // LeaseRenewalManager - default } else { // Rejoin with (recovered) state information joiner = new JoinManager( proxy, // service object attributes, // service attributes serviceID, // Service ID lookupDiscMgr, // DiscoveryManagement - default null); // LeaseRenewalManager - default } } Logging You will need to provide a framework for logging service information. The ServiceImpl class uses some of the same techniques that are implemented in Jini services, such as mercury and norm. A service must be capable of capturing configurations and changes to service definition. These include: • • • • The set of lookup attributes The set of lookup groups The set of lookup locators The service ID In addition, a service might need to store service-specific information such as session data or transaction state. If you have already run the service and received a service ID from the LUS, then you must use that ID on subsequent restarts. The first time the service runs, you supply a net.jini.lookup.ServiceIDListener to JoinManager. The ServiceIDListener is notified by the manager when the service ID is received from the LUS. The serviceIDNotify method is invoked with the service ID. You save the ID by writing a log record to persistent storage. Listing 8.10 The SrvcIDListener Class /** * Class which handles the callback of a service ID assignment * by the JoinManager */ private class SrvcIDListener implements ServiceIDListener { public SrvcIDListener() { 184 super(); } /** * The JoinManager will invoke this method when it receives a * valid ServiceID from a lookup service. */ public void serviceIDNotify(ServiceID id) { // Set the ID setServiceID(id); System.out.println("Received service id " + id); // Log this event try { store.snapshot(); } catch(IOException e) { e.printStackTrace(); } } } /** * Utility method for setting the service's ID obtained from * the lookup registration process. */ private void setServiceID(ServiceID id) { serviceID = id; } Your persistent storage is created in the ServiceImpl constructor. try { store = new PersistentStore(logDir, new ServiceLogHandler(), this); } catch (IOException e) { e.printStackTrace(); } This PersistentStore class is a slight modification to the PersistentStore class used in the lookup discovery service supplied with the Jini distribution. You create a PersistentStore by passing the following: • • • The directory where the store should persist its data An object that implements com.sun.jini.releableLog.LogHandler class that will process the log and recover the service's state A ServiceImpl object that is called back after an update so it can decide whether or not to take a snapshot. Listing 8.11 The PersistentStore Class class PersistentStore { // The ReliableLog reference private ReliableLog log; // Support for multiple readers – one writer semantics final private ReadersWriter mutatorLock = new ReadersWriter(); /** Location of the persistent store */ final private File storeLocation; /** Object that handles the recovery of logs */ final private LogHandler logHandler; 185 /** The Service we are part of */ final private ServiceImpl server; /** Number of updates since last snapshot */ private int updateCount; PersistentStore(String logDir, LogHandler logHandler, ServiceImpl server) throws IOException { this.logHandler = logHandler; this.server = server; storeLocation = new File(logDir); log = new ReliableLog(storeLocation.getCanonicalPath(), logHandler); // this will be passed to the ServiceImpl log handler log.recover(); } The com.sun.jini.reliableLog package contains a set of classes that can assist you in writing a reliable log process. You simply have to extend the abstract class LogHandler. When you create the PersistentStore, a com.sun.jini.reliableLog.ReliableLog is created. The ReliableLog constructor requires a user-defined class that extends com.sun.jini.reliableLog.LogHandler. Listing 8.12 The LogHandler Class public abstract class LogHandler { /** * Creates a LogHandler for a ReliableLog. */ public LogHandler() { } /** * This callback is invoked when the client calls the snapshot method * of ReliableLog. */ public abstract void snapshot(OutputStream out) throws Exception; /** * This callback is invoked when the client calls the recover method of * ReliableLog. */ public abstract void recover(InputStream in) throws Exception; /** * This callback is invoked when the client calls the update method of * ReliableLog. */ public void writeUpdate(OutputStream out, Object value) throws Exception 186 { ObjectOutputStream s = new ObjectOutputStream(out); s.writeObject(value); s.flush(); } /** * This callback is invoked during recovery, once for every record in the * log. After reading the update, this method invokes the applyUpdate * (abstract) method in order to execute the update. */ public void readUpdate(InputStream in) throws Exception { ObjectInputStream s = new ObjectInputStream(in); applyUpdate(s.readObject()); } /** * This callback is invoked during recovery, once for every record in the * log. After reading the update, this method is invoked in order to * execute the update. */ public abstract void applyUpdate(Object update) throws Exception; } To create an implementation of a LogHandler, you must implement the abstract methods applyUpdate, snapshot, and recover. The ServiceLogHandler is an implementation of the Jini LogHandler abstract class. It is an inner class, defined in the ServiceImpl abstract class. Listing 8.13 The ServiceLogHandler Class // the ServiceImpl (inner class) reliable log file handler public class ServiceLogHandler extends com.sun.jini.reliableLog.LogHandler { public void applyUpdate(Object update) throws Exception { } public void snapshot(OutputStream out) throws IOException { ObjectOutputStream oostream = new ObjectOutputStream(out); oostream.writeObject(serviceID); // get the attributes from the JoinManager writeAttributes(joiner.getAttributes(), oostream); // the groups and lookups from the lookup discovery manager oostream.writeObject(lookupDiscMgr.getGroups()); oostream.writeObject(lookupDiscMgr.getLocators()); oostream.flush(); } public void recover(InputStream in) throws Exception { ObjectInputStream oistream = new ObjectInputStream(in); serviceID = (ServiceID)oistream.readObject(); attributes = readAttributes(oistream); groups = (String[])oistream.readObject(); 187 locators = (LookupLocator[])oistream.readObject(); } } When your Jini service starts, the PersistentStore calls recover on the ReliableLog, which passes the request to the recover method of the user-defined ServiceLogHandler. The recover method of the ServiceLogHandler (see Listing 8.12) provides a default implementation for restoring the service ID and the groups and locators used to configure the JoinManager. This provides you with the persistent state required to restore the service and implement good Jini citizenship behavior. A snapshot provides a moment-in-time capture of the current state of a service. A snapshot is taken of the current state of the service after the service is started. The snapshot method of the ServiceLogHandler (see Listing 8.12) retrieves the current state of the service from the Join Manager and the LookupDiscoveryManager. It then saves the service information to safe storage. The synchronization of processing a single writer is achieved in the Jini service through a "reader/writer" mutex construct. This process enables only one writer at a time, but permits an unlimited number of simultaneous readers as long as no writer has locked the resource. The following code fragment from the PersistentStore class ensures that only one writer can update the service log. It grabs a write lock, and then calls snapshot on the ServiceLog Handler. It releases the write lock once the log update has completed. void snapshot() throws IOException { try { // Using write lock because we want an exclusive lock mutatorLock.writeLock(); updateCount = 0; // Don't need to sync on this because // mutatorLock.writeLock() gives us an exclusive lock log.snapshot(); } finally { // Using write lock because we want an exclusive lock mutatorLock.writeUnlock(); } } The PersistentStore update method ensures that the locking process is synchronized and logs the update Object. It then calls the ServiceImpl updatePerformed method to allow the service to determine whether a snapshot should be taken. void update(Object o) { final Long lockStateVal = (Long)lockState.get(); if (lockStateVal == null || lockStateVal.longValue() == 0) throw new IllegalStateException("PersistentStore.update:" + "Must acquire mutator lock before calling update()"); synchronized (this) { try { log.update(o, true); updateCount++; server.updatePerformed(updateCount); } catch (IOException e) { // $$$ should probably be propagating this exception System.err.println("IOException while updating log"); 188 e.printStackTrace(); } } } The updatePerformed method of ServiceImpl can be overridden to perform servicespecific processing, such as determining what information should be captured and how often. Listing 8.14 The SnapshotThread Class // ServiceImpl called by PersistentStore public void updatePerformed(int updateCount) { snapshotter.takeSnapshot(); } /** * Thread that performs the actual snapshots, done in a separate thread * so it will not hang up in progress remote calls */ private class SnapshotThread extends Thread { /** Create a daemon thread */ private SnapshotThread() { super("snapshot thread"); setDaemon(true); } /** Signal this thread that it should take a snapshot */ private synchronized void takeSnapshot() { notifyAll(); } public void run() { while (!isInterrupted()) { synchronized (this) { try { wait(); } catch (InterruptedException e) { return; } } try { store.snapshot(); } catch (InterruptedIOException e) { return; } catch (Exception e) { e.printStackTrace(); } } } } The SnapShotThread is started in the init method of ServiceImpl. It calls the PersistentStore snapshot method, which performs the concurrency checks mentioned and ensures a single log writer. Service Administration Implementation 189 You now have the necessary framework to do the following: • • • • • Export a service to RMI Start the logging and recovery process Register the service with the LUS Export the client proxy Capture the fundamental administration elements to persistent storage Now let's look at how to wire this framework into the administration interfaces. It is surprisingly simple and straightforward. JoinAdmin Implementation Most services will implement the JoinAdmin interface because it provides methods to control the three fundamental elements of Jini service composition and behavior: attributes, groups, and locators. By using the JoinManager (joiner) and the LookUpDiscovery manager (lookupDiscMgr) that are defined in the ServiceImpl class, join administration can simply be delegated to these managers as shown in Listing 8.15. Attribute sets are changed, added, and retrieved by using the JoinManager. When you change service attributes, the JoinManager will update the associated attributes in all registered LUSes. Groups and locators are managed by using the LookupDiscovery manager. Managing the set of lookup locators allows you to control the addresses the service uses for unicast discovery. Managing the set of groups allows you to control the LUSes the service will join. When you change the locators or groups the service supports, the discovery and join process will update all relevant LUS registrations. Listing 8.15 The ServiceImpl Class Continued /* Implementation of the JoinManager Interface */ ** This method allows you to augment the current service definition with */ additional entries using the join manager public void addLookupAttributes(Entry[] attrSets) throws RemoteException { joiner.addAttributes(attrSets); } /* This method allows you to add additional supported groups using the */ lookup discovery manager public void addLookupGroups(java.lang.String[] groups) throws RemoteException { try { lookupDiscMgr.addGroups(groups); } catch (IOException e) { throw new RemoteException(e.toString()); } } /* This method allows you to add additional locators using the */ lookup discovery manager public void addLookupLocators(LookupLocator[] locators) throws RemoteException { lookupDiscMgr.addLocators(locators); } // The following methods provide retrieval of attributes, groups, and locators 190 public Entry[] getLookupAttributes() throws RemoteException { return joiner.getAttributes(); } public java.lang.String[] getLookupGroups() throws RemoteException { return lookupDiscMgr.getGroups(); } public LookupLocator[] getLookupLocators() throws RemoteException { return lookupDiscMgr.getLocators(); } // Modification of attributes will be reflected in LUS registrations public void modifyLookupAttributes(Entry[] attrSetTemplates, Entry[] attrSets) throws RemoteException { joiner.modifyAttributes(attrSetTemplates, attrSets); } // Removal of groups will trigger notifications to interested parties through the ServiceRegistrar public void removeLookupGroups(java.lang.String[] groups) throws RemoteException { lookupDiscMgr.removeGroups(groups); } // Removal of locators eliminates specific LUS hosts from being discovered and thereby joined public void removeLookupLocators(LookupLocator[] locators) throws RemoteException { lookupDiscMgr.removeLocators(locators); } // Will impact LUS registrations public void setLookupGroups(java.lang.String[] groups) throws RemoteException { try { lookupDiscMgr.setGroups(groups); } catch (IOException e) { throw new RemoteException(e.toString()); } } // Will impact LUS registrations public void setLookupLocators(LookupLocator[] locators) throws RemoteException { lookupDiscMgr.setLocators(locators); } StorageLocationAdmin Implementation This implementation of the StorageLocationAdmin interface enables administrative clients to specify a persistent storage location. The service will use this location to save, update, and recover logged information. The init method defined in ServiceImpl is setting this location (logDir) from a command line argument. 191 /* ** Discovery Storage Location implementation */ // return the current logging directory public String getStorageLocation() throws RemoteException { return new String(logDir); } // warning does not try to synchronize existing logs or process public void setStorageLocation(String location) throws IOException, RemoteException { logDir = location; } } DestroyAdmin Implementation This implementation of the DestroyAdmin interface enables administrative clients to remove the persistent data associated with the service and unregister the service with any activation system and LUS. The ServiceImpl implementation calls the PersistentStore which invokes the ReliableLog to close the log file, remove all related files from the log directory, and delete the directory. /* ** Destroy Admin implementation */ public void destroy() throws RemoteException { try { store.destroy(); } catch (IOException io) { throw new RemoteException("Unable to destroy log " + io.getLocalizedMessage()); } } Client-Side Implementation The client-side implementation simply delegates the calls to the remote server. This class can be extended to satisfy unique service administration requirements as seen in Figure 8.4. Figure 8.4. Clients invoke the ServiceAdminProxy, which implements the ServiceAdmin interface. In this example, the proxy delegates the request to service implementation. 192 The ServiceAdminProxy is the client-side implementation of the ServiceAdmin interface. When the client determines the service supports the Administrable interface, it invokes the getAdmin method on the ServiceImpl implementation. The getAdmin method is overridden by implementations of the ServiceImpl abstract class. You can create and return a ServiceAdmin Proxy in your getAdmin implementation to provide the functionality discussed in this chapter. The proxy simply delegates the calls to the remote server. Listing 8.16 The ServiceAdminProxy Class package org.jworkplace.admin; import import import import import import import import import import java.rmi.Remote; java.rmi.RemoteException; java.io.Serializable; java.io.IOException; net.jini.admin.JoinAdmin; net.jini.lookup.DiscoveryAdmin; net.jini.core.entry.Entry; net.jini.core.discovery.LookupLocator; com.sun.jini.admin.DestroyAdmin; com.sun.jini.admin.StorageLocationAdmin; import org.jworkplace.service.*; public class ServiceAdminProxy implements ServiceAdmin, Serializable { private final ServiceBackEnd server; public ServiceAdminProxy(ServiceBackEnd server) throws RemoteException { super(); this.server = server; } public void addLookupAttributes(Entry[] attrSets) throws RemoteException { 193 server.addLookupAttributes(attrSets); } public void addLookupGroups(java.lang.String[] groups) throws RemoteException { server.addLookupGroups(groups); } public void addLookupLocators(LookupLocator[] locators) throws RemoteException { server.addLookupLocators(locators); } public Entry[] getLookupAttributes() throws RemoteException { return server.getLookupAttributes(); } public java.lang.String[] getLookupGroups() throws RemoteException { return server.getLookupGroups(); } public LookupLocator[] getLookupLocators() throws RemoteException { return server.getLookupLocators(); } public void modifyLookupAttributes(Entry[] attrSetTemplates, Entry[] attrSets) throws RemoteException { server.modifyLookupAttributes(attrSetTemplates, attrSets); } public void removeLookupGroups(java.lang.String[] groups) throws RemoteException { server.removeLookupGroups(groups); } public void removeLookupLocators(LookupLocator[] locators) throws RemoteException { server.removeLookupLocators(locators); } public void setLookupGroups(java.lang.String[] groups) throws RemoteException { server.setLookupGroups(groups); } public void setLookupLocators(LookupLocator[] locators) throws RemoteException { server.setLookupLocators(locators); } /* ** Discovery Storage Location implementation */ 194 public String getStorageLocation() throws RemoteException { return server.getStorageLocation(); } public void setStorageLocation(String location) throws IOException, RemoteException { server.setStorageLoaction(location); } /* ** Destroy Admin implementation */ public void destroy() throws RemoteException { server.destroy(); } Summary One of the primary goals of Jini is to reduce the amount of administration that is required to establish and maintain a Jini community. Jini has defined a standard set of administration interfaces that services can implement and extend. These interfaces can be used to provide a standard for accessing and controlling service behavior. Service behavior is reflected in the set of groups, attributes, and locators that a service supports. The abstract ServiceImpl class provides you with a base class for defining UnicastRemote Object servers. Using this class, and the associated helper classes, can reduce the amount of development effort required to produce robust Jini services that exhibit good Jini citizenship behavior. 195 Chapter 9. Security in Jini IN THIS CHAPTER • • • • • • • • Defining Security Requirements Jini-Specific Requirements Java Security The Java Virtual Machine Configuring Basic Security with Policy Files Additional Security Considerations and Alternative Techniques In Need of Evolution Summary How is security implemented in Jini networks? This chapter explains the current state of security in Jini technology. It highlights the dependencies on the Java 2 platform, RMI, and the Java language. In addition, common security mechanisms and standards for distributed Jini services are evaluated. Security is a hot topic. With the growth of the Internet, it has become an issue that often headlines the nightly news. These stories can sound more like biological warfare than computer technology, as it has become common to talk of the spread of viruses, bugs, and infections. Despite the heightened awareness and risk, systems are still being deployed that are more tightly coupled to the global infrastructure—the Internet—than ever before. Security is still a risk versus reward proposition. How much security do you need to protect the assets you have? Every Information Technology department has had to answer this question. Unfortunately for most, the security deployed is lacking, leaving the company's systems open to users, partners, and the Internet at large. However, the cures are forthcoming. Defining Security Requirements Traditionally, security is defined as the protection of information, systems, and services against manipulation, mistakes, and disasters. Network security is comprised of authentication, authorization, integrity, confidentiality, and non-repudiation. Authentication is the most common type of network security. It generally involves a user or process demonstrating some form of evidence to prove identity. Such evidence may be information only the user would likely know (a password), or it may be information only the user could produce (signed data using a private key). Authorization involves the ability to enforce access controls upon an authenticated user. Commonly implemented as an access control policy that provides an association between a user's access rights and system resources, such as databases, files, and processes. Integrity ensures that messages are delivered correctly, and that messages in transit have not been tampered with maliciously. 196 Confidentiality and privacy ensure that data cannot be seen or disclosed over the network by outside parties. Encryption is a procedure used to convert text into code in order to prevent any but the intended recipient from reading that data. It can be used to ensure the confidentiality of data, the authentication of the data sender, or the integrity of the data sent. Finally, non-repudiation guarantees that a sender cannot deny having sent a particular message, as well as the reverse—a sender cannot claim to have sent a message that he did not. These processes are fundamental to network security implementations. However, Jini, because of its dynamic nature both in terms of code movement and service discovery, continues to stretch network security in new directions. Security frameworks that address the Jini environment adequately are just now surfacing. Jini-Specific Requirements Jini requires the standard network authentication-type security. In addition, Jini needs to provide an environment that supports standard security processes for mobile and downloaded code. Jini should provide an API that supports a variety of network security implementations, as well as increased application portability across different network security implementations. It should provide integration with other Java platform security mechanisms, such as the Java Authentication and Authorization Service (JAAS), in addition to enabling a variety of access control (authorization) schemes to be used. Additional information on JAAS can be found at http://java.sun.com/products/jaas/. An extensible framework should support security needs and trust relationships that can vary according to the client, server, operation, and data involved. To leverage the power of service discovery, a balance has to be reached between what's visible and reachable and what's protected and confidential. The framework should support mechanisms that are adaptable to specific implementations. These requirements have been difficult to meet. The diversity of systems, security standards, deployment alternatives, and the complexity of integration have impeded progress. A major development area in the next release of Jini—The Davis Project—is an effort to address the need for a Jini technology security architecture (see www.jini.org/projects). As of this writing, the Davis Project has produced the Overture release (currently available for download) that includes the basic security architecture necessary to support many of these requirements. Java by its very nature has been addressing security from its inception. Before diving directly into Jini security, let's discuss the Java security framework. Java Security Java security has been evolving and changing at a rapid pace. Access control has evolved to be far more fine grained with each version of the Java platform. The following list highlights the most significant changes in the JDK releases. 197 • • • JDK 1.0—In the sandbox model, local code is trusted to have full access to system resources, such as the file system; however, downloaded remote code is not trusted, and can access only the limited resources provided inside the sandbox. JDK 1.1—Introduces the concept of digitally signed code for applets. Digitally signed code is treated like local code, with full access to resources, if the public key used to verify the signature is trusted. Unsigned code still runs in the sandbox. Signed applets are delivered, with their respective signatures, in signed jar (Java ARchive) files. JDK 1.2—All code, regardless of whether it is local or remote, can now be subject to a security policy. The security policy defines the set of permissions available for code from various signers or locations and can be configured by a user or a system administrator. Each permission specifies a permitted access to a particular resource, such as read and write access to a specified file or directory, or connect access to a given host and port. The runtime system organizes code into individual domains, each of which encloses a set of classes whose instances are granted the same set of permissions. A domain can be configured to be equivalent to the sandbox, so downloaded code can still be run in a restricted environment if the user or the administrator so chooses. As before, applications run unrestricted by default, but can optionally be subject to a security policy. • JDK 1.3, 1.4—Access control decisions can now be based both on the executing code's CodeSource, as well as on the user running the code, or the Subject. With JDK 1.3, the Java Authentication and Authorization Service was packaged as an extension. In 1.4, this service has moved into the standard runtime distribution. The Java Virtual Machine The Java Virtual Machine (JVM), from a security perspective, provides the sandbox that ensures code runs in a protected space and has limited access to system resources. The key components in the JVM security framework are the following: • • • Class loader Class file verifier Security manager The Class Loader The class loader is used by the JVM to locate and load into memory the classes necessary to execute a Java program. As has been demonstrated with Jini and RMI, not all class files that are needed come from the local file system. Classes can be divided into two categories—trusted and untrusted. Figure 9.1. The Java Virtual Machine. 198 Trusted classes are those classes that are assumed to be safe. In Java 2, these were restricted to only the Java Runtime Environment (JRE) classes. These are the classes that are found in the boot class path. Untrusted classes are all classes that fall outside of the boot class path. This includes the files that are downloaded over the network. These files are verified by the class file verifier. The Class File Verifier The class file verifier is invoked by the class loader to ensure that a file meets certain criteria. A series of checks is performed: 1. File Integrity Check—Ensures that a file has the appropriate signature and length. 2. Class Integrity Check—Ensures that all classes, methods, and fields have legal names and signatures. 3. Bytecode Integrity Check—The runtime behavior of the bytecodes is examined and verified. 4. Runtime Integrity Check—Extends the bytecode integrity check to look for code that actually gets executed. This is to ensure that code that is never executed does not have to go through the entire verification process unnecessarily. The Security Manager The security manager is responsible for the runtime restrictions that are imposed on code after the code has gone through the verification process. The security manager enforces restrictions based on security policy statements. The class java.lang.SecurityManager supports the policy-driven security model. It enables an application to determine, before performing a possibly unsafe or sensitive operation, what the operation is and whether it is being attempted in a security context that will enable the operation to be performed. The application can then allow or disallow the operation. 199 The SecurityManager class contains methods with names that begin with the word check. The invocation of such a check method typically looks like this: SecurityManager security = System.getSecurityManager(); if (security != null) { security.checkXXX(argument, . . . ); } The security manager is given an opportunity to prevent completion of the operation by throwing an exception. A security manager simply returns if the operation is permitted, but throws a SecurityException if the operation is not permitted. The current security manager is set by the setSecurityManager method in the class System. The current security manager is obtained by the getSecurityManager method. if (System.getSecurityManager() == null) { System.setSecurityManager(new SecurityManager()); } RMISecurityManager provides a security manager for use by RMI and Jini applications that use downloaded code. RMI's class loader will not download any classes from remote locations unless a security manager has been set. This is a common mistake in Jini development, because often no warning is given as to why the service has failed. In your examples, you have been setting the security manager in your main methods: System.setSecurityManager(new RMISecurityManager()); To determine whether the code has sufficient permissions, the SecurityManager implementation delegates responsibility to the java.security.AccessController, which obtains an image of the current AccessControlContext and then ensures that the retrieved AccessControlContext contains sufficient permissions for the operation to be permitted. If a requested access is allowed, checkPermission returns. If denied, a SecurityException is thrown. Permissions fall into categories: File, Socket, Net, Security, Runtime, Property, AWT, Reflect, and Serializable. The classes managing these various permission categories are as follows: • • • • • • • • • java.io.FilePermission java.net.SocketPermission java.net.NetPermission java.security.SecurityPermission java.lang.RuntimePermission java.util.PropertyPermission java.awt.AWTPermission java.lang.reflect.ReflectPermission java.io.SerializablePermission See the permissions in the Java 2 SDK permission-related information for additional information. The document includes a table listing the various SecurityManager check methods and the permission(s) the default implementation of each such method requires. It also contains a table of 200 all the version 1.2 methods that require permissions, and for each method tells which permission it requires. Configuring Basic Security with Policy Files To configure security in Jini and other Java-based systems, you define a policy file and associate the file with a process at startup: java -Djava.security.policy=/usr/policy [rest of command] The -Djava.security.policy property designates the location and name of the policy file to associate with the command process. Policy Files Permissions are managed in a text file. The default policy file is named java.policy and is located in your user.home directory. You can determine your system locations, such as user.home by compiling and running the following short program. class ListProperties { public static void main(String args[]) { System.out.println("sun.boot.class.path = " + System.getProperty ("sun.boot.class.path")); System.out.println("java.class.path = " + System.getProperty ("java.class.path")); System.out.println("user.home = " + System.getProperty("user.home")); System.out.println("java.ext.dirs = " + System.getProperty ("java.ext.dirs")); } } A policy file contains a list of entries or directives. grant [signedBy signers] [,codebase URL] { permission permission_class [target][,action][,signedBy signers]; permission ... } ; • • • • signers is replaced with the name of the entity or entities that have signed the code. URL is replaced by the URL address of the location from where the code originated. permission_class is the class type of the permission, such as java.net.SocketPermission or java.io.FilePermission. target is the target identifier of the permission; for instance, the socket number or • action is the permitted action of the permission, such as connect, listen, read, write, filename. and so on. If signers is omitted, then code coming from any signer will be granted the permission. Likewise, a missing URL will grant the permission to code coming from any location. A 201 permission class called AllPermission exists that implies all permissions are granted. As can be seen, the policy file you have been using in the examples grants all permissions to any code from anywhere. This is clearly not desirable in a production environment. Caution The AllPermission class is intended to be used only in a secure test environment! grant { permission java.security.AllPermission "", ""; } ; Protection Domains A protection domain is a set of permission entries associated with a code source. The code source includes a URL of where the code originated, and optionally, the certificates of the entities that have signed the code. Every Java class is uniquely associated with a code source. When a untrusted class is loaded, it is mapped to a protection domain based upon its code source and any signers it might have (see Figure 9.2). Figure 9.2. The protection domain of an untrusted class. The grant entries in the policy file describe the permissions granted to the code source. Classes that have the same permissions but are from different code sources belong to different protection domains. For example, to only grant permission to code signed from a specific user from a specific location, the following code would be used: grant signedBy "rflenner" codebase "http://www.jworkplace.org" { permission permission_class [target][,action][,signedBy signers]; permission ... } ; This could represent a very restrictive policy, because it specifies both signed user code and code origination for the permission given. To grant permission only to code signed from a specific user from any location, you would use this: 202 grant signedBy "rflenner" { permission permission_class [target][,action][,signedBy signers]; permission ... } ; To grant permission only from a specific location to any code, you would use this: grant codebase "file:${ java.class.path} " { permission permission_class [target][,action]; permission ... } ; In this last example, you are specifying the local file system class path as the location. Permissions Relevant to Jini Of course, all permissions are relevant to Jini; however, there are a few that are notable. Jini has introduced a new permission class: net.jini.discovery.DiscoveryPermission. This class signifies permission to connect to an LUS that supports a particular group. The name can be the "*" to signify all groups, the empty string to signify the public group, or a specific group name. permission net.jini.discovery.DiscoveryPermission "*"; Jini relies heavily on the RMI framework. The RMI default server port is 1099; however, if you are downloading code from the client back to the server (callback), RMI will make connections on anonymous ports, and therefore requires access to ports greater than the privileged ports starting at 1024. The following gives permission to all hosts (*) at port 1024 and above. permission java.net.SocketPermission "*:1024-", "connect,accept"; The actions on sockets are accept, connect, listen, and resolve. The action resolve is implied by accept, connect, and listen—that is, those who can listen or accept incoming connections from a host or initiate outgoing connections to a host should be able to look up the name of the remote host. If you are running your HTTP server on port 8080, an entry of the following would be appropriate: permission java.net.SocketPermission "*:8080", "connect"; Service discovery and lookup may require the use of the following multicast addresses. See Chapter 4, "Building on Jini Foundation Concepts," for additional details on multicast addressing. permission java.net.SocketPermission "224.0.1.84", "connect,accept"; permission java.net.SocketPermission "224.0.1.85", "connect,accept"; Sample Policy File Listing 9.1 is the policy file that is used with the reggie implementation of the lookup service. Listing 9.1 reggie Policy File /* 203 * This is an example policy file for the lookup service's activation group. * You should be able to use this as-is if you use reggie.jar as an * executable jar file, or if the classpath you use when running * com.sun.jini.reggie.CreateLookup is just a single jar file or a single * directory with a trailing slash. Otherwise you need to replace the * codebase string below. If you want the lookup database stored somewhere * other than in /tmp/reggie_log/, then change that as well. */ grant codebase "file:${ java.class.path} " { permission java.io.FilePermission "/tmp/reggie_log", "read,write,delete"; permission java.io.FilePermission "/tmp/reggie_log/-", "read,write,delete"; // uncomment this one if you need lookup to accept file: codebases // permission java.io.FilePermission "<<ALL FILES>>", "read"; permission java.lang.RuntimePermission "modifyThreadGroup"; permission java.lang.RuntimePermission "modifyThread"; permission java.net.SocketPermission "*:1024-", "connect,accept"; // for http: codebases permission java.net.SocketPermission "*:80", "connect"; permission java.net.SocketPermission "224.0.1.84", "connect,accept"; permission java.net.SocketPermission "224.0.1.85", "connect,accept"; permission java.util.PropertyPermission "java.rmi.server.hostname", "read"; permission java.util.PropertyPermission "com.sun.jini.reggie.*", "read"; permission java.util.PropertyPermission "net.jini.discovery.*", "read"; permission net.jini.discovery.DiscoveryPermission "*"; // for transient case only permission java.lang.RuntimePermission "getContextClassLoader"; permission java.lang.RuntimePermission "setContextClassLoader"; } ; The code is unsigned, but specifies a specific location (the class path) to grant permissions for this domain. File permissions are enabled for logging; runtime permissions are enabled for thread management. Socket permissions enable HTTP connections and multicast discovery. The key to understanding this file lies primarily in the socket permissions that are granted. The discovery process requires multicast addresses (224… ), and RMI uses anonymous ports above 1024. In addition, the HTTP server needs to be set to the port running your HTTP server. There are other sample policy files that come with the Jini distribution that can help you determine the settings required. Java-based Security Tools There are three security-related tools in the Java distribution that enable you to configure security for code signing and policy files. You use these tools in conjunction with the jar utility to create signed code and associated policy declarations. The tools are in the java/bin directory and all can be invoked from the command line. keytool The keytool command line utility creates pairs of public and private keys, and imports and exports signed (X509) certificates into a keystore. A keystore is a protected database that 204 contains the trusted certificate entries and the key/certificate entries, each containing a private key and the corresponding public key certificate. The keytool utility can be used to: • • • • • Create private keys and their associated public key certificates Issue certificate requests, which you send to the appropriate certification authority Import certificate replies, obtained from the certification authority you contacted Import public key certificates belonging to other parties as trusted certificates Manage your keystore The private keys are used to digitally sign your code. The public keys are used to verify the signed code and certificates verify that the public key belongs to the signer. Code Signing The jarsigner utility is used to sign and verify the signature of jar files. The jarsigner accesses the keystore to find: • • • The private key when signing a jar file The public key when verifying a signature The certificate when verifying a public key policytool The policytool utility is used to create and modify policy files. It presents a graphical user interface as opposed to a common text editor approach. For specific options and use of the Java security tools, consult the Java security documentation. Additional Security Considerations and Alternative Techniques This section provides an overview of security implementation alternatives, starting with the pervasive presence of firewalls. Firewalls and HTTP Tunnels Firewalls are one of the major inhibitors to network traffic. They are used to block and filter network packets to and from designated TCP/IP addresses and ports. They became extremely popular with the rise of the World Wide Web, when most corporations started exposing technology assets over the public network. Unfortunately (or fortunately, depending on your point-of-view), this has diminished the potential of mobile code and dynamic (multicast) discovery over the Internet. To address this issue, RMI uses two popular techniques to bypass common filtering: direct and indirect forwarding, using HTTP tunnels that encapsulate RMI and the Java Remote Method Invocation Protocol (JRMP). Figure 9.3 demonstrates indirect forwarding, which requires a cgi script to be running on the remote server to forward requests to the RMI server. Because this is still a hit-and-miss proposition without proper system configuration, it has limitations. 205 Figure 9.3. The HTTP tunnel. Secure Socket Layer The Secure Socket Layer (SSL) protocol provides encryption, authentication, and integrity of data by introducing a protocol layer above TCP/IP and defining a handshake process that ensures communicating peers are authenticated. All data in transit is encrypted to ensure confidentiality. The Java Secure Sockets Extension (JSSE) supports SSL 3.0 and Transport Layer Security 1.0. In Chapter 4 you created the JiniShell class. The following code fragments set up your JiniShell for secure communication using SSL. First extend JiniShell, which extends UnicastRemoteObject. Change the constructor to reference an RMISSClientSocketFactory and an RMISSLServerSocketFactory that you will define. public class JiniSSLShell extends JiniShell // create the RMI server using supplied socket factories public JiniSSLShell() throws IOException { super(0, new RMISSLClientSocketFactory(), new RMISSLServerSocketFactory() ); Define a client-side socket factory that implements the RMIClientSocketFactory interface method createSocket using the SSLSocketFactory. Listing 9.2 RMISSLClientSocketFactory Class public class RMISSLClientSocketFactory implements RMIClientSocketFactory, Serializable { public Socket createSocket(String host, int port) throws IOException { SSLSocketFactory factory = (SSLSocketFactory)SSLSocketFactory.getDefault(); SSLSocket socket = (SSLSocket)factory.createSocket(host, port); return socket; } } Define a server-side socket factory that implements the RMIServerSocketFactory interface method createServerSocket using the SSLServerSocketFactory. 206 Listing 9.3 RMISSLServerSocketFactory Class public class RMISSLServerSocketFactory implements RMIServerSocketFactory, Serializable { public ServerSocket createServerSocket(int port) throws IOException { SSLServerSocketFactory ssf = null; try { // set up key manager to do server authentication SSLContext ctx; KeyManagerFactory kmf; KeyStore ks; // get an instance of the SSL factory that supports TLS ctx = SSLContext.getInstance("TLS"); // generate a KeyManagerFactory that implements the X509 algorithm kmf = KeyManagerFactory.getInstance("SunX509"); // get the default Java KeyStore ks = KeyStore.getInstance("JKS"); // load the keystore from the designated file using password char[] passphrase = "passPhrase".toCharArray(); ks.load(new FileInputStream("testkeys"), passphrase); // initialize the factory with the supplied keystore kmf.init(ks, passphrase); // initialize the context with the factory key managers ctx.init(kmf.getKeyManagers(), null, null); // get the server socket factory for this context ssf = ctx.getServerSocketFactory(); } catch (Exception e) { e.printStackTrace(); } // now return our server socket return ssf.createServerSocket(port); } } Instances of the SSLContext class represent a secure socket protocol implementation, which acts as a factory for secure socket factories. The KeyManagerFactory class acts as a factory for key managers based on a source of key material. Each key manager manages a specific type of key material for use by secure sockets. The key material is based on a keystore and/or provider-specific sources. The JSSE was an optional package (extension) prior to JDK 1.4, and was a separate download from the core distribution. As of JDK 1.4, JSSE has been integrated into the core platform. Java Authentication and Authorization Service (JAAS) The Java Authentication and Authorization Service (JAAS) is designed to provide a framework and standard programming interface for authenticating users and for assigning privileges. 207 Together with Java 2, an application can provide code-centric access control, user-centric access control, or a combination of both. This is an important framework for the future of Jini security. It is not a service in the sense of a Jini service—it does not register with the lookup service—but it will be critical to providing a framework for security. JAAS defines a number of core classes that are central to providing authentication and authorization. As mentioned at the start of this chapter, authentication confirms identity, and authorization provides access control. The JAAS core classes can be broken into 3 categories—common, authentication, and authorization. Common Classes The JAAS common classes are Subject, Principal, and classes associated with credentials. Subject and Principal JAAS uses the term subject to refer to any user of a computing service. Both users and computing services represent subjects. The term principal represents a name associated with a subject. Because subjects may have multiple names (potentially one for each service with which it interacts), a subject comprises a set of principals. Listing 9.4 Principal Class public interface Principal { public String getName(); } Listing 9.5 Subject Class public final class Subject { public Set getPrincipals() { } To authorize access to resources, applications first need to authenticate the source of the request. The JAAS framework defines the term Subject to represent the source of a request. A Subject may be any entity, such as a person or service. Once authenticated, a javax.security.auth.Subject is populated with associated identities, or Principals. A Subject may have many Principals. For example, a person might have a name Principal ("John Doe") and a SSN Principal ("123-45-6789"), which distinguish him from other Subjects. Credentials A credential contains information used to authenticate the subject to new services. Such credentials include passwords, Kerberos tickets, and public key certificates. JAAS credentials may be any type of object. Therefore, existing credential implementations (java.security.cert.Certificate, for example) can be incorporated into JAAS. Third party credential implementations may also be plugged into the JAAS framework. JAAS divides each Subject's credentials into two sets. One set contains the Subject's public credentials (public key certificates, Kerberos tickets, and so on). The second set stores the Subject's private credentials (private keys, encryption keys, passwords, and so on). To access a 208 Subject's public credentials, no permissions are required. However, access to a Subject's private credential set is security-checked. Listing 9.6 Subject ... public Set getPublicCredentials() { } // not security checked public Set getPrivateCredentials() { } // security checked } To modify or operate upon a Subject's Principal Set, public credential Set, or private credential Set, callers use the methods defined in the java.util.Set class. The following example demonstrates this: Subject subject; Principal principal; Object credential; // add a Principal and credential to the Subject subject.getPrincipals().add(principal); subject.getPublicCredentials().add(credential); Authentication Classes The JAAS authentication classes—LoginContext, LoginModule, CallbackHandler, and Callback—are central to authentication. Pluggable and Stackable Authentication (PAM) The JAAS authentication framework is based on Pluggable and Stackable Authentication (PAM), and therefore supports an architecture that enables system administrators to plug in the appropriate authentication services to meet their security requirements. The architecture also enables applications to remain independent from the underlying authentication services. The JAAS LoginContext class represents a Java implementation of the PAM framework. The LoginContext consults a configuration that determines the authentication service, or LoginModule, that gets plugged in under that application. Listing 9.7 LoginContext Class public final class LoginContext { public LoginContext(String name) { } public void login() { } // two phase process public void logout() { } public Subject getSubject() { } // get the authenticated Subject } Listing 9.8 LoginModule Class public interface LoginModule { boolean login(); // 1st authentication phase boolean commit(); // 2nd authentication phase boolean abort(); boolean logout(); } 209 JAAS, like PAM, supports the notion of stacked LoginModules. To guarantee that either all LoginModules succeed or none succeed, the LoginContext performs the authentication steps in two phases. In the first phase, or the 'login' phase, the LoginContext invokes the configured LoginModules and instructs each to attempt the authentication. If all the necessary LoginModules successfully pass this phase, the LoginContext then enters the second phase and invokes the configured LoginModules again, instructing each to formally 'commit' the authentication process. During this phase, each LoginModule associates the relevant authenticated principals and credentials with the subject. If either the first phase or the second phase fails, the LoginContext invokes the configured LoginModules and instructs each to 'abort' the entire authentication attempt. Each LoginModule then cleans up any relevant state it had associated with the authentication attempt. To authenticate a Subject, the following steps are performed: 1. An application instantiates a LoginContext. 2. The LoginContext consults a Configuration to load all of the LoginModules configured for that application. 3. The application invokes the LoginContext's login method. 4. The login method invokes all of the loaded LoginModules. Each LoginModule attempts to authenticate the Subject. Upon success, the LoginModules associate relevant Principals and credentials with the Subject. 5. The LoginContext returns the authentication status to the application. 6. If authentication succeeds, the application retrieves the authenticated Subject from the LoginContext. The following code sample demonstrates the calls necessary to authenticate and logout a Subject: // let the LoginContext instantiate a new Subject LoginContext lc = new LoginContext("JWorkPlace"); try { // authenticate the Subject lc.login(); System.out.println("authentication successful"); // get the authenticated Subject Subject subject = lc.getSubject(); ... // all finished -- logout lc.logout(); } catch (LoginException le) { System.out.println("authentication unsuccessful"+le.printStackTrace()); } To instantiate a LoginModule, a LoginContext expects each LoginModule to provide a public constructor that takes no arguments. Then, to initialize a LoginModule with 210 the relevant information, a LoginContext calls the LoginModule's initialize method. void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options); The following method begins the authentication process: boolean login() throws LoginException; The following method completes and finalizes the authentication process: boolean commit() throws LoginException; If Phase 1 of the authentication process was successful, then this method continues with Phase 2— associating relevant Principals, public credentials, or private credentials with the Subject. If Phase 1 failed, then this method removes any previously stored authentication state, such as usernames or passwords. CallbackHandler is an interface with one method to implement: void handle(Callback[] callbacks) throws java.io.IOException, UnsupportedCallbackException; Underlying security services make requests for different types of information by passing individual Callbacks to the CallbackHandler. The CallbackHandler implementation decides how to retrieve and display information depending on the Callbacks passed to it. For example, if the underlying service needs a username and password to authenticate a user, it uses a NameCallback and PasswordCallback. The CallbackHandler can then choose to prompt for a username and password serially, or to prompt for both in a single window: public interface Callback { } Callback implementations simply provide the means to pass requests to applications, and for applications, if appropriate, to return requested information back to the underlying security services. Upon successful authentication of a Subject, fine-grained access controls can be placed upon that Subject by invoking the Subject.doAs methods. The permissions granted to that Subject are configured in a JAAS Policy. Authorization Classes The JAAS authorization classes Policy, AuthPermission, and PrivateCredentialPermission are central to authorization. Policy is an abstract class for representing the system policy for Subject-based authorization. A subclass implementation of this class provides a means to specify an access control policy. An AuthPermission class is for authentication permissions. An AuthPermission contains a name (also referred to as a "target name") but no actions list; you either have the named permission or you don't. 211 The PrivateCredentialPermission class is used to protect access to private credentials belonging to a particular Subject. The Subject is represented by a Set of Principals. After authentication has successfully completed, JAAS provides the capability to enforce access controls on the principals associated with the authenticated subject. The JAAS principal-based access controls supplement the existing Java 2 codesource-based access controls. Principal-Based Access Control The following example depicts a codesource-based policy entry currently supported by the default policy provided with Java 2. This entry grants code loaded from foo.com, and signed by foo; permission to read all files in the cdrom directory and its subdirectories. Because no principal information is included with this policy entry, the code will always be able to read files from the cdrom directory, regardless of who executes it. grant Codebase "http://foo.com", Signedby "foo" { permission java.io.FilePermission "/cdrom/-", "read"; } The next example is a principal-based policy entry supported by JAAS. This sample entry grants code loaded from bar.com, signed by bar, and executed by duke; and permission to read only those files located in the /cdrom/duke directory. To be executed by duke, the Subject affiliated with the current access control context must have an associated principal of class bar.Principal, whose getName method returns duke. Note that if the code from bar.com, signed by bar, ran standalone (it was not executed by duke), or if the code was executed by any principal other than duke, then it would not be granted the FilePermission. Also note that if the JAAS policy entry did not specify the Codebase or Signedby information, then the entry's FilePermission would be granted to any code running as duke. grant Codebase "http://bar.com, Signedby "bar", Principal bar.Principal "duke" { permission java.io.FilePermission "/cdrom/duke/-", "read"; } Access Control Implementation The Java 2 runtime enforces access controls via the java.lang.SecurityManager, and is consulted anytime untrusted code attempts to perform a sensitive operation (access the local file system, for example). To determine whether the code has sufficient permissions, the SecurityManager implementation delegates responsibility to the java.security.Access Controller, which first obtains an image of the current AccessControlContext, and then ensures that the retrieved AccessControlContext contains sufficient permissions for the operation to be permitted. JAAS supplements this architecture by providing the method Subject.doAs to dynamically associate an authenticated subject with the current AccessControlContext. Hence, as subsequent access control checks are made, the AccessController can base its decisions on both the executing code itself, and the principals associated with the Subject. public final class Subject { // associate the subject with the current // AccessControlContext and execute the action public static Object doAs(Subject s, java.security.PrivilegedAction action) { } 212 } The doAs implementation associates the Subject with the current AccessControlContext and then executes the action. When security checks occur during execution, the Java 2 SecurityManager queries the JAAS policy, updates the current AccessControlContext with the permissions granted to the Subject and the executing codesource, and then performs its regular permission checks. When the action finally completes, the doAs method simply removes the subject from the current AccessControlContext and returns the result back to the caller. To associate a subject with the current AccessControlContext, the doAs method uses an internal JAAS implementation of the java.security.DomainCombiner interface, newly introduced in version 1.3 of the Java 2 SDK. It is through the JAAS DomainCombiner that the existing Java 2 SecurityManager can be instructed to query the JAAS policy without requiring modifications to the SecurityManager itself. Details of the interaction between the Java 2 SecurityManager and DomainCombiners are documented in the documentation for the java.security.DomainCombiner interface. JAAS Example The following sample configuration file defines a single login module. /** Login Configuration for JWorkPlace **/ JWorkPlace { org.jworkplace.login.WorkPlaceLoginModule required debug=true; } ; A login module can be associated with a service by setting the property: Djava.security.auth.login.config==/usr/JiniServices/CMS/config/login. config You will need to grant the necessary security permissions to your codebase. If you are using JDK 1.3, then the JAAS is not part of the core distribution and therefore would require a grant statement similar to the following: /** Java 2 Access Control Policy for the JAAS Application **/ /* grant the JAAS core library AllPermission */ grant codebase "file:/usr/java/jdk1.3/jre/lib/ext/jaas.jar" { permission java.security.AllPermission; } ; /* grant the sample LoginModule AllPermission */ grant codebase "file:/usr/JWorkPlace/lib/login.jar" { permission java.security.AllPermission; } ; grant codebase "file:/usr/JWorkPlace/lib/service.jar" { permission permission permission permission javax.security.auth.AuthPermission "createLoginContext"; javax.security.auth.AuthPermission "doAs"; java.util.PropertyPermission "java.home", "read"; org.jworkplace.login.AccountPermission "createAccount"; 213 } ; A login policy can be associated with a service by setting the property: -Djava.security.policy==/usr/JiniServices/CMS/config/login.policy The following policy extends the Java 2 codebase policy with Subject-based access control. This grant entry provides the necessary permissions to perform a sensitive operation provided in the service.jar to any Subject that has an associated WorkPlacePrincipal. /** Subject-Based Access Control Policy for the JWorkPlace Application **/ grantcodebase "file:/usr/JWorkPlace/lib/service.jar", Principal org.jworkplace.login.WorkPlacePrincipal * { permission org.jworkplace.login.AccountPermission "createAccount"; } ; A login authorization policy can be associated with a service by setting the property: Djava.security.auth.policy==/usr/JiniServices/CMS/config/login_jaas.p olicy The following LoginHandler class implements the CallbackHandler interface. Listing 9.9 LoginHandler Class package org.jworkplace.login; import import import import import import import import java.io.*; java.util.*; java.security.Principal; javax.security.auth.*; javax.security.auth.callback.*; javax.security.auth.login.*; javax.security.auth.spi.*; com.sun.security.auth.*; public class LoginHandler implements CallbackHandler { LoginContext lc; NameCallback nameCallback; PasswordCallback passwordCallback; int loginTries = 0; String user; char[] password; // create the LoginContext using the JWorkPlace configuration public LoginHandler() { try { lc = new LoginContext("JWorkPlace", new Subject(), this); } catch (LoginException le) { le.printStackTrace(); System.exit(-1); } } 214 // The Callback Interface // provides the types of callbacks used to interface with the LoginModule public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { for (int i = 0; i < callbacks.length; i++) { if (callbacks[i] instanceof TextOutputCallback) { // display the message according to the specified type TextOutputCallback toc = (TextOutputCallback)callbacks[i]; switch (toc.getMessageType()) { case TextOutputCallback.INFORMATION: System.out.println(toc.getMessage()); break; case TextOutputCallback.ERROR: System.out.println("ERROR: " + toc.getMessage()); break; case TextOutputCallback.WARNING: System.out.println("WARNING: " + toc.getMessage()); break; default: throw new IOException("Unsupported message type: " + toc.getMessageType()); } } else if (callbacks[i] instanceof NameCallback) { nameCallback = (NameCallback)callbacks[i]; nameCallback.setName(user); } else if (callbacks[i] instanceof PasswordCallback) { passwordCallback = (PasswordCallback)callbacks[i]; passwordCallback.setPassword(password); } else { throw new UnsupportedCallbackException (callbacks[i], "Unrecognized Callback"); } } } // user defined login method which simply counts the number of login attempts // and sets the user and password which will be used in the callback above public boolean login(String user, char[] password) { if(loginTries > 2) { System.out.println("Sorry charlie..."); return false; } else { this.user = user; this.password = password; } loginTries++; try { // attempt authentication on the LoginContext lc.login(); 215 // if we return with no exception, authentication succeeded return true; } catch (AccountExpiredException aee) { System.out.println("Your account has expired. " + "Please notify your administrator."); } catch (CredentialExpiredException cee) { System.out.println("Your credentials have expired."); } catch (FailedLoginException fle) { System.out.println("Authentication Failed"); } catch (Exception e) { System.out.println("Unexpected Exception - unable to continue"); e.printStackTrace(); } // authentication did not succeed return false; } } The following WorkPlacePrincipal class implements the Principal interface. Listing 9.10 WorkPlacePrincipal Class package org.jworkplace.login; import java.security.Principal; public class WorkPlacePrincipal implements Principal, java.io.Serializable { private String name; public WorkPlacePrincipal(String name) { if (name == null) throw new NullPointerException("illegal null input"); this.name = name; } public String getName() { return name; } public String toString() { return("WorkPlacePrincipal: " + name); } public boolean equals(Object o) { if (o == null) return false; if (this == o) return true; if (!(o instanceof WorkPlacePrincipal)) return false; 216 WorkPlacePrincipal that = (WorkPlacePrincipal)o; if (this.getName().equals(that.getName())) return true; return false; } public int hashCode() { return name.hashCode(); } } The following WorkPlaceLoginModule class implements the LoginModule interface. The specific validation of user/password is not shown. You would provide your specific implementation. Listing 9.11 WorkPlaceLoginModule Class package org.jworkplace.login; import import import import import import java.util.*; java.io.IOException; javax.security.auth.*; javax.security.auth.callback.*; javax.security.auth.login.*; javax.security.auth.spi.*; public class WorkPlaceLoginModule implements LoginModule { // initial state private Subject subject; private CallbackHandler callbackHandler; private Map sharedState; private Map options; // configurable option private boolean debug = false; // the authentication status private boolean succeeded = false; private boolean commitSucceeded = false; // username and password private String username; private char[] password; private WorkPlacePrincipal userPrincipal; // the initialize method is called by the LoginContext public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options) { this.subject = subject; this.callbackHandler = callbackHandler; this.sharedState = sharedState; this.options = options; } 217 // this is the login method that is called by the LoginContext public boolean login() throws LoginException { // prompt for a username and password if (callbackHandler == null) throw new LoginException("Error: no CallbackHandler available "); // Two Callback are created, one for the name and one for the password Callback[] callbacks = new Callback[2]; callbacks[0] = new NameCallback("JWorkPlace username: "); callbacks[1] = new PasswordCallback("JWorkPlace password: ", false); try { // this is the callback to our LoginHandler callbackHandler.handle(callbacks); // get the username and password provided in the callback username = ((NameCallback)callbacks[0]).getName(); char[] tmpPassword = ((PasswordCallback)callbacks[1]).getPassword(); if (tmpPassword == null) { // treat a NULL password as an empty password tmpPassword = new char[0]; } password = new char[tmpPassword.length]; System.arraycopy(tmpPassword, 0, password, 0, tmpPassword.length); ((PasswordCallback)callbacks[1]).clearPassword(); } catch (java.io.IOException ioe) { throw new LoginException(ioe.toString()); } catch (UnsupportedCallbackException uce) { throw new LoginException("Error: " + uce.getCallback().toString()); } // verify the username/password // insert code to verify user / password here // Authentication succeeded succeeded = true; return true; // Authentication failed succeeded = false; throw new FailedLoginException("Password Incorrect"); } // the 2 phases of authentication - success commit public boolean commit() throws LoginException { if (succeeded == false) { return false; } else { // add a Principal (authenticated identity) // to the Subject // assume the user we authenticated is the WorkPlacePrincipal userPrincipal = new WorkPlacePrincipal(username); if (!subject.getPrincipals().contains(userPrincipal)) { subject.getPrincipals().add(userPrincipal); 218 } // clean out state username = null; for (int i = 0; i < password.length; i++) password[i] = ' '; password = null; commitSucceeded = true; return true; } } // the 2 phases of authentication - failure abort public boolean abort() throws LoginException { if (succeeded == false) { return false; } else if (succeeded == true && commitSucceeded == false) { // login succeeded but overall authentication failed succeeded = false; username = null; if (password != null) { for (int i = 0; i < password.length; i++) password[i] = ' '; password = null; } userPrincipal = null; } else { // overall authentication succeeded and commit succeeded, // but someone else's commit failed logout(); } return true; } public boolean logout() throws LoginException { subject.getPrincipals().remove(userPrincipal); succeeded = false; succeeded = commitSucceeded; username = null; if (password != null) { for (int i = 0; i < password.length; i++) password[i] = ' '; password = null; } userPrincipal = null; return true; } } The following WorkPlaceAction class implements the PrivilegedAction interface. The PrivilegedAction interface is implemented by classes that require access control checks to be performed. The run method is called by AccessController.doPrivileged after enabling privileges. Listing 9.12 The WorkPlaceAction Class package org.jworkplace.login; import java.io.File; import java.security.PrivilegedAction; 219 public class WorkPlaceAction implements PrivilegedAction { // run method defined in the PrivilegedAction interface public Object run() { // get the security manager and check if permission has been granted SecurityManager sm = System.getSecurityManager(); sm.checkPermission(new AccountPermission("createAccount")); return null; } } Finally, the AccountPermission class extends BasicPermission. BasicPermission provides a simple base class for creating new permission types. In this case, you create a new AccountPermission class that is checked in the WorkPlaceAction to verify that the thread of control associated with the current Subject has the necessary permission to create an account. package org.jworkplace.login; import java.security.BasicPermission; public final class AccountPermission extends BasicPermission implements java.io.Serializable { public AccountPermission(String name) { this(name,null); } public AccountPermission(String name, String action) { super(name); } } In Need of Evolution As you recall, the security requirements and architecture required to support environments like RMI and Jini are difficult to standardize and control. The Davis Project is being developed to address these security issues (see www.jini.org/projects). The Davis Project The Davis Project is an effort by the Jini technology project team at Sun Microsystems to address the need for a Jini technology security architecture, as well as to contribute other improvements to Jini technology. The Davis Project will ultimately result in a Jini technology starter kit release from Sun, following the Alewife 1.2 release. In the short run, the Davis Project has produced the Overture release. Currently available for download, Overture includes the basic security architecture needed to support further efforts. The Overture Release As stated in the release notes, Overture contains the following components: • A network security programming model and API for remote calls. 220 • • • • • • • • • A programming model and API for exporting remote objects. A configuration programming model and API for deployment-time customization of applications without modifying code. An implementation (on par with JRMP and RMI/IIOP) of the RMI programming model called Jini extensible remote invocation, which supports the export programming model and provides flexible customization of remote communication, including invocation semantics and transport protocols. An implementation of the RMI programming model called secure Jini extensible remote invocation, which supports the network security programming model in addition to the other features supported by non-secure Jini extensible remote invocation. TCP and HTTP transport providers for Jini extensible remote invocation, and SSL and HTTPS transport providers for secure Jini extensible remote invocation. Implementations of the export programming model for JRMP and RMI/IIOP. An RMIClassLoaderSpi provider implementation that enhances codebase annotation retention, supports customized codebase annotations, and provides control over what code can be dynamically downloaded. Extensions to the RMI activation framework to support the network security and export programming models. An RMI activation system daemon implementation named phoenix that supports and uses the network security, configuration, and export programming models as well as the activation extensions. The Davis release will further enhance the security architecture and environment that Jini is capable of supporting. This should provide a catalyst for the use of Jini in new environments, including the Internet. Summary Jini requires standard network authentication-type security. In addition, Jini needs to provide an environment that supports standard security processes for mobile and downloaded code. The diversity of systems, security standards, deployment alternatives, and the complexity of integration have impeded progress in defining security architecture. However, Java security has been evolving and changing at a rapid pace. Access control has evolved to be far more fine-grained with each version of the Java platform. Access control decisions can now be based both on the executing code's CodeSource, as well as on the user running the code, or the Subject. The Java Virtual Machine (JVM), from a security perspective, provides the sandbox that ensures code runs in a protected space, and has limited access to system resources. The key components in the JVM security framework are the following: • • • Class loader Class file verifier Security manager The security manager is responsible for the runtime restrictions that are imposed on code after the code has gone through the verification process. The security manager enforces restrictions based on security policy statements. 221 There are security-related tools in the Java distribution that enable you to configure security for code signing and policy files. Firewalls are one of the major inhibitors to network traffic. They are used to block and filter network packets to and from designated TCP/IP addresses and ports. RMI uses two popular techniques to bypass common filtering—direct and indirect forwarding, using HTTP tunnels that encapsulate RMI and the Java Remote Method Invocation Protocol (JRMP). The Java Authentication and Authorization Service (JAAS) is designed to provide a framework and standard programming interface for authenticating users and for assigning privileges. Together with Java 2, an application can provide code-centric access control, user-centric access control, or a combination of both. This is an important framework for the future of Jini security. A major development area in the next release of Jini—The Davis Project—is an effort to address the need for a Jini technology security architecture. 222 Part III: Developing Applications IN THIS PART 10 GroupWare 11 Workflow 12 Agents 13 Jini as a Web Service 14 Pervasive Computing and Mobile Devices 223 Chapter 10. GroupWare IN THIS CHAPTER • • • • • • Defining the JWorkPlace The Role of XML in Workplace Definition The Supporting Services: Reggie, Outrigger, and Mahalo Client-Side Service Discovery Introducing the ServiceUI Project Summary This chapter provides practical applications and examples of the Jini and JavaSpaces concepts introduced in the prior two sections. You will develop a sample GroupWare application that includes the following: • • • • Content management service Instant messaging service Account management service Visual User Interface techniques The GroupWare example is based on JWorkPlace, which is my original creation. Visit www.jworkplace.org. Defining the JWorkPlace GroupWare applications are becoming more important to businesses and individuals due to the explosion of digital commerce linkage ushered in by the Internet economy. Initially, the term "GroupWare" was used to refer to integrated e-mail and calendar/scheduling software but has evolved to include other functions, such as instant messaging, file sharing, workflow support, and voice/video over IP. The peer-to-peer (P2P) revolution extended GroupWare from the corporate closet to now include the Internet at large. This will have a radical impact on future collaboration possibilities. Individuals and businesses will no longer be bound by specific tools within a community or geographic region. Common interests and objectives will be the driving force. The tools to support the formation and integration of decentralized communication and ad hoc group formation already exist. For example, the popularity of AOL Instant Messenger and ICQ have demonstrated that real-time communication is a viable and often preferred way to communicate. Products such as Napster and FreeNet have proven that decentralized file sharing is possible. Jini and JavaSpaces provide an excellent technology choice to meet this demand for decentralized and ad hoc group formation—Jini with its inherent capability to discover services and devices on a network, and JavaSpaces with its ease in supporting network rendezvous points. Together they provide the necessary infrastructure to build the next generation of GroupWare applications. In this chapter, you turn your attention from service building blocks to service application. In other words, you take the core services that you have been learning and apply them to produce applications that can serve as a foundation for further exploration. The first application to explore is JWorkPlace. 224 The Role of XML in Workplace Definition JWorkPlace was created based on the idea that code fragments (snippets) are often exchanged by developers when developing software projects. Although class libraries and frameworks offer more complete solutions, the developer many times requires only a portion of code to complete a coding effort. The advantage to working with snippets is that fragments are easy to learn and provide a solution focused on a specific requirement. However, the downside is that they tend to be randomly constructed and lack any management in the development process. JWorkPlace was designed to allow code exchange between development team members. In addition, by integrating instant messaging, it provides real-time communication capabilities. Code fragments in the JWorkPlace are defined using XML. A simple code fragment might be defined as <fragment author="anonymous" text=" private static class ShutdownThread extends Thread { private JoinManager joinManager; public ShutdownThread(JoinManager joinManager) { this.joinManager = joinManager; } public void run() { joinManager.terminate(); return; } }" </fragment> A more complete definition of XML might provide information relative to code usage or where detailed information might be located (URL reference). More structure could be added by classifying code fragments within categories, such as usage or source of code definition. The fundamental idea is that development team members can improve on the common cut, copy, paste, and e-mail process that exists today. You are going to extend the basic definition of an XML-based code fragment repository to apply to a more general content management and collaboration platform using Jini and JavaSpaces. The content management system is going to be defined using P2P techniques, as shown in Figure 10.1. Figure 10.1. Jini and JavaSpaces can provide a platform for peer-to-peer discovery and information exchange. 225 Your P2P content management system will first register with a Jini lookup service. A using entity will create an account through the account service to gain access to the content system. Establishing an account allows you to publish a portion of your computer's directory to the shared content system. As a result, you can add files—even though they remain on your PC—to the shared repository. If you have given access to a file to another member in your group, that member can transfer your file to his local machine from your machine. In addition, all group members can share and exchange files located within the group repository. You use Jini as the mechanism to discover the services that comprise the workplace and use JavaSpaces as a point of rendezvous. The Supporting Services: reggie, outrigger, and mahalo Let's look at the supporting services required to build your workplace. In addition to the lookup service, your workplace requires JavaSpaces and the transaction service. JavaSpaces provides a rendezvous point to view content available for exchange. A rendezvous point provides a meeting place on the network. There, you will provide messaging capabilities, event notifications, and content space definitions you want to share with other group members, as seen in Figure 10.2. Figure 10.2. JWorkPlace builds on Jini and JavaSpaces by providing applicationspecific content, account, and messaging capabilities. 226 Account Service AccountManager provides the interface to administering accounts in JWorkPlace. Note that it extends Administrable, so it defines the getAdmin method. Listing 10.1 The AccountManager Interface package org.jworkplace.account; import java.rmi.RemoteException; import java.io.IOException; import net.jini.admin.Administrable; public interface AccountManager extends Administrable { public Account getAccount(String user) throws RemoteException; public Account[] getAccounts() throws RemoteException; public void create(Account account) throws RemoteException; public void update(Account account) throws RemoteException; public void remove(String userName) throws RemoteException; } To access JWorkPlace, you must have an account. An account contains the following information: Listing 10.2 The Account Class package org.jworkplace.account; import net.jini.entry.AbstractEntry; public class Account extends AbstractEntry { public String user; public String password; public String name; public String location; public String primaryHome; public String secondaryHome; public Account() { super(); } } The Account class also extends AbstractEntry, so your accounts are capable of being written to and read from JavaSpaces. The Account class contains the familiar user ID, password, and username properties. In addition it contains reference to a primary and secondary home. The primary and secondary 227 homes are locations where you have registered your interest in a group at JWorkPlace. Location refers to your current IP address. When you create an account, the AccountService persists the account information. As shown in Figure 10.3, AccountService implements the RemoteAccountManager and ServiceAdmin interface and extends the ServiceImpl class defined in Chapter 8, "Service Administration." Figure 10.3. AccountService extends the abstract ServiceImpl class defined in Chapter 8. public class AccountService extends ServiceImpl implements ServiceAdmin, RemoteAccountManager { private Map dataMap; The dataMap is a HashMap used to index accounts. You need to wrap the HashMap or synchronize on some object if you are going to allow multiple threads to change the structure of the map—add/remove mappings. You can use Collections.synchronizedMap to return a synchronized (thread-safe) map backed by the specified map: Map m = Collections.synchronizedMap(new HashMap(...)); The methods within the AccountService simply access the dataMap by mapping users to Account objects through get and put methods. Content Service IndexManager provides the interface to the content management service: Listing 10.3 The IndexManager Interface package org.jworkplace.content; import java.rmi.RemoteException; import java.io.IOException; import net.jini.admin.Administrable; public interface IndexManager extends Administrable { public void addContentSpace(String user, String localPath, String contentSpace) 228 public void removeContentSpace(String user, String localPath, String contentSpace) public SpaceIndex[] list(String user, String contentSpace) public ContentMap[] getContentSpace(String user) public void addContent(String user, String contentSpace, Content content) public void removeContent(String user, String contentSpace, Content content) public Content getContent(String user, String contentSpace, String name) public Content[] getContent(String user, String contentSpace ) public void shareContent(String guest, String owner, String localPath, String contentSpace) public void removeContentShare(String guest, String owner, String localPath, String contentSpace) } The ContentManager implements the IndexManager interface and provides support for creating a space (see Figure 10.4). A space is a shared repository that enables group members to view and exchange files. Figure 10.4. The ContentService extends the abstract ServiceImpl class defined in Chapter 8. It implements the IndexManager, ServiceAdmin, and the Jini DiscoveryListener interfaces. Note The space is actually an index to files located on group member machines. When you create a space, you are merely naming an index that you share with other group members. The files are never moved to a central location. They are accessed peer-to-peer. The ContentManager provides the support necessary to use the JavaSpaces service to read and write SpaceIndex entries using the JavaSpace API. public class ContentManager extends ServiceImpl implements DiscoveryListener, ServiceAdmin, RemoteIndexManager 229 { private private private private Map spaceMap; Map userMap; Map groupMap; JavaSpace space; The spaceMap provides a mapping between user-defined content space names and files in that named content space. The userMap provides a mapping between users and a list of owned content space names. The groupMap provides a mapping between users and a list of nonowned shared content space names. Defining and Sharing Content The Content interface provides the mapping of a name to a location. Specific types, such as file content, would implement this interface. Listing 10.4 The Content Interface package org.jworkplace.content; import java.io.Serializable; import java.rmi.RemoteException; import java.net.URL; public interface Content extends Serializable { public String getLocation() throws RemoteException; public String getName() throws RemoteException; } The returned name and location are content type-specific. For instance, the String of type FileContent is a String representation of a URL. The String of type SpaceContent is a reference to a Space location. The Space class wraps the LookupLocator defined by Jini. The LookupLocator does not use a valid URL as defined by the net.java.URL class. Listing 10.5 The SpaceContent Class package org.jworkplace.content; import java.net.URL; import java.net.MalformedURLException; import java.rmi.RemoteException; public class SpaceContent implements Content { public final String protocol = "jini"; public Space space; public SpaceContent(String host, int port, String contentSpace) { space = new Space(protocol, host, port, contentSpace); } 230 public String getLocation() throws RemoteException { return space.toString(); } public String getName() throws RemoteException { return space.getName(); } } Listing 10.6 The FileContent Class package org.jworkplace.content; import java.net.URL; import java.net.MalformedURLException; import java.rmi.RemoteException; public class FileContent implements Content { public URL url; public final String protocol = "http"; public FileContent(String host, int port, String file) throws MalformedURLException { url = new URL(protocol, host, port, file); } public String getLocation() throws RemoteException { return url.toString(); } public String getName() throws RemoteException { return url.getFile(); } } You use HTTP to transfer files between user machines. An HTTP server will typically be running on your machine. You can use the Jini-supplied HTTP server used for the examples in this book, or you can use an HTTP server of your choice. The following fragment implements the addContentSpace method of the IndexManager interface: public void addContentSpace(String user, String localPath, String contentSpace) throws RemoteException, IOException { try { if(!userMap.containsKey(user)) { List contentList = new ArrayList(); userMap.put(user, contentList); } List contentList = (List)userMap.get(user); int index = -1; index = ( (contentList.lastIndexOf(user) == -1 ) ? contentList.size() : index+1); contentList.add(index, new ContentMap(user, localPath, contentSpace)); spaceMap.put(user+contentSpace, new HashMap()); } catch (Exception e) { e.printStackTrace(); } 231 } The method checks to see whether the user already has a content list defined. If the user does not, the method creates a new content list and then adds a ContentMap entry to the list. The ContentMap contains the mapping between the user, the local directory, and a user-defined space. In this way, you can organize your files in "spaces" that allow you to break from the hierarchical organization inherent in systems today. You can define spaces or containers for files based on other organizational dimensions, such as time, space, or process. The SpaceIndex class extends Index, which extends AbstractEntry and is used to define the content space name and an ID that associates the space with a specific file. Listing 10.7 The SpaceIndex Class package org.jworkplace.content; import net.jini.entry.AbstractEntry; public class SpaceIndex extends Index { public String contentId; public SpaceIndex() { this(null,null); } public SpaceIndex(String spaceId, String contentId) { this.name = spaceId; this.contentId = contentId; } } You read and write SpaceIndex entries to an instance of JavaSpaces using the ContentManager as a proxy to the JavaSpace API. You can therefore run the JavaSpaces service on a server not directly exposed to any client-side access. The SpaceIndex is merely a parallel structure to the spaceMap hash map. You can use this structure to easily integrate event notification and messaging into the workplace environment. The following fragment implements the shareContent method of the IndexManager interface: public void shareContent(String guest, String owner, String localPath, String contentSpace) throws RemoteException, IOException { if(!groupMap.containsKey(guest)) { List contentList = Collections.synchronizedList(new ArrayList()); groupMap.put(guest, contentList); } List contentList = (List)groupMap.get(guest); int index = -1; index = ( (contentList.lastIndexOf(guest) == -1 ) ? contentList.size() : index+1); contentList.add(index, new ContentMap(owner, localPath, contentSpace)); } 232 The method checks to see whether the guest already has a content list defined. This method would be invoked to grant file access to another user as specified by the guest parameter. Again the ContentMap contains the mapping between the user, the local directory, and a user-defined space. However, this content map is associated with a guest as opposed to the owner. When you determine access to content you concatenate the user and group content lists. public ContentMap[] getContentSpace(String user) throws RemoteException, IOException { List userSpaceList = (List)userMap.get(user); if(userSpaceList == null) return new ContentMap[0]; ArrayList contentList = new ArrayList(userSpaceList); if(groupMap.containsKey(user)) { List groupContent = (List)groupMap.get(user); contentList.addAll(groupContent); } return (ContentMap[])contentList.toArray(new ContentMap[0]); } Messenger Service The messenger service is a modified version of the chat application developed in Chapter 5, "JavaSpaces Service." The MessengerInterface provides a simplified wrapper to the JavaSpace API. The MessengerService implements the MessengerInterface. Listing 10.8 The MessengerInterface package org.jworkplace.chat; import import import import import import java.rmi.RemoteException; java.rmi.MarshalledObject; java.io.IOException; net.jini.core.entry.Entry; net.jini.core.transaction.TransactionException; net.jini.core.entry.UnusableEntryException; public interface MessengerInterface extends java.io.Serializable { public Entry read(Entry template) throws RemoteException, TransactionException, UnusableEntryException, InterruptedException; public Entry readIfExists(Entry template) throws RemoteException, TransactionException, UnusableEntryException, InterruptedException; public void write(Entry entry) throws RemoteException, TransactionException, UnusableEntryException, InterruptedException; public void write(Entry entry, long leaseTime) throws RemoteException, TransactionException, UnusableEntryException, InterruptedException; public Entry take(Entry entry) throws RemoteException, TransactionException, UnusableEntryException, InterruptedException; public Entry takeIfExists(Entry entry) throws RemoteException, TransactionException, UnusableEntryException, InterruptedException; } 233 In the sample JWorkPlace application, channels of communication are associated with active content space names. In other words, when you activate a content space, you activate a message channel using JavaSpaces. You can send messages to all members who share or have access to that space. In addition, there is a default channel (WorkPlaceChannel) that allows you to broadcast messages to all workplace members. Figure 10.5. The messenger uses JavaSpaces as a communication channel. It provides broadcast and shared space messaging capability. This service will continue to evolve in Chapter 11, "Workflow," and Chapter 12, "Agents." Client-Side Service Discovery Now that you have learned about the background on the service side of JWorkPlace, let's move to the client side and introduce some support for common functions required by any client of a service. A common problem that must be addressed on the client side is service discovery. Utilities that ease the burden of developing from scratch with each service you want to access are nice to have available. The net.jini.lookup.ServiceDiscoveryManager provides such support. You can use the ServiceDiscoveryManager for service discovery support in your client-side applications. In JWorkPlace, you define two helper classes, ClientStarter and ClientLookup, that use the ServiceDiscoveryManager (see Figure 10.6). Figure 10.6. The Jini ServiceDiscoveryManager simplifies finding services for clients in a Jini community. 234 The ServiceDiscoveryManager The ServiceDiscoveryManager is used by clients to find specific services in a Jini community. The ServiceDiscoveryManager (SDM) takes a DiscoveryManager and a LeaseRenewalManager as parameters—both of which may be null. The SDM provides a client-side cache that can be used to cache ServiceItems locally and then allow the client to receive notification when new services are found or when existing services change. The ClientLookup class that follows illustrates how the ServiceDiscoveryManager may be used to provide common client-side service discovery capabilities: Listing 10.9 The ClientLookup Class package org.jworkplace.client; import import import import import import import import import import import import net.jini.core.lookup.ServiceItem; net.jini.core.lookup.ServiceTemplate; net.jini.core.lookup.ServiceRegistrar; net.jini.lookup.ServiceDiscoveryManager; net.jini.lookup.LookupCache; net.jini.lookup.ServiceItemFilter; net.jini.lookup.ServiceDiscoveryListener; net.jini.discovery.DiscoveryManagement; net.jini.lease.LeaseRenewalManager; java.io.IOException; java.rmi.RemoteException; java.rmi.RMISecurityManager; public class ClientLookup { // the ServiceDiscoveryManager protected ServiceDiscoveryManager lookupMgr; public ClientLookup() throws IOException { this(null, null); } public ClientLookup(DiscoveryManagement discMgr, LeaseRenewalManager leaseMgr) throws IOException { if ( System.getSecurityManager() == null ) { System.setSecurityManager( new RMISecurityManager() ); } // create the SDM using the passed DiscoveryManager // and LeaseRenewalManager lookupMgr = new ServiceDiscoveryManager( discMgr, leaseMgr ); } 235 // Sub-classes of ClientLookup can override to provide // ServiceDiscoveryListener public ServiceDiscoveryListener getListener() { return null; } // Sub-classes can override to provide ServiceItemFilter public ServiceItemFilter getFilter() { return null; } // Sub-class can override on a lookup failure public boolean requiredFailureAction(ServiceTemplate template) { System.out.println(template); return false; } // helper method to find a LookupService public ServiceRegistrar getLookupService() throws RemoteException { ServiceRegistrar registrar = null; Class[] types = { ServiceRegistrar.class } ; ServiceTemplate tmpl = new ServiceTemplate( null, types, null ); ServiceItem item = getServiceItem(tmpl); if(!(item.service instanceof ServiceRegistrar)) { throw new RemoteException(); } return (ServiceRegistrar)item.service; } // Used to find a service using the tmplate provided public ServiceItem getServiceItem(ServiceTemplate template) { ServiceItem service = null; try { // instruct SDM to wait no more than 10 seconds to discover service service = lookupMgr.lookup( template, getFilter(), 10000 ); } catch ( RemoteException ex ) { ex.printStackTrace(); } catch ( InterruptedException ex ) { } if ( service == null ) { boolean result = requiredFailureAction(template); if(result) System.exit(1); } return service; } // Used to find an array of services using the template provided public ServiceItem[] getServiceItems(ServiceTemplate template) { ServiceItem[] service = new ServiceItem[0]; try { service = lookupMgr.lookup( template, 1, Integer.MAX_VALUE, 236 } } getFilter(), 10000 ); catch ( RemoteException ex ) { ex.printStackTrace(); catch ( InterruptedException ex ) { } if ( service == null ) { boolean result = requiredFailureAction(template); if(result) System.exit(1); } return service; } // Used to construct a client side cache using the filter if overridden public LookupCache createCache(ServiceTemplate template) throws RemoteException { return lookupMgr.createLookupCache(template, getFilter(), getListener()); } // terminates the discovery process public void terminate() { lookupMgr.terminate(); } } Remember that the ServiceDiscoveryManager exports a remote ServiceDiscoveryListener object. The client must have an HTTP server running with a supplied codebase for the lookup service to access the necessary classes. The lack of a clientside HTTP server has caused some confusion on the deployment of the ServiceDiscoveryManager because the behavior, if not properly deployed, is to simply find no services. The four files necessary to be included in the client-side jar are: net.jini.core.event.RemoteEvent.class net.jini.core.event.RemoteEventListener.class net.jini.core.event.UnknownEventException.class net.jini.lookup.ServiceDiscoveryManager$LookupCacheImpl$ LookupListener_Stub.class Knowing this can save you hours of debugging code. Believe me, I know this firsthand! Listing 10.10 The StartWorkPlace Class package org.jworkplace.workplace; import import import import import import import import import import import import java.io.IOException; java.rmi.Remote; net.jini.core.lookup.ServiceTemplate; net.jini.core.lookup.ServiceItem; net.jini.core.entry.Entry; net.jini.lookup.entry.Name; net.jini.lookup.entry.ServiceInfo; org.jworkplace.chat.MessengerInterface; org.jworkplace.start.ClientStarter; org.jworkplace.content.IndexManager; org.jworkplace.account.AccountManager; org.jworkplace.client.WorkPlaceClient; 237 public class StartWorkPlace { private static String starterClass = StartWorkPlace.class.getName(); // List of services required private static String contentMgrClass = IndexManager.class.getName(); private static String accountMgrClass = AccountManager.class.getName(); private static String spaceMgrClass = MessengerInterface.class.getName(); private static final int SERVICE_COUNT = 3; // name of resource file if supplied private static String resourcename = "service"; // Atrributes of services private static Entry[] entries = { new Name("ContentManager"), new ServiceInfo("JWorkPlace", "Robert Flenner", "Sams", "1.0","", "") } ; // Similar to the server side start-up public static void main(String[] args) { // pass the command line arguments // the name of this class // the service bootstrap class // and a resource file name ClientStarter.create(args, starterClass, contentMgrClass, resourcename); } // this method is invoked by reflection from ClientStarter public static ServiceTemplate[] createTemplates() throws ClassNotFoundException { try { { { { Class[] content = new Class[] Class.forName(contentMgrClass) } ; Class[] account = new Class[] Class.forName(accountMgrClass) } ; Class[] space = new Class[] Class.forName(spaceMgrClass) } ; // create the necessary templates to find the services ServiceTemplate[] templates = new ServiceTemplate[SERVICE_COUNT]; templates[0] = new ServiceTemplate(null, content, entries); templates[1] = new ServiceTemplate(null, account, null); templates[2] = new ServiceTemplate(null, space, null); return templates; } catch (Exception e) { e.printStackTrace(System.out); 238 throw (ClassNotFoundException)e; } } // this method is called once the services have been discovered public static void doStart(ServiceItem[] item) { if(item!= null) { store(item); // start the workplace client with the ServiceItems new WorkPlaceClient(item); } } // override public static void store(ServiceItem[] item) { } } The StartWorkPlace class provides client support for the JWorkPlace application. Most of the code is boilerplate code that can be declared. It provides an easy way to request multiple interfaces and then make a determination as to the functionality that the resulting environment can support. Overriding the store method provides the capability to save information locally, such as service IDs, service names, and so on. The ClientStarter class uses the ClientLookup utility to find the necessary workplace services. The Jini services supplied by Sun Microsystems demonstrate startup techniques for the Jini services, such as reggie, mahalo, norm, and so on. The ClientLookup class is modeled after that approach to provide for a more sophisticated client-side startup. Listing 10.11 The ClientLookup Class package org.jworkplace.start; import java.lang.reflect.Method; import net.jini.core.lookup.ServiceTemplate; import net.jini.core.lookup.ServiceItem; import org.jworkplace.client.ClientLookup; public class ClientStarter { // ServiceDiscoveryManager helper protected static ClientLookup clientLookup; // will return the ServiceItem(s) from the templates created public static void create(String[] args, String starterClassName, String implClassName, String resourcename) { Method starterMethod = null; ServiceTemplate[] template = null; try { // create the ServiceDiscoveryManager clientLookup = new ClientLookup(); 239 Class starterClass = Class.forName(starterClassName); // get the templates required from the client starterMethod = starterClass.getMethod("createTemplates",null); template = (ServiceTemplate[])starterMethod.invoke(null,null); // do lookup on each required service interface Class[] argTypes = { ServiceItem[].class } ; starterMethod = starterClass.getMethod("doStart", argTypes); Object[] methodTypes = { doLookup(template) } ; // invoke the doStart method of the client with the results starterMethod.invoke(null,methodTypes); } catch (Exception e) { e.printStackTrace(System.out); throw (RuntimeException)e; } } private static ServiceItem[] doLookup(ServiceTemplate template[]) { ServiceItem[] item = new ServiceItem[template.length]; for(int i=0; i<template.length; i++) { item[i] = clientLookup.getServiceItem(template[i]); } return item; } private static ServiceItem doLookup(ServiceTemplate template) { return clientLookup.getServiceItem(template); } } Introducing the ServiceUI Project The ServiceUI project is a Jini community project that is standardizing the user interfaces to Jini services. This is a broad topic because services have such a varied definition. A service can be implemented on a small device, a PC, or a mainframe computer. However, the community believes that it is important to have a standard definition to be able to "view" and therefore interface with Jini services. Bill Venners, of www.artima.com is leading the ServiceUI project and worked with the community to release a beta version of the ServiceUI API. The ServiceUI Basically, the ServiceUI defines three items used to associate a user interface with a Jini service: The UI must supply • the user interface itself, 240 • • a user interface factory that produces the UI, and a user interface descriptor that describes the UI. A UI factory is an object that has one or more factory methods that produce and return a UI object. A UI descriptor, an instance of net.jini.lookup.entry.UIDescriptor (UIDescriptor), serves as a container for the UI factory and other objects that describe the UI produced by the factory. Because UIDescriptor implements net.jini.core.entry.Entry (Entry), a UIDescriptor can be included in the attribute sets array of a Jini service item, thereby associating the UI with the service. A UI descriptor contains four public fields: • • • • Role—A String that gives the fully qualified name of a Java interface type that represents the role played by the UI Toolkit—A String that gives the name of the main package of the UI toolkit required by the UI Attributes—A java.util.Set of attribute objects that describe the UI produced by the factory Factory—A reference to a java.rmi.MarshalledObject that contains the UI factory object in marshalled form This code fragment creates a UIDescriptor entry that can be used to associate a user interface with a Jini service. The example identifies a Swing JFrame factory and supplies the object responsible for implementing the factory method: WorkPlaceFactory. The WorkPlaceFactory is passed as a MarshalledObject. // describe the UI in a Set Set typeNames = new HashSet(); // signifies this factory produces a JFrame typeNames.add(JFrameFactory.TYPE_NAME); Set attribs = new HashSet(); attribs.add(new UIFactoryTypes(typeNames)); // create the factory object MarshalledObject factory = null; try { // WorkPlaceFactory implements the factory method factory = new MarshalledObject( new WorkPlaceFactory()); } catch (Exception e) { e.printStackTrace(); System.exit(1); } // Create the UIDescriptor entry Entry[] entries = new UIDescriptor(MainUI.ROLE,// Role string WorkPlaceFactory.TOOLKIT,// Toolkit string attribs,// attribute objects factory)// UI factory Eight factory interfaces are defined in the current version of the Jini service UI API. • DialogFactory— Returns an instance of class java.awt.Dialog, or one of its subclasses, that depends on the AWT libraries but not the Swing libraries 241 • • • • • • • FrameFactory— Returns an instance of class java.awt.Frame, or one of its subclasses, that depends on the AWT libraries but not the Swing libraries JComponentFactory— Returns an instance of class javax.swing.JComponent, or one of its subclasses, that depends on both the AWT and Swing libraries JDialogFactory— Returns an instance of class javax.swing.JDialog, or one of its subclasses, that depends on both the AWT and Swing libraries JFrameFactory— Returns an instance of class javax.swing.JFrame, or one of its subclasses, that depends on both the AWT and Swing libraries JWindowFactory— Returns an instance of class javax.swing.JWindow, or one of its subclasses, that depends on both the AWT and Swing libraries PanelFactory— Returns an instance of class java.awt.Panel, or one of its subclasses, that depends on the AWT libraries but not the Swing libraries WindowFactory— Returns an instance of class java.awt.Window, or one of its subclasses other than Frame or Dialog, that depends on the AWT libraries but not the Swing libraries From the Jini specification: "It is strongly encouraged that service and UI providers decouple the class files required by a particular service item from the class files Note required by any UIs that may be associated with that service via UI descriptors in the service item. If the class files for the service item and those of all the UIs associated with the service via UI descriptors are placed into the same jar file, then a client will have to download all the class files for all the UIs just to download the service item, even if the client never uses a single UI." Basically, develop your deployment jar files to optimize expected service usage. If a service rarely needs a graphical user interface, then bundle the UI class files in a separate jar file to conserve resources. Example ServiceUI Factory The WorkPlaceFactory implements the JFrameFactory interface defined in the net.jini.lookup.ui package. This is a new package and needs to be downloaded separately from the Jini distribution . The ServiceUI download is freely available from the www.jini.org site. The JFrameFactory interface defines the single getJFrameMethod, which is passed the ServiceItem associated with the user interface factory when you registered the service with your attribute definition. This is a MarshalledObject downloaded when the service is accessed: Listing 10.12 The WorkPlaceFactory Class package org.jworkplace.workplace; import javax.swing.JFrame; import net.jini.core.entry.Entry; import net.jini.core.lookup.ServiceItem; import net.jini.lookup.entry.UIDescriptor; import net.jini.lookup.ui.attribute.UIFactoryTypes; import net.jini.lookup.ui.factory.JFrameFactory; 242 import net.jini.lookup.ui.MainUI; public class WorkPlaceFactory implements JFrameFactory { public JFrame getJFrame(Object obj) { if(!(obj instanceof ServiceItem)) { return null; } ServiceItem item = (ServiceItem)obj; // Get the attribute set Entry[] entries = item.attributeSets; // loop through attributes looking for UIDescriptor for(int i=0; i<entries.length; i++) { if(entries[i] instanceof UIDescriptor) { UIDescriptor descriptor = (UIDescriptor) entries[i]; // Ensure the role of this descriptor is for the MainUI if(descriptor.role.equals(net.jini.lookup.ui.MainUI.ROLE)) { // Now create JFrame for WorkPlace, passing the ServiceItem JFrame frame = new WorkPlaceFrame(item, FRAME_TITLE); return frame; } } } return null; } } Of course, your factory can be as sophisticated as your application requires. The factory here simply creates the WorkPlaceFrame, which is an extension of ServiceFrame that implements the MainUI role (see Figure 10.7). Figure 10.7. The WorkPlaceFactory creates the WorkPlaceFrame, which is the main frame in the JWorkPlace application. Each user-defined service has an associated UI controller that creates and controls interaction with JPanels, JLists, JTables, and so on. 243 WorkPlaceFrame ensures that the ServiceItem implements the IndexManager interface. At that point, you are probably in familiar territory in terms of user interface development. All the common libraries, AWT, Swing, and so on, are available. What remains is designing a user interface that cooperates with your service interface. Some sample code is provided that demonstrates possible partitioning of responsibility across UI components. Listing 10.13 The WorkPlaceFrame Class package org.jworkplace.workplace; import import import import net.jini.core.lookup.ServiceItem; net.jini.lookup.ui.MainUI; org.jworkplace.client.ServiceFrame; org.jworkplace.content.IndexManager; public class WorkPlaceFrame extends ServiceFrame { public WorkPlaceFrame(ServiceItem item, String name) { super(item, name); if(item.service instanceof IndexManager) { WorkPlaceController workPlace = new WorkPlaceController(); workPlace.initialize(item.service); getContentPane().add(workPlace); this.pack(); } } } The ServiceController abstract class defines an initialize method that receives a service object. For example, this is a reference to the proxy object that is returned from the Jini ServiceRegistrar. Listing 10.14 The ServiceController Class import javax.swing.JComponent; public abstract class ServiceController extends JComponent { abstract public void initialize(Object service); } The AccountController is an example of a UI class that is initialized with the AccountManager proxy object. It creates an AccountEditor that interfaces with a UI model (AccountModel) of the account service. Listing 10.15 The AccountController Class import java.awt.BorderLayout; import org.jworkplace.ui.ServiceController; import org.jworkplace.client.WorkPlaceClient; public class AccountController extends ServiceController { private AccountEditor editor; private AccountModel model; private static String accountMgrClass = AccountManager.class.getName(); 244 public AccountController() { super(); } public void initialize(Object object) { AccountManager accountMgr = null; if(object instanceof AccountManager) accountMgr = (AccountManager)object; else accountMgr = getServiceInterface(); model = new AccountModel(accountMgr); editor = new AccountEditor(model); setLayout(new BorderLayout()); add(editor, BorderLayout.CENTER); } // This method resolves the account interface if it was not available // or supplied at initialization public AccountManager getServiceInterface() { AccountManager manager = null; try { manager = (AccountManager)WorkPlaceClient.getServiceInterface (Class.forName(accountMgrClass)); } catch (ClassNotFoundException e) { e.printStackTrace(); } return manager; } } Running JWorkPlace To run JWorkPlace, change to the Chapter 10 example 1 directory. The startShare.cmd script can be found in the services/scripts directory. The script is used to start an HTTP server for sharing your content in JWorkPlace. You must edit the PORT and MYSHARE variables to match your installation. The script by default uses port 8081 and the directory chapter10\example1\MySpace for content. Any content that is capable of being shared would be placed under this directory. Of course, you can edit this to any location on your machine. The script invokes the Jini-supplied HTTP server. If you are using another server, you must edit the command. C:\JiniJavaSpaces\Chapter10\services\scripts>startShare Listing 10.16 The startShare.cmd Script @echo off @call setup.cmd set PORT=8081 set MYSHARE=chapter10\example1\MySpace echo Jini and JavaSpaces Application Development echo ------------------------------------------- 245 echo echo echo echo echo echo Jini install directory %JINI_HOME% Examples install directory %JWORK_HOME% Default share port %PORT% Default share space %JWORK_HOME%\%MYSHARE% ------------------------------------------Starting the Share Server... java -jar %JINI_HOME%\lib\tools.jar -port 8081 -dir %JWORK_HOME%\%MYSHARE% -trees -verbose Now ensure that both the mahalo transaction service and the outrigger JavaSpaces service are running. To run these services, check out the startMahalo.cmd and startOutrigger.cmd scripts under the services/scripts directory. Under the services/scripts directory is the startWorkPlace1.cmd script. Invoke the command. C:\JiniJavaSpaces\chapter10\services\scripts\startWorkPlace1.cmd Under the chapter10/example1 directory is the startClient.cmd script. There are two properties that must be set in the startClient.cmd script: -Dorg.jworkplace.workport=%PORT% -Dorg.jworkplace.workspace=%MYSHARE% These properties need to match the PORT and MYSHARE variables set in the startShare.cmd script. Invoke the command. C:\JiniJavaSpaces\chapter10\example1\startClient.cmd JWorkPlace is integrated into the Web. It allows you to communicate over a number of channels. Figure 10.8 shows the JWorkPlace startup page. Figure 10.8. The JWorkPlace sample application startup screen. 246 Note Additional information on the code, scripts, and setup of JWorkPlace can be found at www.jworkplace.org. You must create an account using the AccountService (see Figure 10.9). Figure 10.9. The JWorkPlace UI to the AccountService. After creating an account, next you create a shared space. Map a local directory to a content space name. The Add button allows you to add files to the Spaces area (see Figure 10.10). 247 Figure 10.10. You create a shared space from the CreateSpace panel. To add a file to the MySpace area, simply select Add with the file selected (see Figure 10.11). Figure 10.11. You add files to share from the Documents panel. The left-hand panel displays the spaces that you have defined, and spaces that you have been given access to. The right-hand panel displays all the users of JWorkPlace. You can map a space to a user by selecting each entry in the table and pressing the share button (see Figure 10.12). Figure 10.12. Sharing space in JWorkPlace from the Groups panel. 248 Summary The peer-to-peer (P2P) revolution is affecting the development of applications. Decentralization and dynamic community formation are powerful concepts being promoted. Jini and JavaSpaces provide an excellent technology choice to meet the demand for decentralized and ad hoc group formation. Jini offers an inherent capability to discover services and devices on a network, and JavaSpaces easily supports network rendezvous points. A rendezvous point provides a meeting place on the network. There you can provide messaging capabilities, event notifications, and content space definitions you want to share with other group members. The ServiceUI project is a Jini community project that is standardizing the user interfaces to Jini services. This is a broad topic, because services have such a varied definition. However, the community believes that it is important to have a standard definition to be able to "view," and therefore interface with Jini services. JWorkPlace provides a sample GroupWare application using Jini and JavaSpaces. Jini and JavaSpaces provide the necessary infrastructure to build the next generation GroupWare applications. 249 Chapter 11. Workflow IN THIS CHAPTER • • • • • • • • What Is Workflow? A Collaborative Workflow Framework Example The Supporting Services Change in State as a Flow-of-Objects The Event Mailbox Service as a Remote Event Notification Platform Integrating Workers with JavaSpaces and the Event Mailbox Service Content Management Workflow Summary This chapter demonstrates the power of Jini and JavaSpaces in controlling distributed workflow processes. It demonstrates the ease with which JavaSpaces can model processes as a flow-ofobjects. The chapter also presents my concept of STAR: (S)pace, (T)ime, (A)ctivity, and (R)esource entries, which define the core model in an example collaborative workflow framework. What Is Workflow? In 1996, the Workflow Management Coalition published a glossary of terms related to workflow. It defines workflow as "The automation of a business process, in whole or part, during which documents, information or tasks are passed from one participant to another for action, according to a set of procedural rules." Workflow management is the automated coordination and control of work processes. A work process is a collection of activities, typically stretching across and beyond organizational boundaries. An activity can involve manual interaction with a user or workflow participant, or the activity might be executed using computing resources. Workflow plays a more strategic role in e-commerce because to be successful you must be able to integrate business transactions into your workflow processes and back-end systems. Business-toconsumer (B2C) offerings have demonstrated the need for end-to-end service and fulfillment. Failure to support products and processes across distribution channels diminishes your chances for success. However, integrating workflow and legacy systems has always been complex and costly. JavaSpaces provides a simplified approach to dynamic communication, coordination, and sharing of objects between network resources. JavaSpaces acts as a virtual space between providers and requesters of network services. This allows participants in a distributed solution to exchange tasks, requests, and information in the form of Java objects. Jini, in concert with JavaSpaces, provides a mechanism to loosely coupled systems through remote events and a persistent shared memory model. Processes can exchange information through a common space and be notified of changes through asynchronous events. The simplicity of the JavaSpaces API provides an easy-to-learn interface, yet it still provides a powerful programming model. As we move to component and service-based architectures, coordination and automation of processes both internal and external to an organization will take on more importance. Efficiencies 250 are gained by integrating internal systems with internal users, partner systems, and external customers. Supply chains will become increasingly dependent on technology. Coordination of activities and dissemination of information across organizational boundaries are key factors to improving chain effectiveness. This is the first step in creating the virtual organization, where partner systems, employees, and customers collaborate to produce and provide goods and services (see Figure 11.1). Figure 11.1. There are three levels of collaborative development required in the virtual organization. Generic workflow engines supporting standard APIs will help integrate both internal and external processes. Jini and JavaSpaces should also be considered as a network-centric middleware solution for enabling next generation communication and collaboration. This chapter demonstrates how JavaSpaces can be used to control workflow among collaborating participants. What Is a Workflow Management System? The Workflow Management Coalition (http://www.wfmc.org/standards/docs/TC1011_term_glossary_v3.pdf) defines a workflow management system as "A system that defines, creates and manages the execution of workflows through the use of software, running on one or more workflow engines, which is able to interpret the process definition, interact with workflow participants and, where required, invoke the use of IT tools and applications." A Workflow Management System can interpret a workflow definition, manage triggers and alarms, and interoperate with external systems. Workflow management benefits include reduced operating costs, improved productivity, faster processing times, and improved communication. 251 Workflow System Frameworks Workflow management systems support different models of execution for different types of control and automation. You can classify workflow frameworks according to their deployment characteristics and execution goals. Production Workflow The key goal of production workflow is to manage many similar tasks and to optimize productivity. This is achieved by automating as many activities as is practical. Human input is required only to manage exceptions. Production workflow can be tightly integrated with existing (legacy) systems. Performance is usually of paramount importance. Autonomous Workflow Engines An autonomous workflow management system is functional without any additional application software. Autonomous workflow management systems are separate pieces of application software that provide the workflow functionality. They are usually installed to support a variety of different applications. The modeling of autonomous workflow applications requires the specification of interface information for the invoked applications, relevant data structures, and involved participants, and thus can become a complex challenge. Embedded Workflow An embedded workflow management system is only functional if it is employed with the surrounding (embedding) system—for example, an Enterprise Resource Planning (ERP) system. The workflow components are used to control the sequence of the application's functions, to manage queues, and to assist with exception processing. Collaborative Workflow Collaborative workflow focuses on teams working together toward common goals. Groups can vary from small, project-oriented teams, to widely dispersed people with interests in common. Ad Hoc Workflow Ad hoc workflow systems allow users to create and amend Process Definitions quickly and easily to meet circumstances as they arise. It is possible to have almost as many Process Definitions as there are instances of the definitions. Ad hoc workflow maximizes flexibility in areas where throughput and security are not major concerns. Whereas in production workflow the organization owns the process, ad hoc workflow users own their own processes. Workflow Reference Model The Workflow Reference Model defined by the Workflow Management Coalition defines five interoperable interfaces (see Figure 11.2): • • Process Definition—The computerized representation of a process that includes the manual definition and workflow definition. This interface deals with passing Process Definitions from external tools to the workflow engine where they are enacted. Workflow Client and Invoked Application—These interfaces have been combined and cover the WAPIs (Workflow APIs). The support of these interfaces in workflow management products allows the implementation of front-end applications that need to access workflow management engine functions (workflow services). Such implementations might be written by workflow management exploiters or workflow 252 • • systems integrators. Integration between workflow and other desktop tasks (calendar, mail, reminders, etc.) is often a common target, and the workflow APIs allow workflow task integration into a common desktop. Workflow Enactment Service—A software service that may consist of one or more workflow engines in order to create, manage, and execute workflow instances. Applications may interface to this service via the workflow application programming interface (WAPI). This interface defines the mechanisms that workflow product vendors are required to implement in order that one workflow engine may make requests of another workflow engine to effect the selection, instantiation, and enactment of known process definitions by that other engine. The requesting workflow engine is also able to pass context data (workflow-relevant or application data) and receive back status information and the results of the enactment of the process definition. Audit and Monitoring—The support of this specification in workflow products allows analysis of consistent audit data across heterogeneous workflow products. During the initialization and execution of a process instance, multiple events occur that are of interest to a business, including WAPI events, internal workflow management engine operations and other system and application functions. With this information, a business can determine what has occurred in the business operations managed by workflow and take the appropriate actions. Figure 11.2. The Workflow Reference Model. A Collaborative Workflow Framework Example Now that the importance of workflow and the basic components and types of workflow frameworks have been highlighted, you can put this knowledge to use by building a workflow service using Jini and JavaSpaces. Collaborative workflow focuses on teams working together toward common goals. Groups can vary from small, project-oriented teams, to widely dispersed people with common interests. Effective use of collaborative workflow is now considered a vital element in the success of 253 enterprises of all kinds. Throughput is not as important as in production workflow systems, and process definitions are not rigid and can be amended frequently. This example will dovetail off the collaborative capabilities introduced in the last chapter. You will build a system capable of the following: • • • Defining process definitions and work items Scheduling tasks and assigning resources Notifying participants when dependent tasks have completed and new tasks can start As you should recall from Chapter 5, "JavaSpaces Service," JavaSpaces is well suited for systems that can be modeled as a flow-of-objects, which is typical of workflow systems. This example focuses on three aspects of the workflow application: • • • Modeling change in activity state as a flow-of-objects Using the Event Mailbox Service as a remote event notification platform Integrating process workers with JavaSpaces and the Event Mailbox Service Figure 11.3 depicts the flow of tasks through the workflow system. Figure 11.3. Collaborative Workflow Framework using Jini, JavaSpaces, and the Event Mailbox Service. An administrator or process owner is responsible for defining workflow templates. These templates define work items that are assigned to members of the group. Descriptions of the task and start and stop time parameters are included. After the process has been modeled, the definition is exported to the remote Workflow Service. 254 The Workflow Service provides a process definition interface and a work process interface that represent the Process Definition and the Workflow Client Application interfaces in the Workflow Reference Model. The Workflow Service engine is responsible for activating the workflow process and updating the activities as workers update associated tasks. Task assignments and task status are received by workers asynchronously, and the overall progress of the process can be monitored. The Supporting Services A number of Jini-specific services are required in a collaborative workflow. Reggie, Outrigger, Mahalo, Mercury In addition to the lookup service, your workflow application will require JavaSpaces and the transaction service (see Figure 11.4). JavaSpaces provides a network-sharable space for storing activity definitions and state change notifications. Again, the transaction service is required by JavaSpaces. In addition, you will use the Event Mailbox Service (mercury), to store event notifications for workers (resources) defined in your workflow application. Figure 11.4. Services comprising your workflow application. Three new services are also defined: • • • The WorkflowService implements the workflow enactment and workflow engine process. The StarService provides a proxy to the JavaSpace API with additional workflow semantics. The MailboxManager provides a user mapping manager to the EventMailbox service. Now let's look at the entries and definitions specific to your workflow service. Change in State as a Flow-of-Objects In this section, the entries comprising your collaborative workflow are defined and described. The Tuples Defining Workflow Space 255 The StarEntry is the basic tuple defined in JavaSpaces that encapsulates your definition of a process activity and its supporting environment: • • • • Space refers to a location-dependent place that the activity requires knowledge of. In the example Workflow Service, the process ID is used as a unique reference to a location in JavaSpaces. Time is used to schedule the start and completion time of an activity. It can be extended to include other time-based measurements to track performance, determine lease durations, and schedule alert notifications. Activity is the basic definition of a step in the process and encapsulates the activity description and expected output or result of the process step. Resource abstracts the assignment of an individual, a group, or another system to the activity. Figure 11.5. A STAR is used to encapsulate the basic elements of workflow. StarEntries are written to JavaSpaces using the StarInterface (see Listing 11.3). StarEntries can be linked to form a constellation that in effect provides a graphical view of the relationship and dependencies between tasks. This is akin to the Gant chart often used in project management tools. Modeling State as Objects The ActivityState interface defines the interface all state objects implement in your workflow system. State objects implement a common interface that changes behavior dependent on the current status of the activity. For instance, the start method invoked on a "ready" task is well defined; however, the start method on a "complete" task throws an invalid state change exception: Listing 11.1 The ActivityState Interface package org.jworkplace.workflow; import java.rmi.RemoteException; import java.io.Serializable; public interface ActivityState { public static final String public static final String public static final String public static final String public static final String extends Serializable TASK_INIT = "inactive"; TASK_READY = "ready"; TASK_START = "start"; TASK_COMPLETE = "complete"; TASK_SUSPEND = "suspend"; public void start() throws RemoteException; 256 public void terminate() throws RemoteException; public void suspend() throws RemoteException; public void resume() throws RemoteException; } Controlling and Managing Process Flow The basic process control states for activities are as follows: • • • • • Inactive is the initial state of an activity. The activity has been defined but has not been started. Start/Running is the state of an activity once acknowledged by a worker. This indicates that the underlying activity is in progress and tracking against the current schedule parameters. Complete means the activity has fulfilled the conditions for completion, and any internal operations such as logging audit data or statistics will be performed. Complete signifies that dependent successor tasks can be moved to a "ready" state. Suspend is the state of an activity that has been placed on hold, and no activities are started until the process has returned to the running state (via a resume command). Ready is the state of an activity that has all dependent tasks complete but has not been acknowledged or started by a user or system (via a start command). The system defines an associated state object entry that extends TaskEntry for each state defined: Listing 11.2 The TaskEntry Class package org.jworkplace.workflow; import java.rmi.RemoteException; import net.jini.entry.AbstractEntry; import com.sun.jini.proxy.UUID; public class TaskEntry extends AbstractEntry implements ActivityState { public UUID processId; public String taskId; public TaskEntry() { this(null); } public TaskEntry(UUID processId) { this(null, null); } public TaskEntry(UUID processId, String taskId) { this.processId = processId; this.taskId = taskId; } // Sub-classes implement the desired behavior // for each command dependent on current state public void start() throws RemoteException { } public void terminate() throws RemoteException { } public void suspend() throws RemoteException { } public void resume() throws RemoteException { } } The state classes that extend TaskEntry are InactiveTask, ActiveTask, ReadyTask, CompleteTask, and SuspendTask. 257 Figure 11.6. The TaskEntry abstract class provides the base class for state objects in your Workflow Service. As workers acknowledge, accept, and complete activities, corresponding task objects are written to JavaSpaces. The StarService The StarInterface provides methods that map to the JavaSpace API. The StarInterface delegates requests to the JavaSpace proxy and provides some additional implied semantics for workflow processing. The StarService implements the StarInterface and is responsible for resolving the reference to the JavaSpaces service. Listing 11.3 The StarInterface Interface package org.jworkplace.workflow; import import import import import import import public java.rmi.RemoteException; java.rmi.MarshalledObject; java.io.IOException; net.jini.core.entry.Entry; net.jini.core.transaction.TransactionException; net.jini.core.entry.UnusableEntryException; net.jini.core.event.RemoteEventListener; interface StarInterface extends java.io.Serializable { public void setRemoteEventListener(Entry template, MarshalledObject handback, RemoteEventListener listener); public Entry read(Entry template) public Entry readIfExists(Entry template) public void write(Entry entry) public void write(Entry entry, long leaseTime) public Entry take(Entry entry) public Entry takeIfExists(Entry entry) } The setRemoteEventListener method takes an Entry template of a MarshalledObject and a RemoteEventListener object as parameters and invokes the JavaSpace notify method to register for remote event notification. This provides a simple mechanism to track the writing of state objects to space. public synchronized void setRemoteEventListener(Entry entry, MarshalledObject handback, RemoteEventListener listener) { space.notify(entry, null, listener, Lease.FOREVER, handback); 258 } The MarshalledObject contains a reference to a process definition. The listener contains a reference to the EventMailbox listener (see Listing 11.5). The Entry template is the CompleteTask object initialized with a specific instance of a process to monitor. A StarEntry is written for each activity defined in a workflow process by calling the write method of the StarInterface. The StarService implementation writes the StarEntry and an associated TaskEntry that parallels the current state of the activity: public void write(Entry entry) throws RemoteException, TransactionException, UnusableEntryException, InterruptedException { if(entry instanceof StarEntry) { StarEntry template = (StarEntry)entry; // get the location, taskId, and status from the StarEntry UUID location = template.location; String taskId = template.taskId; String command = template.status; // using the StarEntry processID and taskID // remove the current TaskEntry (state object) for // work process activity TaskEntry taskTemplate = new TaskEntry(location, taskId); takeIfExists(taskTemplate); // now create and update the current TaskEntry to reflect // the StarEntry status // initializing - create an inactive task if(command.equals(ActivityState.TASK_INIT)) { InactiveTask task = new InactiveTask(location, taskId); space.write(task, null, Long.MAX_VALUE); // starting - move task to the start state } else if (command.equals(ActivityState.TASK_START)) { ActiveTask task = new ActiveTask(location, taskId); task.start(); space.write(task, null, Long.MAX_VALUE); // ready for activation - move task to the ready state } else if (command.equals(ActivityState.TASK_READY)) { ReadyTask task = new ReadyTask(location, taskId); space.write(task, null, Long.MAX_VALUE); // completing - move task to the complete state } else if (command.equals(ActivityState.TASK_COMPLETE)) { CompleteTask task = new CompleteTask(location, taskId); task.terminate(); space.write(task, null, Long.MAX_VALUE); // suspending - move task to the suspended state } else if (command.equals(ActivityState.TASK_SUSPEND)) { SuspendTask task = new SuspendTask(location, taskId); task.suspend(); space.write(task, null, Long.MAX_VALUE); 259 } // remove the current StarEntry StarEntry removalTemplate = new StarEntry(); removalTemplate.location = location; removalTemplate.taskId = taskId; takeIfExists(removalTemplate); // write the new StarEntry Lease lease = space.write(template, null, Lease.FOREVER); } } The StarEntry reflects the current status of a task in a work process and contains the STAR information. The TaskEntry models the state changes that occur on a task. The write method removes the current TaskEntry if it exists and creates a new entry based on the StarEntry status. The status of the StarEntry in effect is the state change command to the created TaskEntry. Illegal state changes are thrown in the TaskEntry subclasses. The current StarEntry is removed and a new StarEntry is written to reflect the updated task status. These state objects trigger event notifications to listeners that have registered for state change events. You use this mechanism to notify the EventMailbox listeners of a change in activity state, which is explained in the next section. The Event Mailbox Service as a Remote Event Notification Platform This section describes the Event Mailbox service and defines a MailboxManager for your collaborative workflow application. The Event Mailbox Service (Mercury) Chapter 7, "The Helper Services," explained how the Event Mailbox service can be used by a client to store event notifications on its behalf. When an entity registers with the Event Mailbox Service, that service will collect events intended for the registered entity until the entity initiates delivery of the events. The Event Mailbox service is defined by the EventMailbox interface: Listing 11.4 The EventMailbox Interface public interface EventMailbox { MailboxRegistration register (long leaseDuration) throws RemoteException; } Clients invoke the register method on the EventMailbox and receive a MailboxRegistration as a result: 260 Listing 11.5 The MailboxRegistration Interface package net.jini.event; public interface MailboxRegistration { Lease getLease(); RemoteEventListener getListener(); void enableDelivery(RemoteEventListener target) throws RemoteException; void disableDelivery() throws RemoteException; } With the MailboxRegistration interface, an entity can manage the time and location of remote event delivery. A using entity retrieves a RemoteEventListener from the registration object and uses this listener in any method that requires a RemoteEventListener. The using entity can then disconnect from the network and not worry about losing events. The events will be directed and stored by the mailbox listener. Later, the using entity can activate and invoke the enableDelivery method specifying a RemoteEventListener that becomes the target for the saved events. The using entity can toggle between enabling and disabling delivery until the entity terminates or allows its lease to expire with the mailbox service. The JavaSpace interface method of interest is the notify method. It takes a RemoteEventListener as a parameter: EventRegistration notify(Entry tmpl, Transaction txn, RemoteEventListener listener, long lease, MarshalledObject handback) throws RemoteException, TransactionException; A notify request registers interest in future incoming entries to the JavaSpaces service that matches the specified template. When matching entries are written, the specified RemoteEventListener will be notified. When you invoke notify, you provide an upper boundary on the lease time, which is how long you want the registration to be remembered by the JavaSpaces service. Each notify returns a net.jini.core.event.EventRegistration object. When an object is written that matches the template supplied in the notify invocation, the listener's notify method is invoked with a RemoteEvent object. Linking the Event Mailbox service with JavaSpaces is as easy as the following code fragment: EventMailbox mailbox; JavaSpace space; // register with the EventMailbox Service MailboxRegistration reg = mailbox.register (Lease.FOREVER); // get the remote event listener for the registration RemoteEventListener listener = reg.getListener(); // create a template to match on SomeEntry template = new SomeEntry(); // create a MarshalledObject to handback 261 MarshalledObject handback = new MarshalledObject(myData); // supply the template, listener, and handback to the JavaSpace notify method EventRegistration eventReg = space.notify(template, null, listener, Lease.FOREVER, handback) When an entry matching the template is written to JavaSpace, the listener (mailbox) is notified with a RemoteEvent object. The delivery of the notifications is now under your control through the enableDelivery method of the MailboxRegistration: // now enable delivery to the supplied listener reg.enableDelivery(myRemoteListener); This method allows you to deactivate (log out) from the workflow system and not miss events that might occur during your absence. When you log back in, you can enable delivery and get all the events targeted for your mailbox registration (see Figure 11.7). Figure 11.7. Using the Event Mailbox service to queue JavaSpace notifications. The MailboxService The MailboxService provides the management of the Event Mailbox registrations and event delivery to the users of the workflow system. The MailboxManager Interface 262 The MailboxManager interface is a wrapper to the EventMailbox and MailboxRegistration interfaces. It allows you to map users to registrations as seen in Listing 11.6. Listing 11.6 The MailboxManager Class package org.jworkplace.mailbox; import import import import import java.rmi.RemoteException; net.jini.core.lease.LeaseDeniedException; net.jini.core.lease.UnknownLeaseException; net.jini.core.event.RemoteEventListener; net.jini.admin.Administrable; public interface MailboxManager extends Administrable { public void register(String user, long duration) throws RemoteException, LeaseDeniedException; public RemoteEventListener getListener(String user) throws RemoteException; public void start(String user, RemoteEventListener listener) throws RemoteException; public void pause(String user) throws RemoteException; public void stop(String user) throws RemoteException, UnknownLeaseException; } The MailboxService implements the RemoteMailboxManager interface. Again you will extend your ServiceImpl class, which should be familiar to you by now: Listing 11.7 The MailboxService Class package org.jworkplace.mailbox; import import import import import import java.rmi.Remote; java.rmi.RemoteException; java.rmi.RMISecurityManager; java.rmi.server.RemoteObject; java.io.File; java.io.IOException; import import import import import import java.util.Set; java.util.Map; java.util.HashSet; java.util.HashMap; java.util.Collection; java.util.Collections; import import import import import import import net.jini.discovery.LookupDiscovery; net.jini.discovery.DiscoveryListener; net.jini.discovery.DiscoveryEvent; net.jini.core.lookup.ServiceRegistrar; net.jini.core.entry.Entry; net.jini.lookup.entry.Name; net.jini.lookup.entry.ServiceInfo; import net.jini.event.EventMailbox; 263 import import import import import import import net.jini.event.MailboxRegistration; net.jini.lease.LeaseRenewalService; net.jini.lease.LeaseRenewalManager; net.jini.core.lease.Lease; net.jini.core.lease.LeaseDeniedException; net.jini.core.lease.UnknownLeaseException; net.jini.core.event.RemoteEventListener; import org.jworkplace.util.ServiceFinder; import org.jworkplace.service.*; public class MailboxService extends ServiceImpl implements ServiceAdmin, RemoteMailboxManager, DiscoveryListener { // The interface to the EventMailbox private EventMailbox mailbox; // The lease renewal manager to renew the registration leases // this could also use the LeaseRenewalService private LeaseRenewalManager leaseRenewalMgr; // performs Lookup discovery of the EventMailbox service private LookupDiscovery mgt; // A mapping of user to registration private Map userMap; public static void main(String args[]) throws Exception { if (System.getSecurityManager() == null) { System.setSecurityManager(new RMISecurityManager()); } try { new MailboxService(args); } catch(IOException e) { e.printStackTrace(); System.exit(1); } try { Thread.sleep(Long.MAX_VALUE); } catch(InterruptedException e) { } System.exit(0); } // public MailboxService(String[] args) throws IOException { super(args); // Enable a thread safe collection userMap = Collections.synchronizedMap(new HashMap()); // Instantiate the lease renewal manager leaseRenewalMgr = new LeaseRenewalManager(); // now start finding registrars (lookup services) mgt = new LookupDiscovery(LookupDiscovery.NO_GROUPS); mgt.addDiscoveryListener(this); ((LookupDiscovery)mgt).setGroups(LookupDiscovery.ALL_GROUPS); 264 } // register the supplied user with the EventMailbox service // and store the mapping public void register(String user, long duration) throws RemoteException, LeaseDeniedException { MailboxRegistration reg = mailbox.register(duration); leaseRenewalMgr.renewFor(reg.getLease(), duration, null); userMap.put(user, reg); } // get the listener for this user public RemoteEventListener getListener(String user) throws RemoteException { MailboxRegistration reg = (MailboxRegistration)userMap.get(user); if(reg != null) { return reg.getListener(); } return null; } // enable delivery to the listener specified public void start(String user, RemoteEventListener listener) throws RemoteException { MailboxRegistration reg = (MailboxRegistration)userMap.get(user); if(reg != null) reg.enableDelivery(listener); } // disable delivery and queue notifications public void pause(String user) throws RemoteException { MailboxRegistration reg = (MailboxRegistration)userMap.get(user); if(reg != null) reg.disableDelivery(); } // disable and stop notification public void stop(String user) throws RemoteException, UnknownLeaseException { // cancel lease MailboxRegistration reg = (MailboxRegistration)userMap.get(user); if(reg != null) { Lease lease = reg.getLease(); lease.cancel(); } } /* ** DiscoveryListener Interface */ public synchronized void discarded(DiscoveryEvent e) { } // Find the EventMailbox Service public synchronized void discovered(DiscoveryEvent de) { // get the array of lookup services discovered ServiceRegistrar[] registrars = de.getRegistrars(); 265 // If we already have found the EventMailbox return if(mailbox == null) { try { // loop through the LUS's to find the EventMailbox for(int i=0; i < registrars.length; i++) { // Call the ServiceFinder utility mailbox = (EventMailbox)ServiceFinder.findEMS(registrars[i]); if(mailbox != null) { // Terminate the discovery threads mgt.terminate(); break; } } } catch (Exception e) { e.printStackTrace(); } } } } The MailboxService provides a user mailbox mapping capability. As users become workers—for example, assigned to a task—the WorkflowService registers the user with the Event Mailbox service. The WorkflowService then creates a StarEntry template using the worker as the Resource and registers for matching entry space notifications. Workers are notified when any StarEntry assigned to them is written to space. Integrating Workers with JavaSpaces and the Event Mailbox Service In this section you integrate the services and users of your collaborative workflow application. The Workflow Service The WorkflowManager interface provides the interface to administering process definitions in the Workflow Service. Note that it extends Administrable, so it defines the getAdmin method: Listing 11.8 The WorkflowManager Interface package org.jworkplace.workflow; import java.rmi.RemoteException; import net.jini.admin.Administrable; import com.sun.jini.proxy.UUID; import org.jworkplace.mailbox.MailboxManager; /* ** This interface defines the methods required to manage ** workflow process definitions */ public interface WorkflowManager extends Administrable { // create a workflow process, the UUID is a universal unique 266 // identifier generated by the workflow system public UUID create(ProcessDef processDef) throws RemoteException; // retrieve a process definition public ProcessDef getProcessDef(String owner, String processName) throws RemoteException; // get all process names public String[] getProcessNames(String owner) throws RemoteException; // update an existing process public void updateProcess(UUID processId, ProcessDef processDef) throws RemoteException; // remove an existing process definition public void removeProcess(UUID processId) throws RemoteException; // get the mailbox manager proxy interface public MailboxManager getMailboxManager() throws RemoteException; // create a process listener for a type of task public void createProcessListener(UUID processId, TaskEntry taskEntry) throws RemoteException; } The methods defined in this interface allow the importing and exporting of process definitions to using entities. There is also a getMailboxManager method that returns a proxy to the MailboxManager associated with the WorkflowService and a createProcessListener that can be used to register interest in a type of event. The workflow engine registers interest in CompleteTask notifications for each process defined. Workflow Process Definition Your workflow process definition is composed of a collection of work items (see Figure 11.8). Work items model specific tasks in the workflow system. Of course, workflow applications can use any number of approaches. For example, you could use XML to define process templates and task execution policy. To keep the example simple, you will create a direct mapping between work item and task definition. Figure 11.8. A ProcessDef object contains a time-sorted list of work items. Process Definition 267 The process definition is contained in a ProcessDef class. A unique ID is associated with every process defined in the workflow system. The unique ID is contained in the class com.sun.jini.proxy.UUID and generated by the factory com.sun.jini.proxy.UUIDFactory: Listing 11.9 The ProcessDef Class public class ProcessDef extends AbstractEntry { // unique id assigned by Workflow service private UUID id; // The human readable name of the process template private String processName; // The owner of the process definition private String processOwner; // A time sorted work activity list private Map workItemList; public ProcessDef() { this(null, null); } public ProcessDef(String processName, String processOwner) { this.processName = processName; this.processOwner = processOwner; workItemList = Collections.synchronizedSortedMap(new TreeMap()); } public public public public public owner; } void setId(UUID id) { this.id = id; } UUID getId() { return id; } String getProcessName() { return processName; } String getProcessOwner() { return processOwner; } void setProcessOwner(String owner) { this.processOwner = public void addWorkItem(WorkItem item) throws RemoteException { workItemList.put(item, item); } public void updateWorkItem(WorkItem item) throws RemoteException { removeWorkItem(item); addWorkItem(item); } public WorkItem getWorkItem(String id) throws RemoteException { System.out.println("getWorkItem: " + id); Iterator itr = (workItemList.values()).iterator(); while(itr.hasNext()) { WorkItem item = (WorkItem)itr.next(); if( item.getId().equals(id)) { return item; } } return null; } 268 public WorkItem getWorkItem(WorkItem template) throws RemoteException { return getWorkItem(template.getId()); } public WorkItem getFirstItem() throws RemoteException { WorkItem[] items = getWorkItems(); return items[0]; } public WorkItem getNextItem(String id) throws RemoteException { WorkItem successor = null; WorkItem item = getWorkItem(id); if(item != null) { SortedMap tailView = ((SortedMap)workItemList).tailMap(item); Object[] array = (tailView.keySet()).toArray(); if(array.length > 1) { successor = (WorkItem)array[1]; } } return successor; } public void removeWorkItem(WorkItem template) throws RemoteException { Iterator itr = (workItemList.values()).iterator(); while(itr.hasNext()) { WorkItem item = (WorkItem)itr.next(); if( item.getId().equals(template.getId())) { workItemList.remove(item); break; } } } public WorkItem[] getWorkItems() throws RemoteException { Collection collection = workItemList.values(); return (WorkItem[])collection.toArray(new WorkItem[0]); } } Work items are defined and assigned to specific workers by the administrator. In a more robust application, assignment may be to a specific role and then to a specific individual. This gives you the capability to separate process definition from process execution. The Workflow Service example defines a Resource class that encapsulates assignments. The Time class contains a scheduled start and stop time for the activity. The WorkItem implements Comparable to maintain a time sequence ordering to the item collection. Listing 11.10 The WorkItem Class package org.jworkplace.workflow; public class WorkItem implements Comparable, java.io.Serializable { // human readable identifier public String itemName; 269 // description of the work item public String description; // Start and stop time public Time time; // worker assigned public Resource assignedTo; public WorkItem() { this(null, null, null, null); } public WorkItem(String itemName, String description, Time time, Resource resource) { this.itemName = itemName; this.description = description; this.time = time; this.assignedTo = resource; } // // code omitted // // Used to sort workitems by start time public int compareTo(Object obj) { WorkItem item = (WorkItem)obj; if (this == item) return 0; long thisStart = this.getStartTime().longValue(); long itemStart = item.getStartTime().longValue(); if ( thisStart < itemStart || thisStart == itemStart ) return -1; return 1; } } The WorkflowService Implementation The WorkflowService provides the primary administration and process activation for your workflow system. It implements the remote version of the WorkflowManager and WorkProcess interfaces defined in the WorkflowBackend. It is also responsible for resolving access to the MailboxManager and the StarService using a DiscoveryListener. There are three primary mappings performed by the WorkflowService: • • • The processMap provides a process ID to process definition mapping. The userMap provides a worker to process mapping. The ownerMap provides a process administrator to process mapping. Listing 11.11 The WorkflowService Class package org.jworkplace.workflow; import org.jworkplace.workplace.WorkPlaceFactory; import org.jworkplace.login.LoginHandler; 270 import import import import org.jworkplace.mailbox.MailboxManager; org.jworkplace.account.Session; org.jworkplace.util.ServiceFinder; org.jworkplace.service.*; public class WorkflowService extends ServiceImpl implements ServiceAdmin, WorkflowBackend, DiscoveryListener { /** UUID generator */ private UUIDFactory uuidFactory = new UUIDFactory(); // processId to processDef private Map processMap; // worker to processId private Map userMap; // owner to list of owned processDefs private Map ownerMap; // thread to invoke process instance private WorkflowEngine engine; // JavaSpace proxy interface private StarInterface space; private static String starInterfaceClass = StarInterface.class.getName(); // Event Mailbox proxy interface private MailboxManager mailbox; private static String mailboxManagerClass = MailboxManager.class.getName(); // process definition queue private UUID processToActivate; private boolean ready = false; // Lookup discovery manager private LookupDiscovery mgt; public static void main(String args[]) throws Exception { if (System.getSecurityManager() == null) { System.setSecurityManager(new RMISecurityManager()); } try { new WorkflowService(args); } catch(IOException e) { e.printStackTrace(); System.exit(1); } try { Thread.sleep(Long.MAX_VALUE); } catch(InterruptedException e) { System.exit(0); } 271 } public WorkflowService(String[] args) throws IOException { super(args); // processId to processDef processMap = Collections.synchronizedMap(new HashMap()); // worker to processId userMap = Collections.synchronizedMap(new HashMap()); // owner to list of owned processDefs ownerMap = Collections.synchronizedMap(new HashMap()); // initialize factory UUID uuid = uuidFactory.newUUID(); // now start finding registrars (lookup services) mgt = new LookupDiscovery(LookupDiscovery.NO_GROUPS); mgt.addDiscoveryListener(this); ((LookupDiscovery)mgt).setGroups(LookupDiscovery.ALL_GROUPS); // start threads engine = new WorkflowEngine(); } // // Workflow Manager Interface // // create the process definition and queue the process for the engine thread public UUID create(ProcessDef processDef)throws RemoteException { // generate a unique id for this process UUID uuid = uuidFactory.newUUID(); processDef.setId(uuid); processMap.put(uuid, processDef); // get the owner and determine if other processes // belonging to this owner are already defined String owner = processDef.getProcessOwner(); if(!ownerMap.containsKey(owner)) { List processList = new ArrayList(); ownerMap.put(owner, processList); } // add this process definition to the owners list List processList = (List)ownerMap.get(owner); int index = -1; index = ( (processList.lastIndexOf(owner) == -1 ) ? processList.size() : index+1); processList.add(index, processDef); // notify the engine a process instance should be created queueProcess(uuid); // return the unique reference return uuid; } // returns a Process Defintion given a owner and process name public ProcessDef getProcessDef(String owner, String processName) 272 throws RemoteException { List processList = (List)ownerMap.get(owner); if(processList == null) throw new RemoteException(); ProcessDef item = null; Iterator itr = processList.iterator(); while(itr.hasNext()) { item = (ProcessDef)itr.next(); if(processName.equals( item.getProcessName())) break; } return item; } // returns the list of processes defined by a given owner public String[] getProcessNames(String owner) throws RemoteException { List processList = (List)ownerMap.get(owner); if(processList == null) throw new RemoteException(); Iterator itr = processList.iterator(); ArrayList nameList = new ArrayList(); while(itr.hasNext()) { ProcessDef item = (ProcessDef)itr.next(); nameList.add(item.getProcessName()); } return (String[])nameList.toArray(new String[0]); } // updates a process defintion public void updateProcess(UUID processId, ProcessDef processDef) throws RemoteException { processMap.put(processId, processDef); } // removes a process definition public void removeProcess(UUID processId) throws RemoteException { processMap.remove(processId); } // return the MailboxManager used by this workflow service public MailboxManager getMailboxManager() throws RemoteException { return mailbox; } // Called by the engine thread with a CompleteTask object // during process instantiation public void createProcessListener(UUID processId, TaskEntry taskEntry) throws RemoteException { // get the process definition associated with this id ProcessDef processDef = (ProcessDef)processMap.get(processId); 273 try { // create a TaskListener TaskListener listener = new TaskListener( (WorkflowBackend)WorkflowService.this, space); // pass the TaskListener the Process Definition MarshalledObject handback = new MarshalledObject(processDef); // register for notification of state objects space.setRemoteEventListener(taskEntry, handback, listener); } } catch(Exception e) { e.printStackTrace(); } // waits for next process definition public synchronized UUID getProcessId() { while (ready == false) { try { wait(); } catch (InterruptedException e) { } } ready = false; notifyAll(); return processToActivate; } // queues a process to activate the process engine public synchronized void queueProcess(UUID value) { while (ready == true) { try { wait(); } catch (InterruptedException e) { } } processToActivate = value; ready = true; notifyAll(); } The WorkProcess Interface The WorkProcess interface provides the Client Application API to the WorkflowService. It allows the client to connect and disconnect to the WorkflowService and to receive task assignments and update tasks assigned: Listing 11.12 The WorkProcess Interface package org.jworkplace.workflow; import java.rmi.RemoteException; import java.io.IOException; import net.jini.admin.Administrable; import org.jworkplace.account.Session; public interface WorkProcess 274 { public Session connect() throws RemoteException, IOException; public void disconnect() throws RemoteException, IOException; public void updateTask(Task item) throws RemoteException, IOException; public Task[] getTasksAssigned(Resource resource) throws RemoteException; } The WorkProcess Implementation The WorkflowService also implements the RemoteWorkProcess interface, although this would not have to be the case. A more complete implementation would separate the Process Definition interface from the Process Control interface. // // WorkProcess Interface // // connect to the workflow engine public Session connect(UUID processId) throws RemoteException { return new Session(); } // disconnect from the workflow engine public void disconnect(Session session) throws RemoteException { session.isActive(false); } // Determine if the task is still valid public void updateTask(Task task) throws RemoteException { ProcessDef processDef = null; // Determine if the task is still valid if(!processMap.containsKey(task.processId)) { return; } // update the task list for the worker updateTaskList(task); // get the associated work item processDef = (ProcessDef)processMap.get(task.processId); WorkItem item = processDef.getWorkItem(task.taskId); // update the StarEntry write(item, task); } private void updateTaskList(Task task) { List taskList = null; Task item = null; try { // find the task in the task list if(userMap.containsKey(task.user)) { taskList = (List)userMap.get(task.user); Iterator itr = taskList.iterator(); while(itr.hasNext()) { 275 item = (Task)itr.next(); if(item.taskId.equals( task.taskId)) { int pos = taskList.indexOf(item); // update the task to reflect any changes taskList.set(pos,task); } } } } catch(Exception e) { e.printStackTrace(); } } // called by clients to get task assingments public Task[] getTasksAssigned(Resource resource) throws RemoteException { List taskList = null; try { // assume resource contains worker String worker = resource.getResource(); if(!userMap.containsKey(worker)) return null; // get the tasklist associated with this worker taskList = (List)userMap.get(worker); } catch(Exception e) { e.printStackTrace(); return null; } return (Task[])taskList.toArray(new Task[0]); } // // Use the StarInterface proxy to interface with JavaSpaces // private synchronized void write(WorkItem item, Task task) { try { if(space == null) { System.out.println("WorkflowService::write Lost in space...unable to continue"); shutdown(); } // Use the item and task information // to create a StarEntry description Time time = item.activityTime(); String resource = item.getAssignedTo(); StarEntry entry = new StarEntry(task.processId, task.taskId, time.getStartTime(), time.getStopTime(), item.description, task.status, resource); // Write the StarEntry to JavaSpaces space.write(entry); } catch (Exception e) { e.printStackTrace(); } } Discovering Dependent Services 276 The implementation of the DiscoveryListener interface resolves references to the Star Service and the MailboxManager. There is no guarantee that the services will be active when the WorkflowService starts. If you fail to find either service, use the notify method of the ServiceRegistrar to notify you when the service joins the community: // // DiscoveryListener Interface // public synchronized void discarded(DiscoveryEvent e) { } public synchronized void discovered(DiscoveryEvent de) { // get the array of lookup services discovered ServiceRegistrar[] registrars = de.getRegistrars(); // find JavaSpace proxy findStarInterface(registrars); // find our mailbox manager findMailboxManager(registrars); // found everything we need terminate discovery if(space != null && mailbox != null) mgt.terminate(); } // loop through discovered service registrars private void findStarInterface(ServiceRegistrar[] registrars) { if(space == null) { try { Class[] cls = new Class[] { Class.forName(starInterfaceClass) } ; ServiceTemplate template = new ServiceTemplate(null, cls, null); for(int i=0; i < registrars.length; i++) { space = (StarInterface)findService(registrars[i], template); if(space != null) { break; } } } catch (Exception e) { e.printStackTrace(); } } } // loop through discovered service registrars private void findMailboxManager(ServiceRegistrar[] registrars) { if(mailbox == null) { try { Class[] cls = new Class[] { Class.forName(mailboxManagerClass) } ; ServiceTemplate template = new ServiceTemplate(null, cls, null); for(int i=0; i < registrars.length; i++) { mailbox = (MailboxManager)findService(registrars[i], template); if(mailbox != null) { break; } } 277 } catch (Exception e) { e.printStackTrace(); } } } // if unable to find service register for notification private synchronized Object findService(ServiceRegistrar registrar, ServiceTemplate template) throws RemoteException { Object service = null; try { service = registrar.lookup( template ); } catch ( RemoteException ex ) { ex.printStackTrace(); } // TRANSITION_NOMATCH_MATCH to indicate when template match occurs // generate remote event if ( service == null ) { registrar.notify(template, ServiceRegistrar.TRANSITION_NOMATCH_MATCH, (WorkflowBackend)this, null, Long.MAX_VALUE); } return service; } // // ServiceEvent Listener // public synchronized void notify(RemoteEvent event) throws UnknownEventException, RemoteException { // If service event received determine which service // has joined the community if(event instanceof ServiceEvent) { ServiceEvent serviceEvent = (ServiceEvent)event; ServiceItem item = serviceEvent.getServiceItem(); if(item.service instanceof StarInterface) { space = (StarInterface)item.service; } else if(item.service instanceof MailboxManager) { mailbox = (MailboxManager)item.service; } } } The WorkflowEngine Implementation The WorkflowEngine is responsible for the following: • • • Creating a space listener for CompleteTask state objects Creating a task for each WorkItem in the process definition and updating the task list for each worker Registering the workers with the MailboxManager if they have not already registered with the service Listing 11.13 The WorkflowEngine Class 278 // Internal class of WorkflowService private class WorkflowEngine extends Thread { public WorkflowEngine() { super(); start(); } public void run() { try { while (!isInterrupted()) { // wait for process to be started UUID uuid = getProcessId(); // create a task completion listener for this process createProcessListener(uuid, new CompleteTask(uuid)); // create all tasks to activate activateProcess(uuid); } } } catch (Exception e) { finally { } e.printStackTrace(); } private void activateProcess(UUID processId) { ProcessDef processDef = (ProcessDef)processMap.get(processId); try { WorkItem[] items = processDef.getWorkItems(); for(int i=0; i<items.length; i++) { System.out.println("Engine activateProcess: " + items[i].getId()); // create a Task for each WorkItem WorkItem item = (WorkItem)items[i]; String processName = processDef.getProcessName(); Task task = createTask(processId, processName, item); // update the task list for the worker if(task != null) updateTaskList(task); // write a StarEntry for each activity write(items[i], task); } } catch (Exception e) { e.printStackTrace(); } } private Task createTask(UUID processId, String processName, WorkItem item) { Task task = null; try { String taskId = item.getId(); String worker = item.getAssignedTo(); task = new Task(processId, processName, taskId, worker, 279 ActivityState.TASK_INIT); // does the worker already have a mailbox RemoteEventListener listener = mailbox.getListener(worker); if(listener == null) { // register the worker mailbox.register(worker, Long.MAX_VALUE); // create the registration template for the worker StarEntry entry = new StarEntry(); entry.resource = item.getAssignedTo(); // get the mailbox for worker listener = mailbox.getListener(worker); MarshalledObject handback = new MarshalledObject(worker); // set the worker mailbox to get space notifications space.setRemoteEventListener(entry, handback, listener); } } catch (Exception e) { e.printStackTrace(); } return task; } private synchronized void updateTaskList(Task task) { try { String worker = task.user; if(!userMap.containsKey(worker)) { List taskList = new ArrayList(); userMap.put(worker, taskList); } List taskList = (List)userMap.get(worker); taskList.add(task); } catch (Exception e) { e.printStackTrace(); } } } // end of WorkflowEngine The TaskListener (RemoteEventListener) The TaskListener is a service side listener that has been registered by the workflow engine for each process defined in the workflow system. It is notified when CompleteTask objects are written to space: Listing 11.14 The TaskListener Class package org.jworkplace.workflow; import import import import import java.rmi.RemoteException; java.rmi.MarshalledObject; java.rmi.server.UnicastRemoteObject; java.io.IOException; net.jini.core.event.RemoteEventListener; 280 import net.jini.core.event.RemoteEvent; public class TaskListener implements RemoteEventListener { private RemoteWorkProcess engine; private StarInterface space; public TaskListener(RemoteWorkProcess workProcess, StarInterface starInterface) throws RemoteException { this.engine = workProcess; this.space = starInterface; UnicastRemoteObject.exportObject(this); } // this should create a new thread public void notify(RemoteEvent event) { try { CompleteTask template = new CompleteTask(); while(template != null) { // for each complete task take from space template = (CompleteTask)space.takeStarIfExists(template); if(template != null) { // the handback will contain the Process Definition MarshalledObject handback = event.getRegistrationObject(); if(handback != null) { ProcessDef processDef = (ProcessDef)handback.get(); // Get the successor task WorkItem item = processDef.getNextItem(template.taskId); if (item != null) { // Create a task template String processName = processDef.getProcessName(); String taskId = item.getId(); String worker = item.getAssignedTo(); Task task = new Task(template.processId, processName, taskId, worker, ActivityState.TASK_READY); // Put the next task in a ready state engine.updateTask(task); } } } } } catch (Exception e) { e.printStackTrace(); } } } The MailEventHandler 281 The MailEventHandler is a client-side listener that has been registered by the client user interface for the user logged in to the workflow system. It receives notifications when a StarEntry is written that matches the user ID of the user connected to the system. Listing 11.15 The MailEventHandler Class package org.jworkplace.mailbox; import java.rmi.RemoteException; import java.rmi.RemoteEventListener; import java.rmi.MarshalledObject; import java.rmi.server.UnicastRemoteObject; import net.jini.core.event.UnknownEventException; import net.jini.core.event.RemoteEvent; import org.jworkplace.workflow.WorkProcessManager; import org.jworkplace.workflow.Resource; import org.jworkplace.workflow.Activity; import org.jworkplace.workflow.WorkItemModel; import org.jworkplace.workflow.Task; public class MailEventHandler extends UnicastRemoteObject implements RemoteEventListener { private EventView view; private WorkItemModel model; public MailEventHandler(EventView view, WorkItemModel model) throws RemoteException { this.view = view; this.model = model; } public synchronized void notify(RemoteEvent event) throws UnknownEventException, RemoteException { // the marshalled object contains the user id try { MarshalledObject object = event.getRegistrationObject(); String worker = (String)object.get(); // get the tasks assigned Task[] items = model.getTasksAssigned(worker); // update the user display view.callback(items); } catch (Exception e) { e.printStackTrace(); } } } Content Management Workflow For this example, a process for publishing health care content on the Web is defined as shown in Figure 11.9. The process definition involves a number of activities, and each activity adds specific content approval criteria and quality control to the publishing process. Figure 11.9. A defined process flow for health care content publishing to the Web. 282 Note The code, scripts, and instructions to run the Workflow system are available at www.jworkplace.org. Creating the Process Definition Figure 11.10 shows how work items are created and assigned in our process definition user interface. The Workflow UI has been integrated with the collaborative capabilities of the peer-topeer content management and instant messaging example from Chapter 10, "GroupWare." Figure 11.10. Defining the Process Definition work items. The Process Definition is entitled Health Care Content Publishing and is owned by the content administrator. The draft document item (first activity) has been assigned to the writer. A short descriptive instruction can be provided, and the scheduled start and completion times are recorded. You would create an item for each activity in the process. The Create Process panel provides an export function that publishes the process to the WorkflowService. 283 The Schedule Display The schedule display (see Figure 11.11) shows all the work items that have been defined in the process, the scheduled start and finish times, and the resources assigned. Figure 11.11. A sample schedule for health care content publishing is displayed by the schedule panel. Although the display in Figure 11.11 shows 12:00 a.m. as the time of day for each task, this is simply the default. Tasks can be scheduled for starting and completing on any time scale. The Writer's Task Assignment Display The example in Figure 11.12 is the result of the writer connecting to the EventMailbox manager and receiving task assignments. The status column indicates that the two tasks assigned to the writer in the Publishing process have yet to start. Figure 11.12. When the writer connects to the workflow system, all the writer's assigned tasks are displayed. 284 The writer indicates that a task has started by selecting the Activate button. When the task is complete the Complete button is selected by the writer. This triggers an update to the remote Workflow service, which writes an updated Task and CompleteTask entry. JavaSpaces notifies the EventMailbox listener, which delivers the updated status to the editor who is waiting to start the next task. The Editor's Task Assignment Display After the writer completes the first activity, the editor's display immediately reflects the change in status (see Figure 11.13). The editor's review task is now available and can be worked. Figure 11.13. The editor is notified when a task is in a "ready" status. 285 Summary This chapter demonstrated that sophisticated collaboration and workflow processing are possible with a minimum of effort when using Jini and Jini services. The example presented provides a working collaborative workflow application. It can be used by teams working together toward common goals. As mentioned, effective use of collaborative workflow is now considered vital in the success of enterprises of all kinds. You now have an application capable of the following: • • • Defining Process Definitions and work items Scheduling tasks and assigning resources Notifying participants when dependent tasks have completed and new tasks can start This example should suggest many other ideas for building collaborative applications using Jini and JavaSpaces. It can serve as a starting point for service composition in distributed workflow processing. 286 Chapter 12. Agents IN THIS CHAPTER • • • • • Agents of Collaboration Simple Agent Frameworks An Example Process Control Agent Future Jini Agents Summary This chapter provides an overview of software agents and the mapping of agent requirements to the Jini services. Demonstration of simple agents of collaboration in a development scenario is provided. We will build on the exercises in Chapter 10, "GroupWare," and Chapter 11, "Workflow," to automate communication between the development community. Agents of Collaboration Software agents always conjure up images of distant future sci-fi technologies; however, the future is now! The popularity of the Internet has ushered in a new era of communication and collaboration, fueled by an explosion of available content. Software agents are being developed to provide a framework for organizing and interpreting the vast amount of information available on the Internet. Agent development has also increased in popularity because the Web has become the prominent channel for user and business communication. This popularity has also blurred the distinction between traditional software and agent-based modules. Agent technology has also been impacted by the growth of e-commerce. A new breed of software agent is attempting to increase efficiency of transactions, across electronic markets and exchanges. Often we think of business-to-business (B2B) and business-to-consumer (B2C) as two separate models of participation and communication. However, B2B and B2C are not discrete models but rather exist on a continuum. Agents can be used to acquire knowledge about customers, partners, and product preferences. As a result, the capability to leverage software agents at key points, for instance in distribution, manufacturing, sales, and customer relationship management, is compelling. Imagine a business engaged in distributing products and services over the web. A tremendous amount of marketing research is involved in determining the effectiveness of distribution channels. Many companies are looking for more effective data capture and analysis techniques. Software agents provide a solution. Jini and JavaSpaces can provide a foundation for building agent frameworks. The benefits include the following: • • • • • • Software mobility "out-of-the-box" Distributed storage using JavaSpaces Support for communicating agents using distributed events A framework to find information, services, and agents A framework to support transactions using the two-phase commit protocol The capability to use cryptographic channels to communicate The Jini network of loosely coupled collaborating processes is an ideal foundation for the global competitive markets of the twenty-first century. As we have moved into the Information Age, 287 strategic advantage now lies not only in the acquisition of information but also in the knowledge it represents. Progressive corporations are investing in technologies to help systems and information workers become more effective in the capture, analysis, and dissemination of information (see Figure 12.1). Figure 12.1. Jini and RMI provide a compelling model for agent frameworks. Jini and JavaSpaces hold great promise in allowing systems, agents, and information workers to collaborate in new and exciting ways. These technologies will enable multidimensional information exchange and intuitive workgroup formation. Simple Agent Frameworks So what is an agent and how does it differ from any other software program? The distinction and refinement of what is and what is not an agent has generated a lot of press and discussion. A considerable amount of effort has been focused on classifying types of software agents. For instance, those that inhabit the physical world are called robots. Those that inhabit computer networks are sometimes referred to as softbots, and those that perform specific tasks are sometimes called taskbots. Researchers often use role-based classification in an attempt to elucidate agent definition. Accordingly we have search agents, navigation agents, management agents, domain-specific agents, as well as development and help agents. Although the line between agent and software program can be gray, other characteristics of agents help to delineate the distinction: • • • Learning—The capability for a software program to acquire knowledge without reprogramming Goal-oriented direction—The capability of a software program to be instructed as to a desired result and then be able to determine the accuracy of the attained result Community—The capability for software to be able to communicate with other software to emulate knowledge acquisition and goal-oriented behavior 288 Hyacinth Nwana in the paper "Software Agents: An Overview" provides a typology for classifying agents. Three attributes or characteristics of agents are identified: autonomy, learning, and cooperation (see Figure 12.2). Figure 12.2. A model for classifying software agents. Autonomous agents act on behalf of the user and exhibit a level of independence from user guidance. Learning agents are capable of "acquiring" knowledge using the resources in their environment. Cooperating agents are capable of interacting with other agents using common semantics, communication language, or ontology. Four types of agents result from this classification: smart, collaborative and learning, collaborative, and interface. However, only smart agents exhibit and aggregate autonomy, learning, and cooperational behavior as shown in Figure 12.2. Mobile Agents Autonomous agents are often defined as systems that can act on behalf of a user and exhibit capabilities to learn and cooperate with their environment. Typically, an agenda or goal drives an agent's behavior. A mobile agent can move across machines and networks to carry out this mission. There is a significant amount of work being done to provide mobile agents for network management. It is easy to envision the benefits gained by network managers using mobile agents to relay conditions and instructions to network elements. Jini provides a foundation for many of the requirements for mobile agents. As discussed previously, the capability to move code from machine to machine through downloadable proxies is a key enabler of the Jini framework. You can quickly define an interface for a remote agent and a mobile agent by exploiting RMI interface semantics. For instance, extending Remote provides an interface that is accessed remotely through a stub reference by the caller. 289 public interface RemoteAgent extends java.rmi.Remote { public void tell(Sender sender, Receiver receiver, Content content) throws RemoteException; } On the other hand, extending Serializable provides a mechanism to move the object to the caller. public interface MobileAgent extends java.io.Serializable { public void tell(Sender sender, Receiver receiver, Content content) throws RemoteException; } You can even pass references from caller to receiver and implement powerful callback mechanisms. Personal Assistants A Personal Assistant is a software agent that acts semi-autonomously for and on behalf of a user. Often it accomplishes routine support tasks to allow the user to concentrate on more important activities. It is unobtrusive but ready when needed and possesses knowledge about the user and her areas of work. The notion of a Personal Assistant is broad. A Personal Assistant can be used for any of the following tasks: • • • • • • Managing a user's calendar Filtering and sorting e-mail Managing a user's activities, plans, and tasks Locating and delivering information Purchasing items Planning travel Henry Lieberman, of the Media Laboratory at the Massachusetts Institute of Technology, developed a Web browsing agent called Letizia. It assists users in navigating the voluminous content of the Web. His focus has been on combining two popular trends in agent development: "Interface agents, software that actively assists a user in operating an interactive interface, and autonomous agents, software that takes action without user intervention and operates concurrently, either while the user is idle or taking other actions." So, let's continue building on the collaboration capabilities of the last two chapters. You will build an agent capable of communicating with a user regarding the status of assigned tasks. An Example Process Control Agent One function of a Personal Assistant is to help a user manage tasks and control the scheduling of activities. Calendaring agents assist with the management of a user's calendar, such as scheduling appointments and sending reminders and meeting alerts. Often this type of agent works in the background, monitoring a user calendar and triggering alert notifications. 290 The Process Control agent defined in this example is similar to a Personal Assistant; however, it is used to monitor task progress and alert the user through notification messages. In addition, it demonstrates the following: • • The LeaseRenewalService as a framework for resource monitoring JavaSpaces as a platform for distributed communication The Workflow Service defined in Chapter 11 provides the necessary process and task definition for the Process Control Agent (see Figure 12.3). Figure 12.3. Agent process monitoring using the Lease Renewal Service. Monitoring Process and Task Definition When a new process is defined, a ProcessEntry is written to JavaSpaces by the WorkflowService (see Listing 12.1). Listing 12.1 The ProcessEntry Class package org.jworkplace.workflow; import java.rmi.RemoteException; import net.jini.entry.AbstractEntry; import com.sun.jini.proxy.UUID; public class ProcessEntry extends AbstractEntry implements ActivityState { 291 public UUID processId; public ProcessEntry() { this(null); } public ProcessEntry(UUID processId) { this.processId = processId; } public public public public void void void void start() throws RemoteException { } terminate() throws RemoteException { } suspend() throws RemoteException { } resume() throws RemoteException { } } Similar to the TaskEntry (see Chapter 11), the ProcessEntry extends AbstractEntry and implements the ActivityState interface. This class might be used as a base class for future process customization. The main point to note in this example is the initialization of the ProcessEntry with the unique process identifier. The AgentService creates a single process listener. A ProcessListener is registered to receive notification of process entries written to JavaSpaces in Step 1(see Figure 12.3). When a new process is defined the WorkflowService writes a ProcessEntry to space as in Step 2. The notify method of the ProcessListener is invoked in Step 3. The ProcessListener delegates the notification event to the AgentService (Step 4 in the diagram shown in Figure 12.3). The AgentService creates an associated process monitor. The process monitor is used to alert a user agent of a task-in-trouble. A task-in-trouble is a task that has not completed and is scheduled to complete within a certain time frame or "window." Although this example does not focus on task-in-trouble or risk assessment definition, it is clear that the framework can be used to trigger notifications based on any risk assessment strategy that may be appropriate. The AgentService removes the ProcessEntry (Step 5) from JavaSpaces and creates a LeaseRenewalSet, as shown in Step 6. The AgentService provides a mapping between process definitions and an associated LeaseRenewalSet. Per-process lease management is provided through the AgentService interface. In other words, the AgentService provides a common interface for the management of leases during the lifetime of an active process. The AgentService creates a LeaseRenewalSet by invoking the createLeaseRenewalSet method of the LeaseRenewalService (LRS). The Sun lease renewal service "norm" is an example implementation. Monitoring Process and Task Completion As discussed in Chapter 7, "The Helper Services," the LRS is a helper service that can be employed by both Jini clients and services to perform all lease renewal duties on their behalf (see Figure 12.4). Figure 12.4. The AgentService uses the Jini Lease Renewal Service to manage leases and provide an early warning notification system for monitoring workflow activity. 292 The LRS enables client-using entities to remain inactive until they are needed and can request that the lease renewal service take on the responsibility of renewing the leases granted to the service. This enables the service to be dormant without risking the loss of access to resources. In the AgentService you are also using the LRS as a mechanism for generating time-based events. The LeaseRenewalSet is itself a leased resource with special semantics. The setExpiration WarningListener (Step 7 in Figure 12.3) method of the LeaseRenewalSet, is used to set a RemoteEventListener notified prior to the expiration of the lease renewal set lease in Step 8. This method takes a long parameter that indicates to the lease renewal set the number of milliseconds prior to expiration to generate a warning event. The RemoteEvent is an instance of ExpirationWarningEvent, which can be used to retrieve the lease associated with the lease renewal set. The following code fragment demonstrates the necessary code to retrieve the Lease associated with the expiring lease renewal set: public synchronized void notify(RemoteEvent event) throws UnknownEventException, RemoteException { try { if(event instanceof ExpirationWarningEvent) { ExpirationWarningEvent evt = (ExpirationWarningEvent)event; Lease lease = evt.getRenewalSetLease(); In addition to the lease, the event notification can contain a MarshalledObject, if it is passed during registration of the expiration warning listener. The getRegistrationObject method of the remote event is used to retrieve the handback object. MarshalledObject handback = evt.getRegistrationObject(); The expiration time of the lease renewal set is set to coincide with the completion time of the next work item in the process definition. As you may recall, the work items in the process definition are sorted according to start time. In the AgentService example, you will create a simple 293 single dependence graph of tasks. In other words, a single predecessor task must complete before a successor task can start. Of course more complex task graphs should be supported in production systems. The stop time of each work item is used to generate an expiration time for the lease renewal set. The expiration time is an adjusted stop time value. The computeWarningTime method of the AgentService is used to generate the notification value. The lease renewal set uses the value to trigger a remote event to the registered ProcessListener, which invokes the Agent value. The lease renewal set uses the value to trigger a remote event to the registered ProcessListener, which invokes the Agent Service's monitorAlert method (see Figure 12.4). The following code fragment from ProcessListener simply checks the type of event and invokes the appropriate agent method. public synchronized void notify(RemoteEvent event) throws UnknownEventException, RemoteException { try { // is a lease renewal set warning being triggered if(event instanceof ExpirationWarningEvent) { // activate monitor alert ExpirationWarningEvent evt = (ExpirationWarningEvent)event; // get the lease for the lease renewal set Lease lease = evt.getRenewalSetLease(); // get the hanback MarshalledObject handback = evt.getRegistrationObject(); agentService.monitorAlert(lease, handback); // assume a new process has been defined } else { // create lease renewal set and activate monitor for new process agentService.activateMonitor(event); } } catch (Exception e) { e.printStackTrace(); } } The activateMonitor method of the AgentService is used to retrieve the ProcessEntry and the associated ProcessDef. The getRenewalTime sets the lease expiration for the lease renewal set to the completion time of the first task in the process. The computeWarningTime provides the advance notification mechanism. This triggers the notify method (refer to Step 8 in Figure 12.3). public void activateMonitor(RemoteEvent event) { try { // get the process to monitor ProcessEntry template = new ProcessEntry(); ProcessEntry entry = (ProcessEntry) space.takeStar(template); // get the process definition ProcessDef processDef = workflowManager.getProcessDef(entry.processId); // get lease set renewal time long renewalTime = getRenewalTime(processDef, 0); 294 // are there tasks to monitor for this process if(renewalTime > 0) { // create a lease set for this process LeaseRenewalSet leaseRenewalSet = leaseRenewalService.createLeaseRenewalSet(renewalTime); // create the handback MarshalledObject handback = new MarshalledObject(new AgentProcessControl(entry.processId)); // activate process monitor setMonitor(leaseRenewalSet, computeWarningTime(), handback); // update the processMap processMap.put(entry.processId, leaseRenewalSet); } } catch (Exception e) { e.printStackTrace(); } } The monitorAlert method (Step 9 in Figure 12.3) uses the registration handback (AgentProcessControl) to identify the process and work item that triggered the warning expiration. This method determines the next work item to monitor and updates the associated lease renewal set. If all tasks are complete, the lease on the lease renewal set is cancelled, and the process is removed from the agent monitor: public void monitorAlert(Lease lease, MarshalledObject handback) { try { // get the process and update the expiration time AgentProcessControl processControl = (AgentProcessControl)handback.get(); UUID processId = processControl.processId; int index = processControl.index; // get the process definition ProcessDef processDef = workflowManager.getProcessDef(processId); // get lease set renewal time long renewalTime = getRenewalTime(processDef, index); // get lease renewal set LeaseRenewalSet leaseRenewalSet = (LeaseRenewalSet) processMap.get(processId); // are there tasks to monitor for this process if(renewalTime > 0) { // update the lease on the renewal set lease.renew(renewalTime); // update the handback processControl.increment(); handback = new MarshalledObject(processControl); 295 // update the monitor setMonitor(leaseRenewalSet, computeWarningTime(), handback); } else { // destroy the monitor lease.cancel(); // remove the process processMap.remove(processId); } } catch (Exception e) { e.printStackTrace(); } } The setMonitor method invokes the setExpirationWarningListener of the lease renewal set. In the preceding example, you created a single process listener associated with multiple renewal sets. Of course you could define a process listener per renewal set. This would be the desirable choice as your system becomes more complex but would incur more resource overhead. In other words, you would be exporting a process listener for every process defined in the system. However, this would be a more flexible solution. The intent here is to demonstrate the lease renewal service as a lease management service in the context of process management and agent event notification. public void setMonitor(LeaseRenewalSet leaseRenewalSet, long expirationTime, MarshalledObject handback) throws UnknownEventException, RemoteException { try { leaseRenewalSet.setExpirationWarningListener(getWarningEvent Listener(), expirationTime, handback); } catch (Exception e) { e.printStackTrace(); } } In addition, the AgentService communicates an alert to the user assigned to the task being monitored. The AgentService uses the StarInterface proxy to write alert messages to JavaSpaces. The chat message techniques described in Chapter 5, "JavaSpaces Service," are used to create a channel for communicating with the user. Channels of Communication JavaSpaces provides a platform for loosely coupled communication. It enables distributed processes to communicate by exchanging entries or serialized data streams. Collaborating agents require this type of communication capability, in addition to supporting common semantics, for information exchange (see Figure 12.5). Figure 12.5. An Interpreter is required to translate between different agent dialects. Interpreters can be "chained" to form a Web of communication. 296 Although JavaSpaces can provide the underlying framework for communication, interfaces must be defined to represent the common information exchange. Knowledge Query and Manipulation Language (KQML) The External Interfaces Working Group is a collection of artificial intelligence and distributed systems researchers interested in software systems of communicating agents. The group was formed in 1990 with the charter to develop protocols for the exchange of represented knowledge among autonomous information systems. The result of this effort is KQML, the Knowledge Query and Manipulation Language. KQML is a language and protocol to support communication between (intelligent) software agents. KQML defines messages or speech acts. A KQML message is also called a performative. A performative is expressed as an ASCII string (see Figure 12.6). Agents exchange statements in two categories: beliefs and goals. Beliefs represent the knowledge the agent has about its environment. The goals represent the processing states the agent wants to attain or achieve. Together this information comprises the agent's view of the world and is represented as a knowledge base. Communication between agents is achieved by querying and manipulating the contents of each agent's knowledge base. Additional information on KQML can be found at www.cs.umbc.edu/kqml/. Figure 12.6. KQML represents the communication between collaborating agents as "speech acts." The speech act is encoded as an ASCII string and has a defined structure. KQML has standard speech acts, called performatives, such as "tell" and "monitor." The implementation of an agent is not necessarily structured as a knowledge base. The implementation may use a database system or a program using a special data structure. The only 297 requirement being that the system can translate the communication exchange into performatives, which represent the virtual knowledge base of the agent. Of course without common communication semantics, agents would be unable to exchange information and coordinate activities. The standardization of information exchange is an important area of agent research. Natural Language Processing Natural language processing (NLP) is an alternative to constraining an agent dialogue to a set of predefined speech acts. NLP aims to support a more natural exchange of information by reflecting the discourse as normal conversation. Most of this effort has been driven by improving human-tocomputer interaction. As mentioned, although JavaSpaces can provide the underlying framework for communication, standard interfaces must still be defined to represent the semantics of the exchange. KQML performatives and NLP provide two possible approaches to standardizing agent communication: the first (KQML), built on a standard definition of speech acts, and the latter (NLP) attempting to process and understand human communication. Great strides have been made in recent years with NLP systems. JavaSpaces could provide the common communication channel to integrate many agent communication models. Alert Notification Although Jini and JavaSpaces do not provide a solution to knowledge representation or standardizing agent communication, they do provide an underlying framework for communication. The AgentService uses this framework to communicate process status between agents or between agents and users (see Figure 12.7). You can use the StarService introduced in Chapter 10, which provides the interface to JavaSpaces, to enable this communication channel. Figure 12.7. The AgentService uses the StarService to establish a communication channel with users and agents. 298 You can extend the ChatMessage introduced in Chapter 5 to define an AlertMessage. The AlertMessage provides the notification information when a task is in trouble: import net.jini.entry.AbstractEntry; import org.jworkplace.chat.ChatMessage; public class AlertMessage extends ChatMessage { public AlertMessage(){ } public AlertMessage(String channel, Integer position, String text) { super(channel, position, text); } } The String parameter can be defined to express a speech act, such as that defined by KQML or an NLP construct. The AlertMessage uses the channel to communicate with one or more receivers of alert notifications. The receivers can be human, other software agents, or a combination of both, as shown in Figure 12.8. Figure 12.8. The agent service will generate an alert message for tasks that are in trouble. Future Jini Agents 299 As you study the Jini and JavaSpaces framework, you will discover many different areas where agent behavior can fit. A few examples may help to provide future exploration. Proxy Agents A proxy agent could be a specialized type of Personal Assistant that learns not from user keystrokes, but from monitoring remote service interaction. This would be beneficial in discovering software interface usage patterns. Frequency of remote invocations would provide additional insight into modeling performance-sensitive systems—for example, systems that are bandwidth-constrained or resource-constrained in general. Proxy agents could also be used to monitor service availability and interject intelligent routing or failover processing capabilities. Discovery Agents A discovery agent could help to manage the lookup process for clients and the join process for services. An agent could learn operational characteristics. For example, it could rate services on cost, availability, and reliability and determine communities of participation based on rules or service entry inspection. Service Capability Agents As we move to composite services or service components, more reliance will be placed on services cooperating and collaborating to provide features and functions. A capability agent could be designed with the intent of mapping capabilities to platform constraints or the current state of the network. For instance, a service currently not reachable might not necessitate total elimination of a service offering but only partial disruption. A capability agent could learn by listening to monitors and then alerting other agents in the system of the condition. Service factories could listen for such notifications and temporarily build the appropriate interfaces. Communication Agents As mentioned earlier in this chapter, communication agents could help to resolve agent dialect differences. Interpretive agents could bridge differences in knowledge base definitions or broadcast requests for services that understand a specific ontology. Summary Agent technology has been affected by the growth of e-commerce. A new breed of software agent is attempting to increase the efficiency of transactions across electronic markets and exchanges. Three attributes or characteristics of agents are autonomy, learning, and cooperation. A Personal Assistant is an agent that helps a user manage tasks and control the scheduling of activities. The Process Control agent defined in this chapter is similar to a Personal Assistant; however, it is used to monitor task progress and alert the user through notification messages. In addition, it demonstrates: • • The LeaseRenewalService as a framework for resource monitoring, and JavaSpaces as a platform for distributed communication. 300 We are still far from Hal, of 2001: A Space Odyssey fame, but we are moving quickly to practical application of agents assisting our business, personal, and recreational activities in the digital world. Jini and JavaSpaces provide a platform for agent frameworks that includes the following: • • • • Software mobility "out-of-the-box" to support mobile agents Improved security based on the Java sandbox Distributed storage using JavaSpaces for building virtual knowledge bases and simplifying distributed communication A framework that provides dynamic discovery of information, services, and agents Many obstacles still will need to be addressed to build the "smart agents" mentioned at the beginning of this chapter. But Jini does present an approach for building many practical applications today. 301 Chapter 13. Jini as a Web Service IN THIS CHAPTER • • • • • • Business-to-Business E-commerce What Are Web Services? Simple Object Access Protocol (SOAP) as a Transport Technology Jini in the B2B Context SOAP-to-Jini Integration Summary This chapter presents Jini in the context of the current trend toward XML-defined Web services. It provides an overview of the primary components defining Web services, which include the following: • • • • The Simple Object Access Protocol (SOAP) Web Service Definition Language (WSDL) Universal Description, Discovery, and Integration (UDDI) The Electronic Business XML initiative (ebXML) The intent is to demonstrate how Jini can play a role in e-commerce evolution. Business-to-Business E-commerce Business-to-business (B2B) e-commerce has become the focus of many business and technology efforts dominating the Internet today. B2B e-commerce can be defined as the transactions businesses engage in to conduct finance, commerce, and trade using the Internet. Many technology solutions have been promoted to support B2B, but the Web services architecture appears to be the heavyweight contender. Why shouldn't it—being initiated by the likes of IBM, Microsoft, and Ariba—with every major vendor on the planet participating in some fashion? Web services are being promoted as the next logical extension to delivering software functionality over the Internet. It is being projected that future applications will be built from Web services dynamically selected at runtime based on their cost, quality, and availability. What Are Web Services? A Web service is a collection of functions packaged and published to a network for use by other programs and users. Web services result from software components being assembled and discovered by service providers and service consumers. The intent is to provide a level of integration standardization in making products and services available worldwide. The definition takes on more substance when combined with the technologies defining it. Web services promote the following standards: • • SOAP (Simple Object Access Protocol) initially using HTTP or SMTP to standardize message exchange WSDL (Web Service Definition Language)—A standard service definition protocol 302 • UDDI (Universal Description, Discovery, and Integration)—A standard service discovery protocol The W3C and members of key organizations—IBM and Microsoft—in Web service development envision an end-to-end view to Web service definition. This can be demonstrated as three complementary architectural stacks (see Figure 13.1). The wire stack provides the "on-the-wire" view of the supporting message exchange. The description stack enables business requirements to be mapped to technical specifications. The discovery stack provides the advertisement and categorization services necessary for business entities to publish and find service definitions. Figure 13.1. The architectural stacks of Web services. Let's look at the major components to Web services in more detail and in the process map those components to similar entities within the Jini model. Simple Object Access Protocol (SOAP) as a Transport Technology The Simple Object Access Protocol (SOAP) is used to enable interoperability between vendor implementations. SOAP is a lightweight protocol for the exchange of information in a decentralized, distributed environment. It is an XML-based protocol that consists of three parts: • • • An envelope that defines a framework for describing what is in a message and how to process it A set of encoding rules for expressing instances of application-defined data types A convention for representing remote procedure calls and responses The following example is a request for a stock quote from a stock quoting Web service. Example: SOAP Request and Response using HTTP POST from the W3C Specification at www.w3.org/TR/SOAP/. Listing 13.1 SOAP Request POST /StockQuote HTTP/1.1 Host: www.stockquoteserver.com 303 Content-Type: text/xml; charset="utf-8" Content-Length: nnnn SOAPAction: "Some-URI" <SOAP-ENV:Envelope xmlns:SOAPENV=http://schemas.xmlsoap.org/soap/envelope/ SOAPENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"> <SOAP-ENV:Body> <m:GetQuote xmlns:m="Some-URI"> <symbol>DIS</symbol> </m:GetQuote> </SOAP-ENV:Body> </SOAP-ENV:Envelope> Example: SOAP Response Listing 13.2 SOAP Response HTTP/1.1 200 OK Content-Type: text/xml; charset="utf-8" Content-Length: nnnn <SOAP-ENV:Envelope xmlns:SOAP-ENV=http://schemas.xmlsoap.org/soap/envelope/ SOAPENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/> <SOAP-ENV:Body> <m:GetQuoteResponse xmlns:m="Some-URI"> <Price>34.5</Price> </m:GetQuoteResponse> </SOAP-ENV:Body> </SOAP-ENV:Envelope> SOAP can use HTTP as a transport protocol. By using HTTP, SOAP has access to most Webbased implementations today. This enables access to a vast array of resources, even through firewalls, if the Web-based implementation is SOAP-compliant. SOAP relies heavily on the namespace identifier for the elements and attributes defined in the SOAP envelope. xmlns:SOAP-ENV=http://schemas.xmlsoap.org/soap/envelope/ In addition the SOAP encodingStyle attribute can be used to indicate the serialization rules used in a SOAP message: SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/> The attribute value is an ordered list of one or more URIs identifying the serialization rules that can be used to deserialize the SOAP message indicated in the order of most specific to least specific. The SOAP Body element provides a mechanism for exchanging mandatory information intended for the ultimate recipient of the message: <m:GetQuote xmlns:m="Some-URI"> <symbol>DIS</symbol> 304 </m:GetQuote> The SOAP Fault element is used to carry error and/or status information within a SOAP message: <SOAP-ENV:Fault> <faultcode>SOAP-ENV:MustUnderstand</faultcode> <faultstring>SOAP Must Understand Error</faultstring> </SOAP-ENV:Fault> Web Service Definition Language (WSDL) WSDL is an XML language for describing Web services as a set of network endpoints that operate on messages. A WSDL service description contains an abstract definition for a set of operations and messages, a concrete protocol binding for these operations and messages, and a network endpoint specification for the binding. It is important to recognize that Web services are being defined to support different technology platforms, operating systems, and programming languages. Although our discussion focuses on Java, Web services may be defined for any system and language that supports the specification. Thus the Microsoft .NET initiative using non-Java-based implementations should integrate with IBM Java-based implementations. This is an important distinction from the Jini model of service delivery, which assumes a Java-based interface. WSDL allows the separation of the description of a service into two parts: the service interface document and the service implementation document (see Figure 13.2). Information common to a certain category of business services, such as message formats, interfaces, and protocol bindings, are included in the service interface document. Information pertaining to a particular service endpoint (that is, port definition) is included in the service implementation portion. Figure 13.2. The WSDL's service description in two parts: the service interface document and the service implementation document. Example: WSDL Service Interface Document and Implementation Document from the W3C Specification at www.w3.org/TR/WSDL/. 305 Listing 13.3 The WSDL Service Interface Document <?xml version="1.0"?> <definitions name="StockQuoteService-interface" targetNamespace="http://www.getquote.com/StockQuoteServiceinterface" xmlns:tns="http://www.getquote.com/StockQuoteService-interface" xmlns:xsd=" http://www.w3.org/2001/XMLSchema " xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns="http://schemas.xmlsoap.org/wsdl/"> <documentation> Standard WSDL service interface definition for a stock quote service. </documentation> <message name="SingleSymbolRequest"> <part name="symbol" type="xsd:string"/> </message> <message name="SingleSymbolQuoteResponse"> <part name="quote" type="xsd:string"/> </message> <portType name="SingleSymbolStockQuoteService"> <operation name="getQuote"> <input message="tns:SingleSymbolRequest"/> <output message="tns:SingleSymbolQuoteResponse"/> </operation> </portType> <binding name="SingleSymbolBinding" type="tns:SingleSymbolStockQuoteService"> <soap:binding style="rpc" transport="http://schemas.xmlsoap.org/soap/http"/> <operation name="getQuote"> <soap:operation soapAction="http://www.getquote.com/GetQuote"/> <input> <soap:body use="encoded" namespace= "urn:single-symbolstock-quotes" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/> </input> <output> <soap:body use="encoded" namespace="urn:single-symbol-stockquotes" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/> </output> </operation> </binding> </definitions> A service interface document can reference another service interface document using an import element. For example, a service interface that contains only the message and portType elements can be referenced by another service interface that contains only bindings for the portType. A service implementation document contains a description of a service that implements a service interface. At least one of the import elements will contain a reference to the WSDL service interface document. A service implementation document can contain references to more than one service interface document. Listing 13.4 WSDL Service Implementation Document 306 <?xml version="1.0"?> <definitions name="StockQuoteService" targetNamespace="http://www.getquote.com/StockQuoteService" xmlns:interface="http://www.getquote.com/StockQuoteServiceinterface" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns="http://schemas.xmlsoap.org/wsdl/"> <documentation> This service provides an implementation of a standard stock quote service. The Web service uses the live stock quote service provided by XMLtoday.com. The XMLtoday.com stock quote service uses an HTTP GET interface to request a quote, and returns an XML string as a response. </documentation> <import namespace="http://www.getquote.com/StockQuoteServiceinterface" location="http://www.getquote.com/wsdl/SQS-interface.wsdl"/> <service name="StockQuoteService"> <documentation> Stock Quote Service </documentation> <port name="SingleSymbolServicePort" binding="interface:SingleSymbolBinding"> <documentation>Single Symbol Stock Quote Service</documentation> <soap:address location="http://www.getquote.com/ stockquoteservice"/> </port> </service> </definitions> The import element in a WSDL service implementation document contains two attributes. The namespace attribute value is a URL that matches the targetNamespace in the service interface document. The location attribute is a URL used to reference the WSDL document that contains the complete service interface definition. The binding attribute on the port element contains a reference to a specific binding in the service interface document. The service interface document is developed and published by the service interface provider. The service implementation document is created and published by the service provider. The roles of the service interface provider and service provider are logically separate, but they can be the same business entity. A service implementation is published in a UDDI registry as a business service with one or more binding templates. The business service is published by the service provider. Jini does not rely on XML for service definition. As mentioned, Jini relies on Java interfaces. The WSDL has no direct counterpart in Jini, other than both provide a mechanism to define service interfaces. The UDDI, however, does resemble the lookup and discovery service to some extent. Universal Discovery and Description Integration (UDDI) UDDI provides a method for publishing and finding service descriptions. The UDDI data entities provide support for defining both business and service information. Compare and contrast this to the Entry objects augmenting service definition in the ServiceRegistrar. The service description information defined in WSDL is complementary to the information found in a UDDI registry. UDDI provides support for different types of service descriptions and does not 307 mandate WSDL. As a result, UDDI has no direct support (as of this writing) for WSDL or any other service description mechanism. It is possible to define bridges between a UDDI registry and a Jini LUS. The UDDI defines three primary components: • • • A registry for service providers to publish services and products offered A service broker that maintains the registry A service requestor that uses the broker to find products and services The service requestor must be able to locate a UDDI registry to gain access to the B2B network of services. The Web services architecture defines three core services (see Figure 13.3): • • • The publish service enables the service provider to publish a service description, so the service requestor can find, understand, and use the service. The find/discovery service enables the service requestor to retrieve a service description directly, or query the service registry for the type and description of the service required for invocation. The bind service enables a service requestor to invoke or initiate an interaction with the service at runtime using the binding details in the service description. Figure 13.3. These three operations—publish, find, and bind—are core to the Web services model. The service provider is the platform that hosts access to the Web service. This is typically the business process owner. The service requestor is the application searching for and invoking or initiating an interaction with a service. This can be a person or a program—for example, another Web service. Service requestors can search by industry code, products and services offered, and geographic location. The service registry is a searchable registry of service descriptions where service providers publish their service definitions. Service requestors find services and obtain binding information (in the service descriptions) for services during service development or during execution. UDDI defines four basic data elements within the data model as shown in Figure 13.4. In version 1.0, they are as follows: • • • • businessEntity (modeling business information) businessService (high-level service description) tModel (modeling a technology type or service type) bindingTemplate (mapping between businessService and tModels) 308 Figure 13.4. UDDI provides a model for business and technical information. The businessEntity provides information about a business and can contain one or more businessServices. The business is the service provider. The technical and business descriptions for a Web service are defined in a businessService and its bindingTemplates. Each bindingTemplate contains a reference to one or more tModels. A tModel is used to define the technical specification for a service. The individual facts about a business, its services, its technical information, or information about specifications for services are accessed individually by way of unique identifiers, or keys. A UDDI registry assigns these unique identifiers when information is first saved, and these identifiers can be used later as keys to access the specific data instances on demand. Each unique identifier generated by a UDDI registry takes the form of a Universally Unique ID (UUID). This is similar to the process of assigning a UUID by the LUS to a service in the service registrar. The businessEntity structure contains one or more unique businessService structures. Similarly, individual businessService structures contain specific instances of bindingTemplate data, which in turn contains information that includes pointers to specific instances of tModel structures. From an XML standpoint, the businessEntity is the top-level data structure that accommodates holding descriptive information about a business or entity. Service descriptions and technical information are expressed within a businessEntity by a containment relationship. The primary role that a tModel plays is to represent a technical specification. An example might be a specification that outlines wire protocols, interchange formats, and interchange sequencing rules. UDDI Web Service Categorization Categorization and the capability to assign category information to data is fundamental to the UDDI registry. Categorization takes the form of classifying information related to some wellknown industry, product, or geographic categorization code set (see Figure 13.5). The intent is to improve the capability to locate data within the UDDI registry for a specific purpose. However, it is impractical to assume that the UDDI registry will be useful for general-purpose business search 309 due to the size of the registry database. It is envisioned that higher level services will be defined to accommodate specific classifications or ontology representations. Figure 13.5. The UUDI "cloud" is a set of distributed—replicated—nodes of UDDI registries. These nodes provide the base definitions and categorization of Web services—for example, industry, product, and geography. The UDDI business registry is a logically centralized, physically distributed service with multiple root nodes that replicate data with each other regularly. After a business registers with a single instance of the business registry service, the data is automatically shared with other UDDI root nodes and becomes freely available to anyone who needs to discover what Web services are exposed by a given business. This again provides some comparison to the role of the LUS in terms of propagation of service information. However, the dynamic capabilities of service discovery within the Jini architecture are far more flexible and adaptable to dynamic changes in domain space. It is projected that public and private UDDI nodes will be defined. The UDDI Operator nodes form the operator cloud facility that can be accessed from www.uddi.org. This represents the public nodes of the UDDI network. Private nodes between partner systems and within large corporations may also surface—for example, e-marketplace UDDI nodes for finding Web services and for doing business within a particular e-marketplace or industry; portal UDDI nodes supporting transactional Web presence and for restricting access and monitoring usage; internal integration nodes to bridge Web presence with back-end service delivery. As expected, there are complementary and competing views to the definition of end-to-end ecommerce. 310 Electronic Business XML (ebXML) The Electronic Business XML (ebXML) initiative is being defined by the United Nations body for Trade Facilitation and Electronic Business (UN/CEFACT) and the Organization for the Advancement of Structured Information Standards(OASIS). They have also initiated a project to standardize XML business specifications. The most important components of the ebXML architecture are the Registry and the Repository (see Figure 13.6). The Registry provides access services, information models, and reference implementation information. The repository provides the physical back-end information store. The repository contains Document Type Definitions (DTD) and schemas retrieved by the registry for conducting e-commerce transactions using ebXML. The ebXML Messaging Service provides for the exchange of ebXML messages between trading partners over various transports (SMTP, HTTP/S, and FTP). Visit www.ebxml.org for more information. Figure 13.6. ebXML is taking a top-down design to e-commerce definition. The initiative complements and competes with the IBM/Microsoft Web service definition. Web services and ebXML recognize the importance of providing a structure and process to B2B system integration. Both approaches require the implementation of a well-known interface at a well-known location. Each approach involves a registry or broker to resolve the following questions: • • • • • What services are available? Who can provide the service? Where is the service located? How is the service invoked? What are the technology requirements? Both approaches use XML to define and specify the answers to these questions. In addition, a considerable amount of effort is required to document the interface and to exchange the necessary information to conduct B2B e-commerce. Needless to say, definition overlap and divergent solutions have and will continue to occur. The degree of divergence in solutions and implications has yet to be fully understood. Expect to see software bridges that enable key component integration or coexistence in areas such as UDDI Service Registry and ebXML Service Repository. The overall architecture is still in flux, and the pace of change presents a challenge to early adopters. There are divisions even within the Web services architecture as competitors try to differentiate their tools and solutions. 311 Jini in the B2B Context Like UDDI and ebXML, Jini supports the concept of discovering services. Conceptually, the lookup service can be thought of as containing any number of service items. Each service item can have attributes associated with it. For instance, like UDDI, they may include industry codes, implementation details, access protocols, and geographic locations (see Figure 13.7). These attributes can be used to refine the search for a specific service. Figure 13.7. Like the UDDI registry, the lookup service can be augmented with classification information by using service attributes. In addition, a proxy for the lookup service is available that knows how to interface with the lookup service (see Figure 13.8). The code is downloadable to the client requesting the service. Downloadable service proxies are one of the key differences between other "yellow pages"-like services and Jini. It is also one of the main reasons it depends on a particular programming language. Figure 13.8. UDDI and the Jini lookup service share "yellow page" concepts, but Jini leverages RMI and the Java platform to provide a more robust interaction model. 312 So, although the other approaches to service definition are being satisfied through agreed-on XML document schemas, Jini discovery and lookup provides a dynamic alternative to finding and interacting with B2B service providers. Jini itself could be used to bootstrap the process of finding UDDI and ebXML registries as shown in Figure 13.9. Figure 13.9. Bridging registries: UDDI-to-LUS. SOAP-to-Jini Integration Listing 13.5 demonstrates a SOAP client accessing a Web service. The Web service is actually a proxy accessing the Jini lookup service. The SOAP client lists the services registered in a Jini lookup service. 313 Listing 13.5 The LookupProxy Class package chapter13; import java.io.*; import java.rmi.*; import java.util.*; import import import import import import import import import import net.jini.core.entry.Entry; net.jini.core.lookup.ServiceID; net.jini.core.lookup.ServiceItem; net.jini.core.lookup.ServiceMatches; net.jini.core.lookup.ServiceRegistrar; net.jini.core.lookup.ServiceTemplate; net.jini.discovery.DiscoveryEvent; net.jini.discovery.DiscoveryListener; net.jini.discovery.LookupDiscoveryManager; net.jini.space.JavaSpace; public class LookupProxy implements DiscoveryListener { private String[] groups = new String[] { "" } ; private Vector services = new Vector(); private StringBuffer buffer = new StringBuffer(); private private private private private LookupDiscoveryManager ldm; ServiceTemplate template; JavaSpace space; ServiceID serviceID; ServiceItem si; public LookupProxy() { System.setSecurityManager(new RMISecurityManager()); } // DiscoveryListener Implementation // Simply build a list of services that match // a supplied template public synchronized void discovered(DiscoveryEvent de) { emit("Discovered LUS: "); ServiceRegistrar[] registrars = de.getRegistrars(); for(int i=0; i < registrars.length; i++) { try { emit("URL: " + registrars[i].getLocator().toString()); emit("ID: " + registrars[i].getServiceID()); String groups[] = registrars[i].getGroups(); emit("GROUPS: " + groups[0]); ServiceMatches sm = registrars[i].lookup(template, Integer.MAX_VALUE); emit("Matching services found ------: " + sm.totalMatches); emit(""); for(int j=0; j < sm.items.length; j++) { // Process each ServiceItem if(sm.items[j].service != null) { emit("Service --------------: " + sm.items[j].service); emit("ServiceID ------------: " + sm.items[j].serviceID); Entry[] entries = sm.items[j].attributeSets; emit("Service attributes ---:"); 314 for(int k=0; k<entries.length; k++) { emit(entries[k]); } services.addElement(sm.items[j]); emit(""); } } } notifyAll(); catch(Exception e) { e.printStackTrace(); } } } public void discarded(DiscoveryEvent de) { } public synchronized Object getService() { while(services.size() == 0) { try { wait(); } catch (InterruptedException ex) { } } return ((ServiceItem)services.elementAt(0)).service; } // this is the method that we will expose to the SOAP client public String getService(String service) { ServiceTemplate template; try { // if no class name is provided match on all services if(service == null) { template = new ServiceTemplate(null, null, null); } else { // build a template to match on the given class name Class[] types = { Class.forName(service) } ; template = new ServiceTemplate(null, types, null); } // set the template this.template = template; // start discovery ldm = new LookupDiscoveryManager(groups,null,this); // wait for discovery to find services getService(); } catch (Exception e) { e.printStackTrace(); } // return the list return new String(buffer); } // build the list display private void emit(Object str) { buffer.append(str + System.getProperty("line.separator")); } } 315 The LookupProxy implements the DiscoveryListener interface. You should be familiar with the technique applied here. The only thing to note is the getService method. This is the method that your SOAP client invokes. Web Service Deployment To deploy this Java class as a Web service, compile it and make sure that it's in your Java Web server's class path (assuming that you already have Apache SOAP 2.x installed and configured). The following steps explain how to deploy the service and invoke it from SOAP. If you do not have Apache SOAP, you can get it from the www.apache.org Web site. The installation instructions and required software are available with the distribution. A number of jar files are required, and the necessary configuration is explained in the installation instructions. This example was tested using the Apache Tomcat Web server, which is also available from the Apache site. To deploy a Web service using Apache SOAP, you must create an Apache deployment descriptor like the one in Listing 13.6. Listing 13.6 The LookupDD.xml Deployment Descriptor <isd:service xmlns:isd="http://xml.apache.org/xml-soap/deployment" id="urn:jini-service-lookup"> <isd:provider type="java" scope="Application" methods="getService"> <isd:java class="chapter13.LookupProxy" static="false" /> </isd:provider> <isd:faultListener>org.apache.soap.server.DOMFaultListener </isd:faultListener> </isd:service> The Apache SOAP Service Manager uses the deployment descriptor to collect information about deployed services. A few things to note: • • • id is the URI of the Web service. methods are a list of the public methods you will expose to clients of the Web service. class is the name of the Java class to invoke and must be on the classpath of your servlet engine. The command file in Listing 13.7 can be used to deploy and activate the Web service. Listing 13.7 test.cmd Script File @echo off echo This test assumes a server URL of http://localhost:8081 /soap/servlet/rpcrouter echo Deploying the lookup service... java org.apache.soap.server.ServiceManagerClient http://localhost:8081 /soap/servlet/ rpcrouter deploy xml/LookupDD.xml echo . echo Verify that it's there java org.apache.soap.server.ServiceManagerClient http://localhost:8081 /soap/servlet/ rpcrouter list 316 echo . echo Running the lookup test to find the EventMailbox service java chapter13.SOAPClient http://localhost:8081/soap/servlet/rpcrouter net.jini.event.EventMailbox echo . echo Undeploy it now java org.apache.soap.server.ServiceManagerClient http://localhost:8081 /soap/servlet/ rpcrouter undeploy urn:jini-service-lookup echo . echo Verify that it's gone java org.apache.soap.server.ServiceManagerClient http://localhost:8081 /soap/servlet/ rpcrouter list The script uses the org.apache.soap.server.ServiceManagerClient to deploy, list, and undeploy the Web service. The SOAPClient class builds a SOAP-RPC and invokes the getService method. The results of the invocation are displayed on the console. Listing 13.8 provides an example of the SOAPClient class. Listing 13.8 The SOAPClient Class package chapter13; import import import import import java.io.*; java.net.*; java.util.*; org.apache.soap.*; org.apache.soap.rpc.*; public class SOAPClient { public static void main(String[] args) throws Exception { // has a SOAP url been entered if (args.length < 1) { System.err.println ("Usage: java " + SOAPClient.class.getName () + " SOAP-router-URL service"); System.exit (1); } // set encoding style String encodingStyleURI = Constants.NS_URI_SOAP_ENC; // set the SOAP router url URL url = new URL (args[0]); String service = null; // check if a specific interface has been requested if(args.length == 2) service = args[1]; // Build the RPC call. Call call = new Call (); 317 // note the target URI was set in the deployment descriptor call.setTargetObjectURI ("urn:jini-service-lookup"); // the method that was defined in the deployment descriptor call.setMethodName ("getService"); // we are not doing any special encoding, // for instance user defined object handling call.setEncodingStyleURI(encodingStyleURI); // set the parameters for the call Vector params = new Vector (); params.addElement (new Parameter("service", String.class, service, null)); call.setParams (params); // invoke the url and get the response Response resp = call.invoke (url, "" ); // Check the response. if (resp.generatedFault ()) { Fault fault = resp.getFault (); System.out.println ("The call failed: "); System.out.println (" Fault Code = " + fault.getFaultCode ()); System.out.println (" Fault String = " + fault.getFaultString ()); } else { // no error display the result to the console Parameter result = resp.getReturnValue (); System.out.println (result.getValue ()); } } } When you run the test.cmd script, you should see a list of services displayed to the console that are registered in lookup services within the multicast radius. Summary—The Road to (Dis)-Integration As this chapter highlights, Web services are trying to bridge the diverse technologies and implementations that exist in systems, networks, and applications today. The Simple Object Access Protocol (SOAP) is used to enable interoperability between vendor implementations by defining a common transport protocol. WSDL is an XML language specification for describing Web services as a set of network addresses and operations. UDDI provides a method for publishing and finding service descriptions. The Electronic Business XML (ebXML) initiative is also trying to standardize XML business specifications. 318 The intent is to provide new levels of integration capabilities that are primarily Internet focused but need not be. This has proven to be a formidable challenge in the past, to hardware and software providers. Just take an audit of the integration technologies that exist in your current environment. If you're a large organization, this can resemble a graveyard for technology dinosaurs. An important question needs to be answered. What integration technologies will you eliminate by embracing Web services? In other words, will Web services simplify your environment or will they add layers of complexity? Successful systems in the future should be built from a more homogenous structure. The key will not be in the capability to support many different implementations. This is a combinatorial problem that can quickly become complex and dilute the focus of development efforts. The key will be in supporting "smarter" systems. Jini defines an environment that supports smarter systems. Jini also provides flexibility in balancing diversity with the acceleration of information access and the devices enabling pervasive computing—the new network reality. 319 Chapter 14. Pervasive Computing and Mobile Devices IN THIS CHAPTER • • • • • Jini Environment and Assumptions Can Jini Scale Down? Will Devices Scale Up? The Network Reality—Pervasive Computing Summary Jini arrived on the technology scene with great momentum and excitement. Jini visionaries spoke of a world where non-PC devices—PDAs, cell phones, life-enhancing appliances, and so on— would proliferate across a global network. Interconnected devices would enable a new generation of communication devices, information exchange, and pervasive computing. Now this is occurring. The market for network-connected devices continues to grow. Whether these devices are new, or just more powerful versions of existing products, all are becoming increasingly interconnected across a network. Like many expressions of future reality, the path to manifestation can be unpredictable. For instance, when Java and Java Applets were introduced, the desktop machine became the first logical target for deployment. However, Java quickly migrated to the middle-tier of application architectures. This was unexpected by many because of performance and scalability concerns with Java. What was not immediately apparent was the interoperability, operating system independence, and hardware agnosticism that Java could provide as a middleware, or "glue," technology. As processing power increased and software optimization continued, Java technology became the cornerstone of a new breed of middleware—the application server. We are now witnessing the peer-to-peer revolution that might lead Java back to the desktop. Peer-to-peer offers a compelling model for the Java language. Its capability to run anywhere, coupled with protocols that define peers of equals, provides unlimited potential for interoperability. So now, as we witness the excitement over Web services, peer-to-peer computing, and envision the Semantic Web, we are confronted with the future role of Jini. As I stated at the start of this book, Jini has a role. It continues to evolve and adapt as a networkcentric solution supporting the new network reality of pervasive computing. This final chapter, in some respects, presents where it all began for Jini. But now the maturity of the technology, coupled with improved device processing capabilities, will play an important role in changing middleware perception. Jini provides a new strand of intelligent middleware that blends devices and applications into the fabric of the network. Figure 14.1. Projections for Java have not always been on target, but have somehow surpassed expectations. 320 Jini Environment and Assumptions As previously illustrated, in order for a hardware or software component to join a Jini network, it must satisfy several critical requirements: • • • It must be able to participate in the Jini discovery and join protocols. It must be able to download and execute classes written in the Java programming language. It may need the capability to export classes written in the Java programming language. The Jini environment assumes that these requirements can be met by all participating members. It assumes objects or proxies for services can move and be downloaded by clients across the network. It assumes the client is capable of downloading the necessary code in jar files. Even though the overhead might not be significant in the client-PC market, the device market has proven to be a significant challenge. Resource-constrained devices, that is, devices that have limited memory and processing capacity, have challenged these Jini assumptions. Devices that are not capable of supporting the Java 2 Standard Edition (J2SE) have been all but forgotten by many commercial Jini efforts. Figure 14.2. Jini assumes a client can download needed code. 321 Devices that are not connected directly to the network, unable to run a JVM, or are limited in RAM, are actually representative of the current state of most devices. Yet, these devices can provide important services, especially in support of work mobility. So if Jini is to play a role in the new network reality, how will Jini adapt to the device market? Can Jini Scale Down? The challenges faced in this space include size (minimizing the JVM footprint), performance (low battery power, limited-memory devices), developing drivers to provide functionality that falls outside of the JVM sandbox, and the lack of useful tools relevant to embedded Java development. Sun has known for some time that different development platforms are needed—one size does not fit all. Today we have three distinct Java development environments directed towards different processing requirements. • • • The Java 2 Enterprise Edition (J2EE) represents the high-end development environment required to support scalable solutions in business-to-business (B2B) and electronic commerce. The Java 2 Standard Edition (J2SE) provides the middle ground solution to capture client development directed at the traditional PC and workgroup environments. The Java 2 Micro Edition (J2ME) now represents the attack on the device market, specifically aimed at resource-constrained devices. Figure 14.3. Sun has defined development environments to match the capabilities of market segments. 322 Java 2 Micro Edition (J2ME) J2ME is a more recent Java application environment. It is a framework for the deployment and use of Java technology in the post-PC world. J2ME software is configured for a variety of market segments. The J2ME architecture is designed to be modular, using two fundamental elements: configurations and profiles. Together, they deliver a specification for consumer electronics and embedded device manufacturers to implement on their products. Configurations and Profiles A configuration is a virtual machine and minimal set of basic class libraries and APIs. It specifies a generalized runtime environment for consumer electronic and embedded devices and acts as the Java platform on the device. A profile is an industry-defined specification of the Java APIs used by manufacturers and developers to address specific types of devices. A profile is built on top of and utilizes the underlying configuration necessary to provide a complete runtime environment for a specific kind of device. Profiles specify both the APIs and the configuration. An application written to the specification can execute in the specified Java technology environment without the addition of other Java classes. Figure 14.4. A configuration and profile provide the building blocks to J2ME. 323 Two configurations make up current J2ME technology: • • Connected Limited Device Configuration (CLDC) Connected Device Configuration (CDC) Connected Limited Device Configuration (CLDC) The Connected Limited Device Configuration (CLDC) was developed for devices with a small amount of memory. It is used in small, resource-constrained devices, each with memory in the range of 160–512KB. This provides the minimal functionality needed to minimize the memory footprint for memory-constrained devices. These devices usually contain 16- or 32-bit processors and a minimum total memory footprint of 128KB. The Mobile Information Device Profile (MIDP) is used with CLDC. It is targeted at devices such as cell phones. The CLDC is composed of the K virtual machine (KVM) and basic class libraries that can be used on a variety of devices. The KVM is a portable virtual machine designed for small memory, limited-resource, network-connected devices. Figure 14.5. CLDC and MIDP targeting the "weak" device market. 324 Connected Device Configuration (CDC) The Connected Device Configuration (CDC) was developed for devices with relatively large amounts of memory: two megabytes or more of total memory available for the Java platform. These devices typically require the full feature/functionality of the Java virtual machine. Target devices for CDC contain connectivity to a network—often a wireless, intermittent connection, with limited bandwidth (9600bps or less). Some devices that might be supported by CDC include residential gateways, next-generation smart phones, two-way pagers, PDAs, home appliances, point-of-sale terminals, and automobile navigation systems. Currently, CDC has one profile called the Foundation Profile. The CDC is a superset of CLDC, and therefore a CLDC-compliant profile is upwardly compatible with CDC. The J2ME architecture is changing rapidly. To find the latest information you may want to visit the Sun Microsystems site at http://java.sun.com/j2me/. Documentation and API specifications for the CLDC and KVM releases can be downloaded from the site at http://www.sun.com/software/communitysource/j2me/cldc/. The CDC Class Library includes the following: • • • • • • java.lang— VM system classes java.util— Underlying Java utilities java.net— UDP Datagram and File I/O java.io— Java File I/O java.text— Bare minimal support for I18n java.security— Minimal security and encryption for object serialization The Foundation Profile The Foundation Profile is a set of Java APIs that, together with the CDC, provide a complete J2ME application runtime environment targeted at consumer electronics and embedded devices. The C Virtual Machine (CVM) in the CDC is the engine for the Foundation Profile libraries. The CVM is a full-featured virtual machine designed for devices needing the functionality of the Java 2 virtual machine feature set, but with a smaller footprint. 325 In general, the Foundation Profile contains the complete basic Java packages from J2SE technology, except that the GUI dependencies on java.awt are removed. The CVM supports version 1.3 of the Java 2 Platform, and includes security, weak references, JNI, RMI, and JVMDI. The basic Java packages from CDC are modified in the Foundation Profile by the following: • java.lang— Rounds out full java.lang.* J2SE package support for the Java language. • • • java.util— Adds full zip support and other J2SE utilities. java.net— Adds TCP/IP Socket and HTTP connections. java.io— Rounds out full java.io.* J2SE package support for the Java language • • java.text— Rounds out full java.text.* J2SE package support for I18n. java.security— Adds Code Signing and Certificates. input/output. Figure 14.6. CDC and Foundation Profile targeting the "strong" device market. Documentation and API specifications for the following releases can be downloaded from the Sun Web site at java.sun.com/products. • • • The CDC release: java.sun.com/products/cdc The CVM release: java.sun.com/products/cdc/cvm The Foundation Profile release: java.sun.com/products/foundation With the release of J2ME+CDC+Foundation Profile and the J2ME RMI profile, full-featured Jini device support is possible. Figure 14.7. J2ME/CDC provides a smaller footprint, yet retains the necessary features to support Jini clients. 326 Does this mean devices unable to meet the requirements of the CDC and KVM are eliminated from a Jini network? What about even smaller devices and support for CDLC? The Jini community is betting on the Jini Surrogate Architecture. The Jini Surrogate Architecture The Surrogate Architecture defined by the Jini Community Process is also addressing resourceconstrained devices in a Jini network. It is providing the necessary classes to support very small devices through the use of surrogate architecture. The concept is actually quite simple, and fits in well with the Jini model of code movement and proxy delegation. The surrogate architecture lowers the requirements that a device must meet to participate in a Jini network. If a device has computational, resource, or network connectivity limitations, a surrogate is provided. The surrogate performs work on behalf of the device, rather than requiring every device to directly support code downloading and Jini capabilities. The surrogate architecture also relies on mobile objects, but the direction of the motion is reversed. Instead of loading mobile objects into a device, the device ships a mobile object out and into a surrogate host. The mobile object moved out of the device runs within the surrogate host and communicates with the device, if necessary, through private protocols. Figure 14.8. The direction of code movement is what enables the Jini surrogate architecture. 327 For example, a Palm Pilot or cell phone could house a small piece of code that gets exported to a surrogate host. This code contains the necessary protocol and service information to communicate with the device. This may be a jar file on the device or a pointer on the network where the jar file can be found. How it does this initialization is dependent on the network protocol used to connect the device. For example, there is a specification for how this works in IP networks. The surrogate lives in a host capable of supporting the Jini environment. The surrogate appears to the Jini network as the actual device; for instance, the cell phone appears to have registered with the Jini network. However, the device does not actually participate in Jini discovery; rather it only needs a mechanism to find its surrogate, such as listening on a specific TCP/IP port. Whenever another service needs to interact with the device, it goes through the surrogate, which communicates back to the device using whatever method has been implemented. The Surrogate Host Surrogate hosts provide the environment for surrogates to participate in a Jini network, thereby providing device participation in a Jini network. Surrogate hosts manage the life cycle of surrogates by loading, starting, stopping, and disposing of surrogates as necessary. Figure 14.9. The surrogate host architecture enables very small devices to join a Jini network through a surrogate. The surrogate host has the necessary resources to support a JVM, RMI, and so on. The interconnect defines the protocol necessary for a device to communicate with the surrogate host. 328 The surrogate host is responsible for implementing the interconnect protocol for a specific interconnect. The first part of the interconnect protocol implements discovery. Discovery, in this context, is the protocol that is used by the device and surrogate host to find each other. The Surrogate Specification defines the following surrogate lifecycle states: • • • • • • Device discovery—When a surrogate host discovers the presence of a device (or a device discovers the presence of a surrogate host). Surrogate retrieval—The action of locating the compiled class files of the surrogate and loading them into the surrogate host. These class files are part of a jar file. The jar file can be located within the device itself and transferred to the surrogate host directly from the device, or it could be located elsewhere. Activation—The act of instantiating the surrogate object, written in the Java programming language, and then making the method call to begin the surrogate's execution. Surrogate hosts can provide threads for these surrogates or whole JVMs. The details of the execution environment are left to the surrogate host designer. However, surrogates are assured some amount of isolation from each other, and isolation from other elements of the surrogate host. Execution—The steady state of a running surrogate. After they are executing, surrogates can accomplish anything that other Java programs can do. They can, for example, access other Jini services or provide Jini services themselves. Deactivation—The act of stopping a surrogate. Disposal—The act of disposing of the objects associated with the deactivated surrogate. The Surrogate Interface The Surrogate interface defines the activate and deactivate methods used by the surrogate host to control the execution of the surrogate. This interface also provides the surrogate access to its execution environment. The surrogate class must implement the Surrogate interface. The class implementing the Surrogate interface must be a public class, and must have a public constructor that takes no parameters, so that a surrogate object can be created by Class.newInstance. Listing 14.1 The Surrogate Interface 329 package net.jini.surrogate; public interface Surrogate { void activate(HostContext hostContext, Object context) throws Exception; void deactivate(); } The GetCodebase interface provides a method that enables the surrogate to set its codebase. The GetCodebase interface is optionally implemented by the surrogate class. Implementing this interface indicates that the surrogate will specify its codebase. This may be used when you want the device to control the location of downloaded files. If the surrogate implements this interface, the surrogate host invokes the getCodebase method and uses the returned URLs to set the surrogate's codebase before the surrogate's activate method is invoked. This will override the default codebase that is set by the -Djava.rmi.server.codebase property. Listing 14.2 The GetCodebase Interface package net.jini.surrogate; public interface GetCodebase { java.net.URL[] getCodebase(HostContext hostContext, Object context) throws Exception; } The HostContext interface provides methods to access the execution environment provided by the surrogate host. An object that implements HostContext is passed as a parameter to the surrogate's activate method and the getCodebase method of GetCodebase. Listing 14.3 The HostContext Interface package net.jini.surrogate; import net.jini.discovery.DiscoveryManagement; public interface HostContext { DiscoveryManagement getDiscoveryManager(); void cancelActivation(); SurrogateController newSurrogate(InputStream surrogate, Object context, DeactivationListener listener) throws SurrogateCreationException; } The Madison Implementation Sun Microsystems supplies a reference implementation of the surrogate host and an IP surrogate that conform to both the Surrogate Architecture Specification and the IP Interconnect Specification. They can be found on the www.jini.org site at http://developer.jini.org/exchange/projects/surrogate/. The Surrogate Specification is available at http://developer.jini.org/exchange/projects/surrogate/specs.html. The IP Project documentation is available at http://developer.jini.org/exchange/projects/surrogate/IP/index.html. 330 The Surrogate Architecture provides an environment to support small devices in a Jini network. Rather than requiring small devices to support the entire Jini environment, a surrogate is defined that represents the device in the Jini network. Thus, the bar to device entry, in terms of capabilities, has been lowered. Will Devices Scale Up? Lowering the processing requirements for devices to join a Jini network is only one facet of Jini device support. Arguably, devices will increase in processing capabilities regardless of the intended environment. In 1965, Gordon Moore predicted that the number of transistors per integrated circuit would double every 18 months. His forecast was that this trend would continue through 1975. What has become known as Moore's Law has been maintained for far longer, and still holds true as we enter the new century. Figure 14.10. Moore's Law has demonstrated that processing power grows at a predictable rate. It is important to put devices in a similar context. Of course much of the talk about "weak" devices has to do with processing, memory, and power. Yet it can be argued that Moore's Law also applies to the capabilities of devices. For example, the Compaq iPaq Pocket PC supports the Intel StrongArm SA1110 microprocessor. This processor already boasts 32MB of memory and 200+ MIPS at 1 watt. Savaje (www.savaje.com), has a Java-based OS that supports the full J2SE on a StrongArm iPaq. We can only imagine the capabilities of devices just two to three years out. A strong case can be made for devices scaling-up to meet Jini requirements. Even if this is not the case in the short term, the surrogate architecture provides another solution. 331 The Network Reality—Pervasive Computing The new network reality is that devices, computers, automobiles, and even appliances are forming a new "infosphere" that will interconnect us all (see Figure 14.11). Traditional network management and administration will not keep pace with this change. Complementary technologies such as Jini can build more intelligent services, reduce administration, and manage connectivity complexity. Figure 14.11. The new network reality—an "infosphere" of pervasive computing. Interconnected devices have already changed our communication and information exchange capabilities. The market for network-connected devices continues to grow. In fewer than five years we have witnessed the Internet connecting the world, cell phones connecting individuals, and devices connecting everything in between. Summary Interconnected devices are enabling a new generation of communication, information exchange, and pervasive computing. Current network and application solutions should not underestimate the interoperability impact. The proliferation of connected devices will further exacerbate the integration and interoperability dilemma. Solutions that boast best-of-breed integration capabilities will fail to keep pace with the rapidly changing device market. The Java language provides a platform for interoperability. The J2EE, J2SE, and now J2ME are Java-based. The segmentation of these environments is based on business and technology profiles. Jini provides a connection between these segments. Jini provides the common "thread." The interconnected nature of our networks, coupled with connected devices, presents unlimited potential. Harnessing this potential however, will require rethinking our basic assumptions about networks, systems, devices, and software. All this points to solutions that are adaptable. Jini is proving its adaptability. This book demonstrates Jini as a key enabler in P2P, Web service integration, and now the new device market. 332 As explained in Chapter 1, "Introduction to Jini," the tipping point is the culmination of social, economic, and technological changes that happen very quickly and sometimes from small events. There are changes happening in networks that are going to cause technologies—some new, some not so new—to soon hit the tipping point. Jini has the potential to be one of them. 333 Part IV: Appendixes IN THIS PART A Setting Up Your Environment B Tools C Links, Community, Resources 334 Appendix A. Setting Up Your Environment IN THIS APPENDIX • • What You Will Need Gotchas and Common Pitfalls This appendix will provide you with the information required to fully exercise the technology and examples presented throughout the book. This includes references to the necessary software, operating systems, hardware, and network technologies supported. In addition, common "gotchas" and pitfalls will be highlighted. What You Will Need Jini is written entirely in Java. It uses many of the features that were introduced in the Java 2 SDK, Standard Edition v 1.2 release, such as RMI remote object activation. At a minimum you will need the JDK 1.2 release to run Jini, although I highly recommend the latest release. As of this writing, that is the JDK 1.3 release or JDK 1.4 beta, and they are available at either http://java.sun.com/j2se/1.3/ or http://java.sun.com/j2se/1.4/. The examples in this book were tested using the JRE 1.3.1-b24. Downloading Jini You can download the Jini binaries and source from the Java Developer Connection at http://developer.java.sun.com/developer/products/jini. You must register to be a member of the Java Developer Connection. These downloads are available after accepting the Sun Community Source License Agreement. The Jini Technology Starter Kit v 1.1 was released 10/2000 and is the current release as of this writing. The starter kit contains the Jini technology infrastructure implementations, as well as supporting helper classes and services, including JavaSpaces. There is also a Jini Technology Starter Kit v 1.2 Beta, released 9/2001 that is available. In addition, there is a Jini TCK (Technology Core Platform Compatibility Kit v 1.1B) available; however, the TCK is not covered in this book. The TCK consists of a set of tests that ensure that Jini technology-enabled services and clients are compatible with the Jini specification. The installation instructions are available at: http://developer.java.sun.com/developer/products/jini/installation.index.html You can download the Jini specifications from http://java.sun.com/products/jini/specs/. System Requirements Platforms on which the Jini Technology Starter Kit has been tested by Sun Microsystems include the following: 335 • • • Java 2 SDK, Standard Edition, v1.3.1 for Solaris SPARC(TM)/x86 on the latest FCS versions of Solaris 2.6, Solaris 2.7, and Solaris 8 on Sparc and Solaris 8 on X86 Java 2 SDK, Standard Edition, v1.3.1 for Microsoft Windows 95/98/ME/2000/NT 4.0 on Windows 2000 (2nd Edition) and NT 4.0 (SP6) Java 2 SDK, Standard Edition, v1.3.1-b24 for Linux (Intel x86) on RedHat 7.1 Gotchas and Common Pitfalls Jini requires an infrastructure and support environment properly configured and established to work properly. Often this process can be difficult to developers new to Jini, especially if you are also new to RMI and networking fundamentals. If you are new to RMI, you should familiarize yourself with RMI fundamentals (http://java.sun.com/products/jdk/rmi/) and ensure that your environment supports the simple examples presented in Chapter 3. In addition, the "Remote Object Activation" tutorial is at http://java.sun.com/j2se/1.3/docs/guide/rmi/activation.html and can assist you with learning RMI fundamentals. Network Configuration Jini requires support for unicast TCP and multicast UDP. The former is used by subsystems using Jini technology such as Java Remote Method Invocation (RMI). Both unicast TCP and multicast UDP are used during the Jini discovery process. Chapter 4 contains a sample multicast sniffer that can be used to display multicast packets being transmitted. Firewalls present a major challenge to getting RMI, and therefore Jini functioning properly. To address this issue, RMI uses two popular techniques to bypass common filtering: direct and indirect forwarding using HTTP tunnels that encapsulate RMI and the Java Remote Method Invocation Protocol (JRMP) in HTTP messages. If you have a firewall, you will need to determine the technique used to filter messages (for example, port assignments), and then determine if tunneling HTTP can provide a solution for your environment. IP Address A valid IP address is required to configure and enable Jini services. Invalid IP addresses are often the result of a misconfigured host machine or lack of network support on your host machine. Verify that the IP address is correct for your local machine. You can start with a simple ping 127.0.0.1 command to ping your localhost. Then try to ping your IP address and verify return packets. Finally, try to ping the computer host name. If you have a problem with any of the network packets not being replied to, then check the active routes in your network table. For instance, type in the command netstat -nr. This should show you the IP addresses that are set on your local machine. Infrastructure In this section the infrastructure is referred to as: • • The HTTP server supporting downloadable code The Remote Method Invocation Daemon (rmid) supporting activatable services The HTTP Server 336 Jini requires a mechanism to move or download code across a network; for example, an HTTP server. This mechanism does not have to be provided by the host itself, but the code must be made available by some host on the network. There is a simple HTTP server that is provided with the Jini distribution. The following list some potential problems: • • • • Codebase—Probably one of the most frequently encountered problems in Jini has to deal with setting and managing the codebase that clients and services use to download code supplied by HTTP servers. The "class not found" problem often manifests itself when changing environments because of an improper codebase setting. As mentioned earlier, Jini requires that code be mobile. This involves setting a codebase property to point to an HTTP server where downloaded code can be found. The code that is available for download is annotated with a reference to its location, based on the codebase parameter. localhost is undoubtedly NOT the correct location when you begin to expose your clients and services on the network. Avoid the habit of using localhost in any of the parameters required to start clients and services that provide downloaded code. Port Conflict Error—The provided HTTP server will default to port 8080. If you receive a port conflict error message, then check to ensure another server is not running on this port. Unable to access jar files—The directory path to serve jar and class files is invalid. Check -dir parameter and ensure that the jar and class files that you wish to provide to clients are under that directory. Service Starter not found—Can't find service launcher. The service launcher program com.sun.jini.example.launcher.StartService is in the jiniexamples.jar file. Check to ensure your classpath is set to find the StartService.class. Remote Method Invocation Daemon (rmid) The RMI daemon is crucial to deploying Jini. The RMI daemon typically runs in the background. By default, when the daemon starts, it creates log files in a log directory under the directory where rmid was started. These log files are where state information is saved for restarts. Under normal circumstances, the log files are closed when rmid shuts down. If you encounter a problem with service activation, you may have to manually remove the log files and delete the log directory. If you encounter an activation exception, such as "logdir c:\log_dir\tmp\reggie_log exists, specify a different path," then remove the directory and try to start the activation daemon clean. This is only to troubleshoot a problem when a service may not have terminated properly. You need to stop rmid to shut down the daemon properly. To shut down rmid, simply enter the command rmid -stop. If you receive "Activation.main: an exception occurred: Port already in use: 1098," you need to kill the rmid process and then restart the daemon. Security Manager and Policy File You must set a security policy when running rmid. The problems most frequently encountered have to do with not setting the security policy, or setting a security policy that is too restrictive. In addition, all services and clients that are running in a Jini environment must have set a security manager and defined an appropriate security policy file. Summary 337 There are a number of resources available that can assist you with troubleshooting your Jini environment. You should read the Jini Frequently Asked Questions at http://www.sun.com/jini/faqs. Also, the Jini mailing-list archive at http://archives.java.sun.com/archives/jini-users.html contains a large amount of information regarding problems you might encounter. The archives allow you to search for a specific topic or keyword. Search the archives using error messages, as search parameters often return a list of discussion items applicable to your problem. Additionally, Appendix C of this book contains a reference to popular Jini sites and resources. 338 Appendix B. Tools IN THIS APPENDIX • • • Start-up Scripts Building Jini Service Browsing and Administration There are a number of tools that you will find beneficial when learning Jini and JavaSpaces. This is a rapidly changing area, so you may want to consult the Jini links referenced in Appendix C, especially www.jini.org, to find the latest updates and releases. Start-up Scripts The source code and scripts explained in this book can be found at: http://www.jworkplace.org You can download the zip file of examples from that site. Included in the jinijavaspaces.zip file are a number of service start-up scripts. The examples and scripts can be found in the JiniJavaSpaces directory. Unzip the file to install and review the examples presented: >example-installdir/JiniJavaSpaces/services/scripts The setup.cmd or setup.sh script should be modified to support your environment. The JINI_HOME environment variable should point to your Jini distribution: set JINI_HOME=d:\files\jini1_2 The HTTP_ADDRESS environment variable should point to your HTTP server hostname and port assignment: set HTTP_ADDRESS=hostname:8080 The HTTP_HOME environment variable should point to your HTTP directory path: set HTTP_HOME=%JWORK_HOME%\services\lib The JWORK_HOME environment variable should point to the book examples install directory: set JWORK_HOME=d:\JiniJavaSpaces Jini also comes with a GUI-supplied service activation program. It allows you to enter the required and optional parameters in a tabbed pane to start Jini services as an alternative to scripts. The following command starts the service launcher interface. From the command prompt, type in the command: 339 java -cp %JINI_HOME%\lib\jini-examples.jar com.sun.jini.example.launcher. StartService To use the launcher, you can open and edit the properties file located under the /jini1_1/examples/launcher directory from the file menu. This will generate the tabs shown and allow you to customize the parameters to your environment. There is a properties file for both Windows and Unix that supplies a list of default values. After you load the appropriate properties file, your display should look similar to Figure B.1. Figure B.1. The StartService Editor provides a graphical alternative to starting and stopping services in the Jini community. Building Jini The build tool provides a graphical alternative to scripts and make files. You can compile and create the necessary jar and rmic stub files using the build tool. The tool can be found at: http://developer.jini.org/exchange/projects/outofbox/buildtool/ Once you have downloaded the build tool distribution, start the graphical tool by invoking java -cp <jdk-installdir>/lib/tools.jar:<installdir>: </buildtool1_0/lib/buildtool.jar com.sun.jini.tool.build.BuildTool -gui You will see a display similar to Figure B.2. 340 Figure B.2. The build tool is being developed to provide a graphical environment for building Jini and user-defined clients and services. Service Browsing and Administration The startBrowser.cmd and startBrowser.sh files for Windows and Unix are used to start the lookup browser. @echo off @call setup.cmd set GROUPS=public echo Jini and JavaSpaces Application Development echo ------------------------------------------echo Jini install directory %JINI_HOME% echo Examples install directory %JWORK_HOME% echo Web server %HTTP_ADDRESS% echo Default group %GROUPS% echo ------------------------------------------echo Starting the Service Browser java -cp %JINI_HOME%\lib\jini-examples.jar -Djava.security.policy=%JINI_HOME%\example\browser\policy -Djava.rmi.server.codebase=http://%HTTP_ADDRESS%/jini-examples-dl.jar com.sun.jini.example.browser.Browser –admin The lookup browser can be used to view your LUS environment. You can display the services that are currently registered at each LUS that is active (see Figure B.3). 341 Figure B.3. The lookup browser is used to display active services in your Jini community. Administration of services is possible by highlighting the service and right-clicking on the service item as shown in the figure. Selecting "Service Admin" displays the administration panel, as in Figure B.4. You can change groups, locators, and remove the service from the LUS and rmid. Figure B.4. The ServiceItem Editor assists in the administration of services in your Jini community. 342 Summary Jini currently lacks a robust set of development and deployment tools. The tools developed are primarily from the Jini community and not from tool vendors. Whether this has slowed the adoption rate of Jini, or the slow rate of adoption has limited tool development is a catch-22. Nonetheless, this is an area that requires attention to improve Jini development productivity. The Jini community site at www.jini.org/projects.html contains projects dedicated to improving the Jini development environment. The Out of Box Project is focused on improving the Jini installation and deployment experience. Information on the Out of Box project is available at the following sites: http://developer.jini.org/exchange/projects/outofbox/ http://developer.jini.org/exchange/projects/outofbox/tools/index.html http://developer.jini.org/exchange/projects/outofbox/buildtool/buildtool.html The RIO Project has been working on generic development tools. Information on the RIO project is available at: http://developer.jini.org/exchange/projects/rio/ The Jini distribution includes a utility class used to inspect a set of classes and determine class dependencies. You can invoke the ClassDep utility from the command line. See the ClassDep.java file for documentation. java -classpath d:\files\jini1_1\lib\tools.jar;d:\jdk1.3\lib\tools.jar com.sun. jini.tool.ClassDep 343 Appendix C. Links, Community, Resources IN THIS APPENDIX • • • • Links Specifications Community Resources Links The Jini distribution and supporting software can be found at the following Web site: http://developer.java.sun.com/developer/products/jini The JDK releases for Jini are available at this Web site: http://java.sun.com/j2se/ The Remote Method Invocation (RMI) can be found at: http://java.sun.com/products/jdk/rmi/ Specifications All the Jini specifications are contained on the Sun Microsystems site at http://www.sun.com/jini/specs/. The specifications include the following: • • • • • • • • • • • Jini Technology Core Platform Specification Jini Discovery Utilities Specification Jini Entry Utilities Specification Jini Lease Utilities Specification Jini Join Utilities Specification Jini Service Discovery Utilities Specification Jini Lookup Discovery Service Jini Lookup Attribute Schema Specification Jini Lease Renewal Service Specification Jini Event Mailbox Service Specification JavaSpaces Service Specification The Remote Method Invocation Specification can be found at http://java.sun.com/products/jdk/1.2/docs/guide/rmi/spec/rmiTOC.doc.html. Community 344 The Jini community central site with access to all Jini projects registered by community members can be found at the following address: http://www.jini.org Jini participation in project discussion, development, and sharing can be found at: http://www.jini.org/participate.html Resources The Jini community has produced many Web-accessible resources to assist in your Jini development and experience. You might want to start here: http://www.jini.org/resources/ The Jini FAQ site: http://www.sun.com/jini/faqs 4The Jini mailing list archives: http://archives.java.sun.com/archives/jini-users.html The JavaSpaces mailing list archives: http://archives.java.sun.com/archives/jini-users.html Another resource you might find valuable is a popular site for articles and resources pertaining to Jini, maintained by Bill Venners of Artima.com: http://www.artima.com/jini/ Finally, all of the examples and updated errata from this book can be found at: http://www.jworkplace.org 345