I've chosen the package com.packtpub.felix.bookshelf.service.tui
for the proxy and com.packtpub.felix.bookshelf.service.tui.activator
for the bundle activator.
The BookshelfServiceProxy
is the main class for the bookshelf command functionality. For easy reference, we will define the SCOPE
and FUNCTIONS
constants that define the commands scope ("book") and the functions that are to be exposed. Currently, we will expose one function for the add
command:
public class BookshelfServiceProxy { public static final String SCOPE = "book"; public static final String[] FUNCTIONS = new String[] { "search" }; private BundleContext context; public BookshelfServiceProxy(BundleContext context) { this.context = context; }
The proxy constructor also takes a BundleContext
, which will be needed to look up the BookshelfService
when executing the command operations.
The search
commands that are exposed have two possible syntax signatures:
Each one of these signatures matches a method in the proxy class:
Set<Book> search(String username, String password, String attribute, String filter)
Set<Book> search(String username, String password, String attribute, int lower, int upper)
The @Descriptor
annotation provides additional information on the method and its parameters. Here, for example, we provide some help on the search command and include a hint on each parameter it takes:
@Descriptor("Search books by author, title, or category") public Set<Book> search( @Descriptor("username") String username, @Descriptor("password") String password, @Descriptor( "search on attribute: author, title, or category") String attribute, @Descriptor( "match like (use % at the beginning or end of <like>"+ " for wild-card)") String filter) throws InvalidCredentialsException { BookshelfService service = lookupService(); String sessionId = service.login( username, password.toCharArray()); Set<String> results; if ("title".equals(attribute)) { results = service.searchBooksByTitle(sessionId, filter); } else if ("author".equals(attribute)) { results = service.searchBooksByAuthor(sessionId, filter); } else if ("category".equals(attribute)) { results = service.searchBooksByCategory(sessionId, filter); } else { throw new RuntimeException( "Invalid attribute, expecting one of { 'title', "+ "'author', 'category' } got '"+attribute+"'"); } return getBooks(sessionId, service, results); }
The remainder of the method is pretty straightforward, the attribute
is checked against the valid values and the appropriate search is triggered.
Since the "rating"-based search is supposed to be directed to the method with another signature, we ensure that this method was not selected by mistake (for example, when upper
is not passed or when it cannot be made into an int)
.
The lookupService()
method uses the stored BundleContext
to look up the bookshelf service and return it. It throws a RuntimeException
if it doesn't find one:
protected BookshelfService lookupService() { ServiceReference reference = context.getServiceReference( BookshelfService.class.getName()); if (reference == null) { throw new RuntimeException( "BookshelfService not registered, cannot invoke "+ "operation"); } BookshelfService service = (BookshelfService) this.context.getService(reference); if (service == null) { throw new RuntimeException( "BookshelfService not registered, cannot invoke "+ "operation"); } return service; }
Notice the paired checks of the service reference and the service for null. As we saw earlier, when we stopped the inventory implementation before starting the bookshelf service, the environment can change at any time, such as services are stopped, upgraded, and so on while others are running. This is one of the powers of this service platform, but is also an added responsibility on the developer.
The getBooks()
method is defined next. It takes a set of ISBNs and returns the corresponding set of Book
entries:
private Set<Book> getBooks( String sessionId, BookshelfService service, Set<String> results) { Set<Book> books = new HashSet<Book>(); for (String isbn : results) { Book book; try { book = service.getBook(sessionId, isbn); books.add(book); } catch (BookNotFoundException e) { System.err.println("ISBN " + isbn + " referenced but not found"); } } return books; }
The second search signature is dedicated to rating-based search. It takes two ints
, instead of a String filter, for lower and upper bounds of the rating:
@Descriptor("Search books by rating") public Set<Book> search( @Descriptor("username") String username, @Descriptor("password") String password, @Descriptor("search on attribute: rating") String attribute, @Descriptor("lower rating limit (inclusive)") int lower, @Descriptor("upper rating limit (inclusive)") int upper) throws InvalidCredentialsException { if (!"rating".equals(attribute)) { throw new RuntimeException( "Invalid attribute, expecting 'rating' got '"+ attribute+"'"); } BookshelfService service = lookupService(); String sessionId = service.login(username, password.toCharArray()); Set<String> results = service.searchBooksByRating(sessionId, lower, upper); return getBooks(sessionId, service, results); }
Depending on the number and type of parameters passed to the command on the shell, Gogo will attempt to find (coerce) a best matching method signature for the command request.
The shell can recognize the basic types and convert them for use as parameters when calling the command function. However, for more complex types, it would require the assistance of a helper class.
The Converter (org.apache.felix.service.command.Converter
) is a service that knows how to convert a String to an object of a specific type and vice-versa.
Without going into too much detail, the converter is registered as a service, along with a property (osgi.converter.classes) that lists the classes it supports conversion for. The service exposes the following two methods:
convert(...)
that takes the target class (the desired type) and an input object and is expected to return the converted objectformat(...)
that takes an object to format, a formatting directive, and a Converter for delegation of the formatting of sub-partsThe converters are ordered by service.ranking
and attempted until one successfully converts or formats the content.
Let's go back to our case study: What's left is the activator to register the service and its commands with the framework and the Gogo Runtime.