Download presentation
Presentation is loading. Please wait.
1
Introduction - What is Jini Technology?
Set of Java APIs & distributed network protocols built on top of RMI – object hierarchies…. Distributed Programming Model built on top of RMI and organised as federation of services (JVMs) Services – hardware devices, software, etc. New network capabilies – SN, AC, SOD Jini is a a set of protocols built on top of RMI, which allow objects (objects hierarchies) to be transported across the network. It is a set of APIs and network protocols that make it possible to build and deploy distributed systems that are organized as federations of services. A service can be anything that sits on the network and is ready to perform a useful function. Hardware devices, software, communications channels – even human users themselves – can be services. A Jini-enabled disk drive, for example, could offer a “storage” service. A Jini-enabled printer could offer a “printing” service. A federation of services, then, is a set of services, currently available on the network, that a client (meaning a program, service, or user) can bring together to help it accomplish some goal. The idea behind the word federation is that the Jini view of the network doesn't involve a central controlling authority. Because no one service is in charge, the set of all services available on the network form a federation – a group composed of equal peers. Jini's runtime infrastructure merely provides a way for clients and services to find each other (via a lookup service, which stores a directory of currently available services). After services locate each other, they are on their own. The client and its enlisted services perform their task independently of the Jini runtime infrastructure. If the Jini lookup service crashes, any distributed systems brought together via the lookup service before it crashed can continue their work.
2
Jini Example Djinn Digital camera service Image/Persistent storage
Printer service Client
3
How Jini Works Runtime Infrastructure –
lookup services, jini-service providers & jini clients -provides mechanisms for adding, using and removing services via Discovery, Join & Lookup Programming Model - Helps build a distributed system as a federation of services and consists of 3 parts: Leasing Transactions Distributed Event Model Jini technology's runtime infrastructure Jini defines a runtime infrastructure that resides on the network and provides mechanisms that enable you to add, remove, locate, and access services. The runtime infrastructure of the Jini technology resides in three places: in lookup services that sit on the network, in the Jini software-enabled devices themselves (service providers), and in the clients using the service. It enables services to register with lookup services through a process called Discovery and Join. The runtime infrastructure uses one network-level protocol, called Discovery, and two object-level protocols, called Join and Lookup. Discovery is the process by which a Jini technology-enabled device locates lookup services on the network and obtains references to them. Join is the process by which a device registers the services it offers with lookup services. Lookup services are the central organizing mechanism for Jini technology-based systems. When devices are plugged into the network, they register themselves with a lookup service and become part of a federation. When clients wish to locate a service to assist with some task, they consult a lookup service. Lookup services organize the services they contain into groups. A group is simply a set of registered services identified by a string. For example, the "Printers" group could be populated by the printing services offered by all the printers on the local network. If the "Printers" group is maintained by multiple lookup services, for example, and one of those lookup services goes off the network, clients will still be able to locate the "Printers“ group via a different lookup service. The Jini Technology Programming Model Building a reliable distributed system is difficult because the network is inherently unreliable: servers can crash, traffic can get clogged, wires can be cut. The Jini technology programming model offers a small set of APIs that can help you create reliable distributed systems. Much of the interaction between clients and services during the processes of discovery, join, and lookup is built around these APIs, so clients and services will use the Jini technology programming model during those processes at least. The Jini technology programming model consists of three parts: Leasing, Transactions, and Distributed Events. Leasing provides a way to manage the lifetimes of distributed objects that can't be managed by the usual rules of garbage collection. In a single address space, the garbage collector can free an object when there are no references to it. But a garbage collector doesn't know if there are any remote references to an object. A lease is a grant of guaranteed access to a remote resource, such as an object, for a specified period of time. It is a guarantee that during the period of the lease, the resource won't be garbage collected away. For example, if a client wishes to make use of an object in a service, the client can make a lease request to the service that includes a desired lease period. The service can, at its discretion, award a lease to the client. The service gets to decide the duration of the lease, presumably taking the requested time period into account, and communicates that duration back to the client. If the client does not renew the lease before the time period decided upon by the service elapses, the service can assume the object is no longer needed by the client and can discard the object. But, so long as the client keeps renewing the lease before it expires (and the service continues to allow the renewal), the service will not garbage collect the object and the object will remain available to the client. Transactions is another aspect of the Jini technology programming model that can help you build reliable distributed systems. The API that supports transactions enables operations that involve multiple clients and services to either succeed or fail as a unit. If some aspect of the operation managed by a transaction fails, for example, if one of the involved services disappears from the network, the participating parties can be instructed to "roll back" to a known good state. The Distributed Event Model is the third aspect of the programming model that facilitates the building of reliable distributed systems. This model extends the 1.1 JavaBeans/AWT/Swing event model, which works in a single address space, to distributed systems. Using the Jini technology event model, an object can register itself as a listener interested in events generated by a remote source. When the remote source fires an event, the event will travel across the network to the registered listeners.
4
Discovery Protocol Jini enabled device drops presence announcement
monitor port for presence announcement packets Send ServiceRegistrar object to client/service Client/service can then invoke methods on this object Discovery Protocol Discovery works like this: Imagine you have a Jini-enabled disk drive that offers a persistent storage service. As soon as you connect the drive to the network, it broadcasts a presence announcement by dropping a multicast packet onto a well-known port. Included in the presence announcement is an IP address and port number where the disk drive can be contacted by a lookup service. Lookup services monitor the well-known port for presence announcement packets. When a lookup service receives a presence announcement packet, it opens and inspects the packet. The packet contains information that enables the lookup service to determine whether or not it should contact the sender of the packet. If so, it contacts the sender directly by making a TCP connection to the IP address and port number extracted from the packet. Using RMI, the lookup service sends an object, called a ServiceRegistrar, across the network to the originator of the packet. The purpose of the ServiceRegistrar object is to facilitate further communication with the lookup service. By invoking methods on this object, the sender of the announcement packet can perform join and lookup on the lookup service. Thus, in the disk drive example, assume a lookup service that maintains a group named "Way Cool Storage Devices“ receives the disk drive's announcement packet. Because the announcement packet mentions the disk drive's ambition to become part of the "Way Cool Storage Devices" group, the lookup service will contact the originator of the announcement packet -- the disk drive -- directly at the specified IP address and port number. The lookup service would make a TCP connection to the disk drive and would send it a ServiceRegistrar object, through which the disk drive would then register its persistent storage service via the join process. The lookup service will send to the disk drive an object that implements an interface through which the disk drive can register itself, via the join process, as a member of the "Way Cool Storage Devices" group.
5
Fig 4 Registering a service with Jini Lookup Service
The Join Process A “service” connects to a Lookup Service via the ServiceRegistrar object received during Discovery A “service” sends information “ServiceItem” about itself through the ServiceRegistrar to the Lookup service The Lookup Service stores this information and associates the service with the group At this point, the service has joined the group on that Lookup Service The join process - Device registers its services with lookup services Once a device has discovered a lookup service, it can register its own services on that lookup service via the join process. The join process begins when a service connects to a lookup service via the ServiceRegistrar object it received from that lookup service during the discovery process. Through the ServiceRegistrar object, the service sends information about itself to the lookup service. To do a join, the service invokes the register( ) method on the ServiceRegistrar object it received from that lookup service during the discovery process, passing as a parameter an object called a ServiceItem, a bundle of objects that describe the service. The ServiceItem is a container for several objects, including an object called a service object, which clients can use to interact with the service. Some potential attributes are icons, classes that provide GUIs for the service, and objects that give more information about the service. The register( ) method sends a copy of the ServiceItem up to the lookup service, where the service item is stored. The lookup service stores the information uploaded from the service and associates that service with the requested group. Once this has completed, the service has finished the join process: its service has become registered in the lookup service. The information sent includes an instance of a class that implements a "service interface." It can also include other attributes, including applets that provide graphical user interfaces through which users can directly interact with the service. The service is identified by the type of the "service interface" uploaded to the lookup service via the join process. Each kind of service is associated with one such Java technology-based interface. The lookup service stores and locates a service based on the type of that interface; clients interact with the service by invoking methods on an object that implement that service interface. Thus, a storage service, for example, would upload during the join process an interface that enables clients to interact with the storage service. For example, a lookup service is a Jini service, and its service object is the service registrar. The register() method invoked by service providers during join is declared in the ServiceRegistrar interface (a member of the net.jini.core.lookup package), which all service registrar objects implement. Clients and service providers talk to the lookup service through the service registrar object by invoking methods declared in the ServiceRegistrar interface. Likewise, a disk drive would provide a service object that implemented some well-known storage service interface. Clients would look up and interact with the disk drive by this storage service interface. Joining the Lookup Service After locating the lookup service, the calculator service would need to join the lookup service. Join occurs when a service has located a lookup service and wishes to contribute itself. The device first locates the Jini lookup service under a specific group and then registers its service in it by loading its service object. A group identifies and/or organizes the set of services available under a category. For example, printer groups registers print services. The default group is public. However, the present Jini implementation has the drawback that it locates all lookup services pertaining to the specific service, not just public alone. If we request for a specific service, both specific and public lookup services will be found. Some important classes relevant in our context of join protocol are discussed below. For further information on Join protocol, please refer the Jini API documentation. net.jini.core.lookup.ServiceItem: This is an encapsulation for any service objects the device wants to provide. The device can provide N number of services, as it needs. Registration/Lookup is possible only in terms of a ServiceItem when it is via ServiceRegistrar. net.jini.core.entry.Entry: As far as jini is concerned, a device may register its service along with a set of objects, which would best describe the device characteristics. Those are called Device Attributes. Those attributes have to signal to the LookupService that they are characteristics of the registering device by implementing the net.jini.core.entry.Entry, interface. For example, a set of attributes may describe the type of the service, service location, service provider, service usage, etc. For simplicity Jini provides an abstract class known as net.jini.entry.AbstactEntry which in turn implements the Entry interface and provides some meaningful functions like equals(), toString() and so on. Just extend it and add up device specific attributes - fields. A small example for defining Attribute goes here. public class DiskAttr extends net.jini.lookup.AbstractEntry { // An integer value representing the disk capacity public Integer capacity = null; // A string representing the product brand name public String brandName = null; // A boolean representing the current state of // the disk locked/unlocked public Boolean status = null; } * Please take a careful look at the above code snippet As you can find all member fields in the above class are public. JINI stores your service objects along with any attribute fields using serialization. This implies that any attribute fields you want serialized will be declared as public for direct access - not static, final or transient. More over we can't have any primitive data types as fields of attributes, that's why the first and third fields in the previous example are wrapped around the equivalent Wrapper classes. net.jini.core.lookup.ServiceRegistration For any service registration with jini, the lookup service returns a ServiceRegistration object, which provides lot of useful facilities and information about our registration. A method to get the related Lease object, with which a service can do a lease renewal and lease cancellation easily. A method to get the allotted ServiceID, which uniquely identifies any service on the lookup service. This ID makes the maintainability easy for our service. More over this ID confirms that there are no redundant service in the lookup service, which is improves the jini system integrity. A set of methods to add/modify/replace with new bunch of attributes. net.jini.core.lookup.ServiceTemplate This is a template class with which we can frame up a jini service search criterion to find matching services available in the lookup services. The below diagram illustrates the various parts of a ServiceTemplate. Fig.3 Illustrating a ServiceItem object from it's internal components perspective There are primarily two ways of joining a lookup service. One is by using the JoinManager utility class in com.sun.jini.lookup package, which comes in handy. The other method is by enumerating services on the network explicitly and registering with them. Before we take a look at both, we need to note one more point about service implementation and uploading. There are various possible methods to make our service available in the jini lookup service. When you implement your service interface without RMI compliance, the server side service application has to be kept alive. So the client can't be sure about it. On the other hand, when you create your interface as a Remote interface your server side application creates a stub class for your implementation class via the RMIC tool and registers either with Rmiregistry or Activation daemon. After this, your server application can simply quit after registering a stub with RMI. In case you decide to register your stub with Rmiregistry, you will need to extend your implementation class with java.rmi.UnicastRemoteObject, otherwise extend your implementation class with java.rmi.activation.Activatable. For further references on this issue, please go through the RMI specification in your JDK1.2 docs. For simplicity, we make use of UnicastRemoteObject model in our examples. Joining through the JoinManager utility class JoinManager is a utility class, which discovers and keeps track of which lookup services to join, registers with them, keeps the registration leases renewed, and keeps the attributes up to date. There are a number of possible constructors for JoinManager. The simplest and first is: JoinManager(java.lang.Object obj, Entry[] attrSets, ServiceIDListener callback, LeaseRenewalManager leaseMgr) This specifies the service to be managed and its entry attributes. The callback will have its serviceIDNotify() method called when a new service is discovered. This argument can be null if the application has no interest in this. The leaseMgr can also be set to null and will then be created as needed later. Using this constructor will initiate a search for service locators belonging to the group "public'', which is defined by a group value of the empty string "". The applications will need to follow this up immediately with a call to search for locators belonging to any group. JoinManager joinMgr = new JoinManager(obj, null, null, null); joinMgr.setGroups(LookupDiscovery.ALL_GROUPS); The second constructor is: JoinManager (java.lang.Object serviceobject, Entry[] attrSets, java.lang.String[] groups, LookupLocator [] locators, ServiceIDListener callback, LeaseRenewalManager leaseMgr) The constructor includes multicast group names and lookup locators. This allows multicast search for locators belonging to certain groups, and also unicast lookup for known locators. A multicast only search for any groups would use JoinManager joinMgr = new JoinManager (serviceobject, null, LookupDiscovery.ALL_GROUPS, null, null, null); Where both additional parameters are set to null. JoinManager(net.jini.core.lookup.ServiceID ServiceID, java.lang.Object obj, net.jini.core.entry.Entry[] attrSets, java.lang.String[] groups, net.jini.core.discovery. LookupLocator[] locators, LeaseRenewalManager leaseMgr) This constructor differs from the previous one in specifying a ServiceID object. We can use this constructor when we already have registered a service and want to update the service. Selection of any one the JoinManager constructor depends on the situation and requirement. A closer look at the example Lets take a closer look at implementing our calculator service and registering its service on the network. The steps involved are a) define a Calculator interface b) Create the GUI Frame class for it c) Create the object that does the actual service d) Use the JoinManager for easy registration. Here is our Calculator interface code. Note that Calculator extends java.rmi.Remote interface in order to make it available to the clients via RMI protocol. an interface Calculator /** This is the contract interface, which is being provided by the CalculatorService class **/ import java.awt.Frame; import java.rmi.*; public interface Calculator extends Remote { public Frame getCalculator() throws RemoteException; } A Calculator Frame class, which is the calculator GUI - CalcFrame This CalcFrame is the service provided by the Calculator Service. Whenever any Client asks for a service of type Calculator, an instance of the Calculator frame is sent from the Lookup Service to the client application. An implementation of the Calculator interface as well as of the Jini lookup interaction is provided by the class CalculatorService The function getCalculator() is implemented as follows. public Frame getCalculator() throws RemoteException { Frame f = new CalcFrame(); f.setSize(300,150); return f; } The CalculatorService is registered with the lookup service as follows In the following code segment we make use the first JoinManager constructor we discussed above. CalculatorService calculatorService = new CalculatorService(); String hostName ="chittu.cswl.com"; Naming.rebind("rmi://"+hostName+"/CalculatorService", calculatorService); System.out.println("Bound with RMIRegistry"); Object serviceStub = Naming.lookup ("rmi://"+hostName+"/CalculatorService"); System.out.println("Trying to Join Manager"); // Join the manager so that it // will become a part of federation // JoinManager joinManager = new JoinManager(serviceStub, attributes,calculatorService, new LeaseRenewalManager()); After this step the CalculatorService object is available to all Jini clients on the network. In the later part of the tutorial we shall see how to gain access to this CalculatorService as a Jini client. Simply passing a LeaseRenewalManager object to the JoinManager constructor doesn't confirm the renewal of Lease for our service. We also need to make our application alive till we want the renewal. How can I take control over the LeaseRenewalManager? For sake of simplicity we took the null argument constructor for LeaseRenewalManager. There is one other form of the constructor that helps in various ways. LeaseRenewalManager ( net.jini.core.lease.Lease lease, long expiration_in_milli_seconds, LeaseListener listener) Here the first two parameters are self-explanatory except the LeaseListener parameter. LeaseListener is a listener object, which has a callback function notify() which is called with a LeaserenewalEvent when the related Lease expires or Lease is denied. We can create and keep alive a separate thread for the LeaseListener purpose and take some corrective measures when the Lease fails. In real time Jini programming this would be a very important consideration. Joining through the ServiceRegistrar's register() method Note: Here, we implement the same calculator service by the name AnotherCalculatorService, as the registration approach is different. The approach of the join method here is quite different from that used in the Join Manager case. In this case we explicitly enumerate lookup services on the network, create a ServiceItem that encapsulates our service object and a set of attributes, and register this with each lookup service we are interested in. Please refer to AnotherCalculatorService.java for coding details. Lookup services on the network can be discovered using the LookupServiceFind class as discussed in the discovery section. Simply call its getLocators() method to get return an array of LookupLocator objects, each representing a lookup service on the network. Fig 4 Registering a service with Jini Lookup Service LookupLocator[] lookupLocators = LookupServiceFind.getLocators(); // Attributes for the service is // initialized as follows. // You can use attributes like ServiceType, // ServiceInfo, Location etc. as desired Name name = new Name("AnotherCalculatorService"); // e.g. a Name attribute of the Service Entry [] attribs = new Entry[1]; attribs[0] = name; // make the Name attribute as part of // the attribute set Note: The attribute set facilitates Service query/Lookup simple and appropriate from a client point of view. // A Service item is an encapsulation // of the actual service object and a set // of attributes which describe the service. AnotherCalculatorService calculatorService = new AnotherCalculatorService(); String hostName ="chittu.cswl.com"; Naming.rebind ("rmi://"+hostName+"/AnotherCalculatorService", calculatorService); System.out.println("Bound with RMIRegistry"); Object serviceStub = Naming.lookup ("rmi://"+hostName+"/CalculatorService"); ServiceItem serviceItem = new ServiceItem(null, serviceStub,attribs); for(int i=0;i<lookupLocators.length;i++) { ServiceRegistrar registrar = lookupLocators[i].getRegistrar(); ServiceRegistration registration = registrar.register(serviceItem , Lease.ANY); Lease lease = registration.getLease(); System.out.println("The allotted lease time is :" + lease.getExpiration()+" milli secs"); System.out.println("The allotted Service ID is :" + registration.getServiceID()); } When you register a service with a lookup service, the registration will be available for a certain period only, known as the lease (a returned object from Jini lookup) period. It is a must to renew the lease before it expires. Otherwise the service will be garbage collected automatically. It is also possible to cancel a lease following which the service will be removed from the lookup service. Granting a lease and its duration is fully up to the Lease granter to decide upon. In the present implementation of Jini, when you start a lookup service using com.sun.jini.tck.reggie.CreateLookup class, the service object will be available for only 5 minutes. But this can be overridden as explained in the next session.
6
The Lookup Process Lookup Server
2. Perform Discovery & get back ServiceRegistrar object 3. Send ServiceTemplate using registra.lookup(template) Register printer service with Lookup Service 4. Lookup service performs query & sends back matching service objects as return values of lookup() The lookup process Once a service has joined at least one group in a particular lookup service, that service is available for use by clients who query that lookup service. To build a federation of services that will work together to perform some task, a client must locate and enlist the help of the individual services. To find a service, clients interact with lookup servers via a process called lookup. The lookup process begins when a client contacts a lookup service and requests services of a particular type. The type specified in this request is a Java technology-based interface that defines the way in which clients interact with the service being requested. This is the “Service Interface" that is uploaded from the service to the lookup service during the join process. To perform a lookup, a client invokes the lookup( ) method on a ServiceRegistrar object. (A client, like a service provider, gets a service registrar through the previously-described process of discovery.) The client passes as an argument to lookup( ) a ServiceTemplate, an object that serves as search criteria for the query. The ServiceTemplate can include a reference to an array of Class objects. These Class objects indicate to the lookup service the Java type (or types) of the service object desired by the client. The ServiceTemplate can also include a service ID, which uniquely identifies a service, and attributes, which must exactly match the attributes uploaded by the service provider in the ServiceItem. The ServiceTemplate can also contain wildcards for any of these fields. A wildcard in the service ID field, for example, will match any service ID. The lookup( ) method sends the service template to the lookup service, which performs the query and sends back zero to any matching service objects (e.g. objects that match the printer service). The client gets a reference to the matching service objects as the return value of the lookup( ) method. With this object reference(s), the client can interact directly with the service (e.g. printer service) simply by invoking methods on the object. In the general case, a client looks up a service by Java type, usually an interface. For example, if a client needed to use a printer, it would compose a ServiceTemplate that included a Class object for a well-known interface to printer services. All printer services would implement this well-known interface. The lookup service would return a service object (or objects) that implemented this interface. Attributes can be included in the service template to narrow the number of matches for such a type-based search. The client would use the printer service by invoking methods from the well-known printer service interface on the downloaded service proxy object. The SummerClient 1 import net.jini.entry.*; 2 import net.jini.core.lookup.*; 3 import net.jini.core.entry.*; 4 import net.jini.lookup.entry.*; 5 import net.jini.core.discovery.*; 6 import java.rmi.*; 7 import java.io.*; 8 9 class SummerClient 10 { public static void main (String[] args) { 12 13 try { System.setSecurityManager (new RMISecurityManager ()); 16 // Perform unicast lookup on localhost LookupLocator lookup = new LookupLocator("jini://localhost"); 19 // Get the service registrar object for the lookup service ServiceRegistrar registrar = lookup.getRegistrar(); 22 // Search the lookup server to find the service that has the // name attribute of "SummerService". Entry[] attributes = new Entry[1]; attributes[0] = new Name("SummerService"); ServiceTemplate template = new ServiceTemplate(null, null, attributes); 29 // lookup() returns the service object for a service that matches // the search criteria passed in as template // Here, although I searched by Name attribute, I'm assuming that // the object that comes back implements the Summer interface Summer summer = (Summer) registrar.lookup(template); 35 LineNumberReader stdinReader = new LineNumberReader( new BufferedReader(new InputStreamReader(System.in))); 38 for (;;) { 40 String userLine = stdinReader.readLine(); 42 if (userLine == null || userLine.length() == 0) { break; } 46 String outString; try { long sum = summer.sumString(userLine); outString = Long.toString(sum); } catch(InvalidLongException e) { outString = e.getMessage(); } System.out.println(outString); } } catch (Exception e) { System.out.println("client: MyClient.main() exception: " + e); e.printStackTrace(); } } 64 } $ java -Djava.security.policy=/sun/jini/bv/policy.all -Djava.rmi.server.codebase= SummerClient 1 1 2 2 2 4 3 3 3 9 The SummerClient2 import net.jini.entry.*; import net.jini.lookup.*; import net.jini.lookup.entry.*; import net.jini.discovery.*; import java.rmi.*; import java.io.*; class SummerClient2 { public static void main (String[] args) { try { System.setSecurityManager(new RMISecurityManager ()); // Perform unicast lookup on localhost LookupLocator lookup = new LookupLocator("jini://localhost"); // Get the service registrar object for the lookup service ServiceRegistrar registrar = lookup.getRegistrar(); // Search the lookup server to find a service that implements // the Summer interface. Class[] types = new Class[1]; types[0] = Summer.class; ServiceTemplate template = new ServiceTemplate(null, types, null); // lookup() returns the service object for a service that matches // the search criteria passed in as template // Here, because I searched by type, I'm certain that // the object that comes back implements the Summer interface. Summer summer = (Summer) registrar.lookup(template); LineNumberReader stdinReader = new LineNumberReader( new BufferedReader(new InputStreamReader(System.in))); for (;;) { String userLine = stdinReader.readLine(); if (userLine == null || userLine.length() == 0) { break; } String outString; long sum = summer.sumString(userLine); outString = Long.toString(sum); catch(InvalidLongException e) { outString = e.getMessage(); System.out.println(outString); catch (Exception e) System.out.println("client: MyClient.main() exception: " + e); e.printStackTrace(); -Djava.rmi.server.codebase= SummerClient2 3 -1 Printer Service 5. Client invokes methods on the service proxy object to interact directly with the printer 6. Printer prints document & notifies client via a remote event Client
7
Remote PDA (Palm V) Printing
Lookup Service with List of Service Items Register HelloWordService with Lookup Service Register Bank service with Lookup Service HelloWorldService Bank Service 2. Find Lookup Services Register Printer Service with Lookup Service 3. Download Printer Service proxy 4. Send document to printer using methods on the service object Print Proxy 5. Print document Printer service Client
8
Conclusion Set of protocols for building self-healing, robust distributed networks organised as federations of services Adds new capabilities to networking, e.g. spontaneous networking, services-on-demand & automatic configuration. Achieves this via its 5 main concepts of Discovery & Join, Lookup, Leasing, Remote Event Model and Transactions
Similar presentations
© 2024 SlidePlayer.com. Inc.
All rights reserved.