1 package com.indexdata.mkjsf.pazpar2;
\r
3 import java.io.Serializable;
\r
4 import java.lang.annotation.Retention;
\r
5 import java.lang.annotation.Target;
\r
6 import java.util.ArrayList;
\r
7 import java.util.Arrays;
\r
8 import java.util.HashMap;
\r
9 import java.util.List;
\r
10 import java.util.Map;
\r
11 import java.util.StringTokenizer;
\r
13 import javax.annotation.PostConstruct;
\r
14 import javax.enterprise.context.SessionScoped;
\r
15 import javax.enterprise.inject.Produces;
\r
16 import javax.faces.context.FacesContext;
\r
17 import javax.inject.Inject;
\r
18 import javax.inject.Named;
\r
19 import javax.inject.Qualifier;
\r
21 import org.apache.log4j.Logger;
\r
23 import com.indexdata.mkjsf.config.Configurable;
\r
24 import com.indexdata.mkjsf.config.Configuration;
\r
25 import com.indexdata.mkjsf.config.ConfigurationReader;
\r
26 import com.indexdata.mkjsf.controls.ResultsPager;
\r
27 import com.indexdata.mkjsf.errors.ConfigurationError;
\r
28 import com.indexdata.mkjsf.errors.ConfigurationException;
\r
29 import com.indexdata.mkjsf.errors.ErrorCentral;
\r
30 import com.indexdata.mkjsf.errors.ErrorHelper;
\r
31 import com.indexdata.mkjsf.errors.MissingConfigurationContextException;
\r
32 import com.indexdata.mkjsf.pazpar2.commands.Pazpar2Commands;
\r
33 import com.indexdata.mkjsf.pazpar2.data.RecordResponse;
\r
34 import com.indexdata.mkjsf.pazpar2.data.Responses;
\r
35 import com.indexdata.mkjsf.pazpar2.state.StateListener;
\r
36 import com.indexdata.mkjsf.pazpar2.state.StateManager;
\r
37 import com.indexdata.mkjsf.utils.Utils;
\r
40 * Pz2Service is the main controller of the search logic, used for selecting the service
\r
41 * type (which can be done by configuration and/or run-time), selecting which search client
\r
42 * to use, and performing high-level control of request cycles and state management.
\r
44 * Command and response beans are also obtained through Pz2Service - although it is
\r
45 * transparent to the UI that they are retrieved through this object.
\r
48 * Pz2Service is exposed to the UI as <code>pz2</code>. However, if the service is pre-configured,
\r
49 * the Faces pages might never need to reference <code>pz2</code> explicitly. Indirectly they will,
\r
50 * though, if the polling mechanism in the tag <code><pz2utils:pz2watch></code> is used.
\r
54 @Named("pz2") @SessionScoped
\r
55 public class Pz2Service implements StateListener, Configurable, Serializable {
\r
57 private static final String MODULE_NAME = "service";
\r
58 private static String SERVICE_TYPE_TBD = "TBD", SERVICE_TYPE_PZ2 = "PZ2", SERVICE_TYPE_SP = "SP";
\r
59 private static final List<String> serviceTypes =
\r
60 Arrays.asList(SERVICE_TYPE_PZ2,SERVICE_TYPE_SP,SERVICE_TYPE_TBD);
\r
61 private String serviceType = SERVICE_TYPE_TBD;
\r
62 private List<String> serviceProxyUrls = new ArrayList<String>();
\r
63 public static final String SERVICE_PROXY_URL_LIST = "SERVICE_PROXY_URL_LIST";
\r
64 private List<String> pazpar2Urls = new ArrayList<String>();
\r
65 public static final String PAZPAR2_URL_LIST = "PAZPAR2_URL_LIST";
\r
67 private static final long serialVersionUID = 3440277287081557861L;
\r
68 private static Logger logger = Logger.getLogger(Pz2Service.class);
\r
69 protected Pz2Client pz2Client = null;
\r
70 protected ServiceProxyClient spClient = null;
\r
71 protected SearchClient searchClient = null;
\r
73 @Inject ConfigurationReader configurator;
\r
75 private StateManager stateMgr = null;
\r
76 private Pazpar2Commands pzreq = null;
\r
77 private Responses pzresp = null;
\r
78 private ErrorCentral errors = null;
\r
80 protected ResultsPager pager = null;
\r
82 protected ErrorHelper errorHelper = null;
\r
84 public Pz2Service () {
\r
85 logger.info("Instantiating pz2 bean [" + Utils.objectId(this) + "]");
\r
88 public static Pz2Service get() {
\r
89 FacesContext context = FacesContext.getCurrentInstance();
\r
90 return (Pz2Service) context.getApplication().evaluateExpressionGet(context, "#{pz2}", Object.class);
\r
94 public void postConstruct() throws MissingConfigurationContextException {
\r
95 logger.info("Pz2Service PostConstruct of " + this);
\r
96 stateMgr = new StateManager();
\r
97 pzreq = new Pazpar2Commands();
\r
98 pzresp = new Responses();
\r
99 errors = new ErrorCentral();
\r
100 pzresp.setErrorHelper(errors.getHelper());
\r
102 logger.debug("Pz2Service PostConstruct: Configurator is " + configurator);
\r
103 logger.debug(Utils.objectId(this) + " will instantiate a Pz2Client next.");
\r
104 pz2Client = new Pz2Client();
\r
105 spClient = new ServiceProxyClient();
\r
106 stateMgr.addStateListener(this);
\r
108 configureClient(pz2Client,configurator);
\r
109 configureClient(spClient,configurator);
\r
110 this.configure(configurator);
\r
111 } catch (MissingConfigurationContextException mcc) {
\r
112 logger.info("No configuration context available at this point");
\r
113 logger.debug("Configuration invoked from a Servlet filter before application start?");
\r
115 } catch (ConfigurationException e) {
\r
116 logger.warn("There was a problem configuring the Pz2Service and/or clients (\"pz2\")");
\r
117 e.printStackTrace();
\r
122 @Target({java.lang.annotation.ElementType.TYPE,
\r
123 java.lang.annotation.ElementType.METHOD,
\r
124 java.lang.annotation.ElementType.PARAMETER,
\r
125 java.lang.annotation.ElementType.FIELD})
\r
126 @Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
\r
127 public @interface Preferred{}
\r
129 @Produces @Preferred @SessionScoped @Named("pzresp") public Responses getPzresp () {
\r
130 logger.trace("Producing pzresp");
\r
134 @Produces @Preferred @SessionScoped @Named("pzreq") public Pazpar2Commands getPzreq () {
\r
135 logger.trace("Producing pzreq");
\r
139 @Produces @Preferred @SessionScoped @Named("errors") public ErrorCentral getErrors() {
\r
140 logger.trace("Producing errors");
\r
144 @Produces @Preferred @SessionScoped @Named("stateMgr") public StateManager getStateMgr() {
\r
145 logger.trace("Producing stateMgr");
\r
150 * Configures the selected search client using the selected configuration reader.
\r
152 * The configuration reader is select deploy-time - by configuration in the application's beans.xml.
\r
154 * @param client search client to use
\r
155 * @param configReader the selected configuration mechanism
\r
156 * @throws MissingConfigurationContextException if this object is injected before there is a Faces context
\r
157 * for example in a Servlet filter.
\r
159 public void configureClient(SearchClient client, ConfigurationReader configReader) throws MissingConfigurationContextException {
\r
160 logger.debug(Utils.objectId(this) + " will configure search client for the session");
\r
162 client.configure(configReader);
\r
163 } catch (MissingConfigurationContextException mcce) {
\r
164 logger.info("No Faces context is available to the configurator at this time of invocation");
\r
166 } catch (ConfigurationException e) {
\r
167 logger.debug("Pz2Service adding configuration error");
\r
168 errors.addConfigurationError(new ConfigurationError("Search Client","Configuration",e.getMessage()));
\r
170 logger.info(configReader.document());
\r
171 pzresp.getSp().resetAuthAndBeyond(true);
\r
174 public void resetSearchAndRecordCommands () {
\r
175 pzreq.getRecord().removeParametersInState();
\r
176 pzreq.getSearch().removeParametersInState();
\r
181 * Updates display data objects by simultaneously issuing the following Pazpar2 commands:
\r
182 * 'show', 'stat', 'termlist' and 'bytarget'.
\r
184 * If there are outstanding changes to the search command, a search
\r
185 * will be issued before the updates are performed. Outstanding changes could come
\r
186 * from the UI changing a search parameter and not executing search before starting
\r
187 * the update cycle - OR - it could come from the user clicking the browsers back/forward
\r
191 * This method is invoked from the composite 'pz2watch', which uses Ajax
\r
192 * to keep invoking this method until it returns '0' (for zero active clients).
\r
195 * UI components that display data from show, stat, termlist or bytarget,
\r
196 * should be re-rendered after each update.
\r
198 * Example of invocation in UI:
\r
200 * <pz2utils:pz2watch id="pz2watch"
\r
201 * renderWhileActiveclients="myshowui mystatui mytermsui" /<
\r
204 * <h:inputText id="query" value="#{pzreq.search.query}" size="50"/>
\r
205 * <h:commandButton id="button" value="Search">
\r
206 * <f:ajax execute="query" render="${pz2.watchActiveclients}"/>
\r
207 * </h:commandButton>
\r
210 * The expression pz2.watchActiveClients will invoke the method repeatedly, and the
\r
211 * UI sections myshowui, mystatui, and mytermsui will be rendered on each poll.
\r
213 * @return a count of the remaining active clients from the most recent search.
\r
215 public String update () {
\r
216 logger.debug("Updating show,stat,termlist,bytarget from pazpar2");
\r
217 if (errors.hasConfigurationErrors()) {
\r
218 logger.error("Ignoring show,stat,termlist,bytarget commands due to configuration errors.");
\r
220 } else if (pzresp.getSearch().hasApplicationError()) {
\r
221 logger.error("Ignoring show,stat,termlist,bytarget commands due to problem with most recent search.");
\r
223 } else if (!hasQuery()) {
\r
224 logger.debug("Ignoring show,stat,termlist,bytarget commands because there is not yet a query.");
\r
227 return update("show,stat,termlist,bytarget");
\r
232 * Simultaneously refreshes the data objects listed in 'commands' from pazpar2, potentially running a
\r
233 * search or a record command first if any of these two commands have outstanding parameter changes.
\r
235 * @param commands, a comma-separated list of Pazpar2 commands to execute
\r
237 * @return Number of activeclients at the time of the 'show' command,
\r
238 * or 'new' if search was just initiated.
\r
240 public String update (String commands) {
\r
241 logger.debug("Request to update: " + commands);
\r
243 if (commands.equals("search")) {
\r
244 pzreq.getSearch().run();
\r
245 pzresp.getSearch().setIsNew(false);
\r
247 } else if (commands.equals("record")) {
\r
248 pzreq.getRecord().run();
\r
249 return pzresp.getRecord().getActiveClients();
\r
250 } else if (pzresp.getSearch().isNew()) {
\r
251 // For returning notification of 'search started' quickly to UI
\r
252 logger.info("New search. Marking it old, then returning 'new' to trigger another round-trip.");
\r
253 pzresp.getSearch().setIsNew(false);
\r
256 handleQueryStateChanges(commands);
\r
257 if (pzresp.getSearch().hasApplicationError()) {
\r
258 logger.error("The command(s) " + commands + " cancelled because the latest search command had an error.");
\r
260 } else if (errors.hasConfigurationErrors()) {
\r
261 logger.error("The command(s) " + commands + " cancelled due to configuration errors.");
\r
264 logger.debug("Processing request for " + commands);
\r
265 List<CommandThread> threadList = new ArrayList<CommandThread>();
\r
266 StringTokenizer tokens = new StringTokenizer(commands,",");
\r
267 while (tokens.hasMoreElements()) {
\r
268 threadList.add(new CommandThread(pzreq.getCommand(tokens.nextToken()),searchClient,Pz2Service.get().getPzresp()));
\r
270 for (CommandThread thread : threadList) {
\r
273 for (CommandThread thread : threadList) {
\r
276 } catch (InterruptedException e) {
\r
277 e.printStackTrace();
\r
280 return pzresp.getActiveClients();
\r
283 } catch (ClassCastException cce) {
\r
284 cce.printStackTrace();
\r
286 } catch (NullPointerException npe) {
\r
287 npe.printStackTrace();
\r
289 } catch (Exception e) {
\r
290 e.printStackTrace();
\r
297 * This methods main purpose is to support browser history.
\r
299 * When the browsers back or forward buttons are pressed, a
\r
300 * re-search /might/ be required - namely if the query changes.
\r
301 * So, as the UI requests updates of the page (show,facets,
\r
302 * etc) this method checks if a search must be executed
\r
303 * before those updates are performed.
\r
305 * It will consequently also run a search if the UI updates a
\r
306 * search parameter without actually explicitly executing the search
\r
307 * before setting of the polling.
\r
309 * @see {@link com.indexdata.mkjsf.pazpar2.state.StateManager#setCurrentStateKey}
\r
312 protected void handleQueryStateChanges (String commands) {
\r
313 if (stateMgr.hasPendingStateChange("search") && hasQuery()) {
\r
314 logger.info("Triggered search: Found pending search change [" + pzreq.getCommand("search").toString() + "], doing search before updating " + commands);
\r
315 pzreq.getSearch().run();
\r
316 pzresp.getSearch().setIsNew(false);
\r
318 if (stateMgr.hasPendingStateChange("record") && ! commands.equals("record")) {
\r
319 logger.debug("Found pending record ID change. Doing record before updating " + commands);
\r
320 stateMgr.hasPendingStateChange("record",false);
\r
321 pzreq.getRecord().run();
\r
326 * Used by the state manager to notify Pz2Service about state changes
\r
329 public void stateUpdated(String commandName) {
\r
330 logger.debug("State change reported for [" + commandName + "]");
\r
331 if (commandName.equals("show")) {
\r
332 logger.debug("Updating show");
\r
333 update(commandName);
\r
339 * Will retrieve -- or alternatively remove -- the record with the given
\r
340 * recid from memory.
\r
342 * A pazpar2 'record' command will then be issued. The part of the UI
\r
343 * showing record data should thus be re-rendered.
\r
348 public String toggleRecord (String recId) {
\r
349 if (hasRecord(recId)) {
\r
350 pzreq.getRecord().removeParameters();
\r
351 pzresp.put("record", new RecordResponse());
\r
354 pzreq.getRecord().setId(recId);
\r
355 pzreq.getRecord().run();
\r
356 return pzresp.getRecord().getActiveClients();
\r
361 * Resolves whether the back-end has a record with the given recid in memory
\r
363 * @return true if the bean currently holds the record with recid
\r
365 public boolean hasRecord (String recId) {
\r
366 return pzreq.getCommand("record").hasParameters() && pzresp.getRecord().getRecId().equals(recId);
\r
370 * Returns the current hash key, used for internal session state tracking
\r
371 * and potentially for browser history entries
\r
373 * A UI author would not normally be concerned with retrieving this. It's used by the
\r
374 * framework internally
\r
376 * @return string that can be used for browsers window.location.hash
\r
378 public String getCurrentStateKey () {
\r
379 return stateMgr.getCurrentState().getKey();
\r
383 * Sets the current state key, i.e. when user clicks back or forward in browser history.
\r
384 * Would normally be automatically handled by the frameworks components.
\r
386 * @param key corresponding to browsers hash string
\r
388 public void setCurrentStateKey(String key) {
\r
389 stateMgr.setCurrentStateKey(key);
\r
392 protected boolean hasQuery() {
\r
393 return pzreq.getCommand("search").hasParameterValue("query");
\r
397 * Returns a component for drawing a pager to navigate by.
\r
398 * @return ResultsPager pager component
\r
400 public ResultsPager getPager () {
\r
401 if (pager == null) {
\r
402 pager = new ResultsPager(pzresp);
\r
408 * Initiates a pager object, a component holding the data to draw a sequence
\r
409 * of page numbers to navigate by and mechanisms to navigate with
\r
411 * @param pageRange number of pages to display in the pager
\r
412 * @return ResultsPager the initiated pager component
\r
414 public ResultsPager setPager (int pageRange) {
\r
415 pager = new ResultsPager(pzresp,pageRange,pzreq);
\r
420 * Sets the URL of the Service Proxy to use for requests
\r
424 public void setServiceProxyUrl(String url) {
\r
425 searchClient = spClient;
\r
426 setServiceType(SERVICE_TYPE_SP);
\r
427 setServiceUrl(url);
\r
431 * Returns the Service Proxy URL currently defined for servicing requests
\r
434 public String getServiceProxyUrl () {
\r
435 if (isServiceProxyService()) {
\r
436 return spClient.getServiceUrl();
\r
443 * Sets the URL of the Pazpar2 to use for requests
\r
447 public void setPazpar2Url(String url) {
\r
448 searchClient = pz2Client;
\r
449 setServiceType(SERVICE_TYPE_PZ2);
\r
450 setServiceUrl(url);
\r
454 * Returns the Pazpar2 URL currently defined for servicing requests
\r
457 public String getPazpar2Url() {
\r
458 if (isPazpar2Service()) {
\r
459 return pz2Client.getServiceUrl();
\r
466 * Sets the URL to be used by the currently selected search client
\r
467 * when running requests.
\r
471 public void setServiceUrl(String url) {
\r
472 if (url!=null && searchClient != null && !url.equals(searchClient.getServiceUrl())) {
\r
473 pzreq.getRecord().removeParametersInState();
\r
474 pzreq.getSearch().removeParametersInState();
\r
475 pzresp.getSp().resetAuthAndBeyond(true);
\r
476 searchClient.setServiceUrl(url);
\r
481 * Gets the currently selected URL used for executing requests.
\r
484 public String getServiceUrl() {
\r
485 return (searchClient!=null ? searchClient.getServiceUrl() : "");
\r
488 public void setServiceId () {
\r
489 pzreq.getRecord().removeParametersInState();
\r
490 pzreq.getSearch().removeParametersInState();
\r
491 pzresp.resetSearchAndBeyond();
\r
492 pz2Client.setServiceId(pzreq.getInit().getService());
\r
495 public String getServiceId () {
\r
496 return pzreq.getInit().getService();
\r
499 public boolean getServiceUrlIsDefined() {
\r
500 return (searchClient != null && searchClient.hasServiceUrl());
\r
503 public List<String> getServiceProxyUrls() {
\r
504 List<String> urls = new ArrayList<String>();
\r
506 urls.addAll(serviceProxyUrls);
\r
510 public List<String> getPazpar2Urls () {
\r
511 List<String> urls = new ArrayList<String>();
\r
513 urls.addAll(pazpar2Urls);
\r
517 public String getServiceType () {
\r
518 return serviceType;
\r
521 public boolean isPazpar2Service () {
\r
522 return serviceType.equals(SERVICE_TYPE_PZ2);
\r
525 public boolean isServiceProxyService() {
\r
526 return serviceType.equals(SERVICE_TYPE_SP);
\r
529 public boolean serviceIsToBeDecided () {
\r
530 return serviceType.equals(SERVICE_TYPE_TBD);
\r
533 public ServiceProxyClient getSpClient () {
\r
537 public boolean getAuthenticationRequired () {
\r
538 return spClient.isAuthenticatingClient();
\r
541 public String getCheckHistory () {
\r
542 return ":pz2watch:stateForm:windowlocationhash";
\r
545 public String getWatchActiveclients () {
\r
546 return ":pz2watch:activeclientsForm:activeclientsField";
\r
549 public String getWatchActiveclientsRecord () {
\r
550 return ":pz2watch:activeclientsForm:activeclientsFieldRecord";
\r
554 public void configure(ConfigurationReader reader)
\r
555 throws ConfigurationException {
\r
556 Configuration config = reader.getConfiguration(this);
\r
557 if (config == null) {
\r
558 serviceType = SERVICE_TYPE_TBD;
\r
560 String service = config.get("TYPE");
\r
561 if (service == null || service.length()==0) {
\r
562 serviceType = SERVICE_TYPE_TBD;
\r
563 } else if (serviceTypes.contains(service.toUpperCase())) {
\r
564 setServiceType(service.toUpperCase());
\r
566 logger.error("Unknown serviceType type in configuration [" + service + "], can be one of " + serviceTypes);
\r
567 serviceType = SERVICE_TYPE_TBD;
\r
569 serviceProxyUrls = config.getMultiProperty(SERVICE_PROXY_URL_LIST,",");
\r
570 pazpar2Urls = config.getMultiProperty(PAZPAR2_URL_LIST, ",");
\r
572 logger.info(reader.document());
\r
573 logger.info("Service Type is configured to " + serviceType);
\r
578 public Map<String, String> getDefaults() {
\r
579 return new HashMap<String,String>();
\r
583 public String getModuleName() {
\r
584 return MODULE_NAME;
\r
588 public List<String> documentConfiguration() {
\r
589 return new ArrayList<String>();
\r
592 public void setServiceTypePZ2() {
\r
593 setServiceType(SERVICE_TYPE_PZ2);
\r
596 public void setServiceTypeSP() {
\r
597 setServiceType(SERVICE_TYPE_SP);
\r
600 public void setServiceTypeTBD() {
\r
601 setServiceType(SERVICE_TYPE_TBD);
\r
604 private void setServiceType(String type) {
\r
605 if (!serviceType.equals(type) &&
\r
606 !serviceType.equals(SERVICE_TYPE_TBD)) {
\r
607 resetSearchAndRecordCommands();
\r
608 pzresp.getSp().resetAuthAndBeyond(true);
\r
610 serviceType = type;
\r
611 if (serviceType.equals(SERVICE_TYPE_PZ2)) {
\r
612 searchClient = pz2Client;
\r
613 logger.info("Setting a Pazpar2 client to serve requests.");
\r
614 } else if (serviceType.equals(SERVICE_TYPE_SP)) {
\r
615 searchClient = spClient;
\r
616 logger.info("Setting a Service Proxy client to serve requests.");
\r
618 logger.info("Clearing search client. No client defined to serve requests at this point.");
\r
619 searchClient = null;
\r
623 public SearchClient getSearchClient() {
\r
624 return searchClient;
\r