1    /*
2     * Copyright 2005 :torweg free software group
3     * 
4     * This program is free software: you can redistribute it and/or modify
5     * it under the terms of the GNU General Public License as published by
6     * the Free Software Foundation, either version 3 of the License, or
7     * (at your option) any later version.
8     * 
9     * This program is distributed in the hope that it will be useful,
10    * but WITHOUT ANY WARRANTY; without even the implied warranty of
11    * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12    * GNU General Public License for more details.
13    * 
14    * You should have received a copy of the GNU General Public License
15    * along with this program.  If not, see <http://www.gnu.org/licenses/>.
16    *
17    */
18   package org.torweg.pulse.service.request;
19   
20   import java.io.IOException;
21   import java.io.Serializable;
22   import java.io.UnsupportedEncodingException;
23   import java.net.URI;
24   import java.net.URISyntaxException;
25   import java.net.URLDecoder;
26   import java.net.URLEncoder;
27   import java.security.NoSuchAlgorithmException;
28   import java.util.ArrayList;
29   import java.util.Arrays;
30   import java.util.Enumeration;
31   import java.util.HashMap;
32   import java.util.HashSet;
33   import java.util.List;
34   import java.util.Locale;
35   import java.util.Map;
36   import java.util.Set;
37   import java.util.SortedMap;
38   import java.util.TreeMap;
39   
40   import javax.servlet.http.HttpServletRequest;
41   import javax.xml.bind.annotation.XmlAccessOrder;
42   import javax.xml.bind.annotation.XmlAccessType;
43   import javax.xml.bind.annotation.XmlAccessorOrder;
44   import javax.xml.bind.annotation.XmlAccessorType;
45   import javax.xml.bind.annotation.XmlElement;
46   import javax.xml.bind.annotation.XmlElementWrapper;
47   import javax.xml.bind.annotation.XmlRootElement;
48   import javax.xml.bind.annotation.XmlTransient;
49   import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
50   
51   import org.apache.commons.fileupload.FileItem;
52   import org.apache.commons.fileupload.FileUploadBase;
53   import org.apache.commons.fileupload.disk.DiskFileItemFactory;
54   import org.apache.commons.fileupload.servlet.ServletFileUpload;
55   import org.apache.commons.fileupload.servlet.ServletRequestContext;
56   import org.jdom.Element;
57   import org.slf4j.Logger;
58   import org.slf4j.LoggerFactory;
59   import org.torweg.pulse.annotations.Action.Security;
60   import org.torweg.pulse.bundle.ActionConfiguration;
61   import org.torweg.pulse.bundle.JDOMable;
62   import org.torweg.pulse.configuration.ConfigurationException;
63   import org.torweg.pulse.configuration.PoorMansCache;
64   import org.torweg.pulse.invocation.lifecycle.Lifecycle;
65   import org.torweg.pulse.service.PulseException;
66   import org.torweg.pulse.service.event.ForbiddenEvent;
67   import org.torweg.pulse.service.request.ServiceRequest.SessionMode;
68   import org.torweg.pulse.util.LocaleUtils;
69   import org.torweg.pulse.util.StringUtils;
70   import org.torweg.pulse.util.io.ByteUtils;
71   import org.torweg.pulse.util.streamscanner.InacceptableStreamException;
72   import org.torweg.pulse.util.xml.bind.LocaleXmlAdapter;
73   
74   /**
75    * implements a {@code Command} used to control the <em>pulse</em> container.
76    * <p>
77    * It is initialised by the {@code init(StringBuilder,ServiceRequest)} method
78    * where it parses the request string and thereof derives the current locale,
79    * mode, bundle, action and the parameters of the current request. New instances
80    * of the command should be created by the {@code newInstance} or
81    * {@code createCopy} methods.
82    * </p>
83    * <p>
84    * The <em>pulse</em> command URIs are structured as follows:<br/>
85    * <table border="0">
86    * <tr>
87    * <th>path to servlet</th>
88    * <th>parsable URI part</th>
89    * <th>suffix and request parameters</th>
90    * </tr>
91    * <tr>
92    * <td><code>http://some.server.com/webapp/ServletName/</td>
93    * <td><code>{locale}.{bundle}.{action}.{sitemapID}.{pulseParameters}/</td>
94    * <td><code>{suffix}?{httpParameters}</code></td>
95    * </tr>
96    * </table>
97    * </p>
98    * <p>
99    * Below the parsable part is explained in more detail:
100   * </p>
101   * <ul>
102   * <li><strong>{locale}</strong> is a string representation of the requested
103   * {@link Locale} (e.g. 'de_DE_EURO')</li>
104   * <li><strong>{bundle}</strong> identifies the requested Controllers for the
105   * application to run (see {@link org.torweg.pulse.bundle.Bundle}).</li>
106   * <li><strong>{action}</strong> denotes the action for the specified
107   * Controllers (see {@link org.torweg.pulse.annotations.Action}).</li>
108   * <li><strong>{sitemapID}</strong> the ID of the {@code SitemapNode} the
109   * {@code Command} is associated with (see: {@link org.torweg.pulse.site.View},
110   * {@link org.torweg.pulse.site.map.SitemapNode})</li>
111   * <li><strong>{pulseParameters}</strong> are given in the following form:
112   * '{parameterName}.{parameterValue}.{parameterName}.{parameterValue1}
113   * *{parameterValue2}'</li>
114   * <li><strong>{suffix}</strong> is a suffix which is appended after the
115   * parsable part of the {@code URI}. The default suffix is <tt>index.html</tt>.</li>
116   * </ul>
117   * <p>
118   * In the case of file uploads, via <tt>multipart/form-data</tt> an
119   * {@link UploadProgress} can be added by adding the <strong>
120   * <em>pulseParameter</em></strong> <tt>addUploadProgress</tt> with an
121   * per-session unique identifier as the value.
122   * </p>
123   * 
124   * @author Thomas Weber, Christian Schatt, Daniel Dietz
125   * @version $Revision: 2038 $
126   */
127  @XmlRootElement(name = "command")
128  @XmlAccessorOrder(XmlAccessOrder.UNDEFINED)
129  @XmlAccessorType(XmlAccessType.FIELD)
130  public final class Command implements JDOMable, Serializable {
131  
132      /**
133       * serialVersionUID.
134       */
135      private static final long serialVersionUID = 2639731163738871391L;
136  
137      /**
138       * the logger.
139       */
140      private static final Logger LOGGER = LoggerFactory.getLogger(Command.class);
141  
142      /**
143       * the name of the bundle of the {@code Command}.
144       */
145      @XmlElement(name = "bundle")
146      private String bundle;
147  
148      /**
149       * the name of the action to execute on this {@code Command}.
150       */
151      @XmlElement(name = "action")
152      private String action;
153  
154      /**
155       * the locale of this {@code Command}.
156       */
157      @XmlJavaTypeAdapter(value = LocaleXmlAdapter.class)
158      private Locale locale;
159  
160      /**
161       * the {@code SitemapNode}'s id of the content loaded by the Command. May be
162       * null, if the view is not part of the {@code Sitemap}.
163       */
164      @XmlElement(name = "sitemap-id")
165      private Long sitemapID = null;
166  
167      /**
168       * a map containing the PulseParameters.
169       */
170      @XmlTransient
171      // JAXB-annotated getter provided
172      private final SortedMap<String, Parameter> pulseParameters = new TreeMap<String, Parameter>();
173  
174      /**
175       * a map containing the http-parameters.
176       */
177      @XmlTransient
178      // JAXB-annotated getter provided
179      private final SortedMap<String, Parameter> httpParameters = new TreeMap<String, Parameter>();
180  
181      /**
182       * the suffix that is added after the parsable command URI (default:
183       * {@code index.html}).
184       */
185      @XmlElement(name = "suffix")
186      private String suffix = "index.html";
187  
188      /**
189       * flag for {@code toString()}.
190       * <p>
191       * {@code true} for all newly created commands, {@code false} for commands
192       * build from a request with an HTTP request method other than " {@code GET}
193       * ".
194       * </p>
195       */
196      @XmlTransient
197      private boolean includeHTTPParametersInToString = true;
198  
199      /**
200       * the level of security to be used when generating the command; default
201       * {@link Security#NEVER}.
202       */
203      @XmlElement(name = "security")
204      private Security security = Security.NEVER;
205  
206      /**
207       * the token attached to the command.
208       */
209      @XmlElement(name = "token-value")
210      private String tokenValue;
211  
212      /**
213       * raw constructor for <em>pulse</em> internal purposes.
214       * 
215       */
216      public Command() {
217          super();
218      }
219  
220      /**
221       * generates an URL fragment containing the locale, mode, bundle, action and
222       * pulse-parameters.
223       * 
224       * @param localeParam
225       *            the {@code Locale} as parameter.
226       * @param session
227       *            the service session for access to the token secret
228       * 
229       * @return the encoded URL-fragment.
230       */
231      protected String generateParsableCommandURI(final Locale localeParam,
232              final ServiceSession session) {
233          String localeString = "null";
234          if (localeParam != null) {
235              localeString = localeParam.toString();
236          }
237          StringBuilder parsableURI = encodePart(new StringBuilder(localeString))
238                  .append('.').append(encodePart(new StringBuilder(this.bundle)))
239                  .append('.').append(encodePart(new StringBuilder(this.action)))
240                  .append('.');
241  
242          /* build sitemapID part */
243          StringBuilder sitemapIdPart = new StringBuilder();
244          if (this.sitemapID != null) {
245              sitemapIdPart.append(this.sitemapID.toString());
246          }
247          /* check, if token is necessary */
248          if ((session != null)
249                  && Lifecycle.getBundle(this.bundle).getActionsRequiringTokens()
250                          .contains(this.action)) {
251              sitemapIdPart.append('_')
252                      .append(generateToken(session.getSecret()));
253          }
254          parsableURI.append(sitemapIdPart);
255  
256          for (Parameter p : this.pulseParameters.values()) {
257              parsableURI.append('.')
258                      .append(encodePart(new StringBuilder(p.getName())))
259                      .append('.');
260              if (!p.getValues().isEmpty()) {
261                  StringBuilder values = new StringBuilder();
262                  for (String value : p.getValues()) {
263                      values.append(encodePart(new StringBuilder(value))).append(
264                              '*');
265                  }
266                  values.deleteCharAt(values.length() - 1);
267                  parsableURI.append(values);
268              }
269          }
270          parsableURI.append('.');
271  
272          return parsableURI.toString();
273      }
274  
275      /**
276       * initialises the command from the given parsable part and updates the
277       * service request where necessary.
278       * 
279       * @param parsable
280       *            the parsable part of the URL
281       * @param sr
282       *            the service request
283       * @return the initialised command
284       * @throws UnparsableRequestException
285       *             on errors extracting the command
286       */
287      public Command init(final StringBuilder parsable, final ServiceRequest sr) {
288          ServiceRequestImpl req = (ServiceRequestImpl) sr;
289          LOGGER.trace("parsing String:'{}'", parsable);
290          checkRequestMethod(req);
291  
292          /* remove slash at start */
293          if (parsable.charAt(0) == '/') {
294              parsable.deleteCharAt(0);
295          }
296          /* cut off suffix */
297          String rawSuffix = "";
298          int endOfParsableFragment = parsable.indexOf("/");
299          if (endOfParsableFragment > -1) {
300              if (endOfParsableFragment < parsable.length()) {
301                  rawSuffix = parsable.substring(endOfParsableFragment + 1);
302              }
303              parsable.delete(parsable.indexOf("/"), parsable.length());
304          }
305  
306          /* process the parsable part of the URI */
307          processParsableURIFragment(parsable, sr);
308  
309          /* prepare request parameters and handle uploaded files */
310          processRequestParametersAndUploads(req);
311  
312          /*
313           * For convenience, it is possible to override the PulseParameters of
314           * this Command with the RequestParameters.
315           */
316          overrideWithRequestParameters(req);
317  
318          /* check, if we have at least a Locale and a Bundle */
319          if ((this.locale == null) || (this.bundle == null)) {
320              throw new CommandURIParseException("The URI '"
321                      + req.getHttpServletRequest().getRequestURI()
322                      + "' does not define a complete command.");
323          }
324  
325          /* set suffix */
326          try {
327              this.suffix = URLDecoder.decode(rawSuffix, "utf-8");
328          } catch (UnsupportedEncodingException e) {
329              LOGGER.error(e.getLocalizedMessage());
330              this.suffix = rawSuffix;
331          }
332          LOGGER.debug("http params: {}", this.httpParameters.size());
333          LOGGER.debug("pulse params: {}", this.pulseParameters.size());
334          LOGGER.debug("token: {}", this.tokenValue);
335          LOGGER.debug("Parsed Command: {}", this.toString());
336          return this;
337      }
338  
339      /**
340       * checks the request method and set {@code includeHTTPParametersInToString}
341       * to false, if the request method is not "{@code GET}".
342       * 
343       * @param req
344       *            the current request
345       */
346      private void checkRequestMethod(final ServiceRequestImpl req) {
347          if ((req != null)
348                  && !req.getHttpServletRequest().getMethod()
349                          .equalsIgnoreCase("get")) {
350              this.includeHTTPParametersInToString = false;
351          }
352      }
353  
354      /**
355       * processes the HTTP parameters and form based file uploads.
356       * 
357       * @param req
358       *            the request
359       */
360      private void processRequestParametersAndUploads(final ServiceRequestImpl req) {
361          this.httpParameters.clear();
362          if ((req != null)
363                  && (FileUploadBase
364                          .isMultipartContent(new ServletRequestContext(req
365                                  .getHttpServletRequest())))) {
366              /* process uploaded files and prepare request parameters */
367              this.httpParameters.putAll(Command.processFileUploads(req, this));
368          } else {
369              /* prepare request parameters */
370              this.httpParameters.putAll(parseRequestParameter(req));
371          }
372      }
373  
374      /**
375       * parses the RequestParameters to map them in a separate {@code Map}.
376       * 
377       * @param req
378       *            the {@code ServiceRequest} to parse the parameters from.
379       * @return the {@code HashMap} containing the http-parameters mapped by
380       *         their name.
381       */
382      private Map<String, Parameter> parseRequestParameter(
383              final ServiceRequest req) {
384          TreeMap<String, Parameter> newHTTPParameters = new TreeMap<String, Parameter>();
385          if (req == null) {
386              return newHTTPParameters;
387          }
388          HttpServletRequest servReq = req.getHttpServletRequest();
389          Enumeration<?> paramNames = servReq.getParameterNames();
390          while (paramNames.hasMoreElements()) {
391              String paramName = (String) paramNames.nextElement();
392              Parameter param = new Parameter(paramName, Arrays.asList(servReq
393                      .getParameterValues(paramName)));
394              newHTTPParameters.put(paramName, param);
395          }
396          return newHTTPParameters;
397      }
398  
399      /**
400       * processes the parsable URI fragment.
401       * 
402       * @param parsable
403       *            the fragment
404       * @param sr
405       *            the {@code ServiceRequest}
406       * @throws UnparsableRequestException
407       *             on empty parsable fragments
408       */
409      private void processParsableURIFragment(final StringBuilder parsable,
410              final ServiceRequest sr) {
411          /* nothing to parse --> return */
412          if (parsable.length() == 0) {
413              throw new UnparsableRequestException("No path info given.");
414          }
415          extractLocaleBundleActionSitemapId(parsable, sr);
416  
417          // now process the pulseParameters
418          parsePulseParameters(parsable);
419      }
420  
421      /**
422       * extracts {@code Locale}, {@code Bundle}, action and sitemapID from the
423       * parasable part of the command URI.
424       * 
425       * @param parsable
426       *            the parsable part of the URI
427       * @param sr
428       *            the current request
429       */
430      private void extractLocaleBundleActionSitemapId(
431              final StringBuilder parsable, final ServiceRequest sr) {
432          // extract locale
433          List<String> decodedSegment = Command.decodePart(parsable);
434          if (decodedSegment.size() == 1) {
435              this.locale = checkLocale(decodedSegment.get(0), sr);
436          } else if (decodedSegment.size() > 1) {
437              throw new CommandURIParseException("Illegal Locale '"
438                      + decodedSegment.get(0) + "'.");
439          }
440  
441          // extract bundle
442          decodedSegment = Command.decodePart(parsable);
443          if (decodedSegment.size() == 1) {
444              this.bundle = decodedSegment.get(0);
445          } else if (decodedSegment.size() > 1) {
446              throw new CommandURIParseException("Illegal bundle '"
447                      + decodedSegment.get(0) + "'.");
448          }
449  
450          // extract action
451          decodedSegment = Command.decodePart(parsable);
452          if (decodedSegment.size() == 1) {
453              this.action = decodedSegment.get(0);
454          } else if (decodedSegment.size() > 1) {
455              throw new CommandURIParseException("Illegal action '"
456                      + decodedSegment.get(0) + "'.");
457          }
458  
459          // extract sitemapId
460          decodedSegment = Command.decodePart(parsable);
461          if (decodedSegment.size() == 1) {
462              if (!decodedSegment.get(0).equals("")) {
463                  String segment = decodedSegment.get(0);
464                  if (segment.indexOf('_') > -1) {
465                      String[] parts = segment.split("_");
466                      if (parts.length == 1) {
467                          this.tokenValue = parts[0];
468                      } else if (parts.length == 2) {
469                          try {
470                              this.sitemapID = Long.parseLong(parts[0]);
471                          } catch (NumberFormatException e) {
472                              this.sitemapID = null; // NOPMD
473                          }
474                          this.tokenValue = parts[1];
475                      }
476                  } else {
477                      try {
478                          this.sitemapID = Long.parseLong(decodedSegment.get(0));
479                      } catch (NumberFormatException e) {
480                          throw new CommandURIParseException( // NOPMD
481                                  "Illegal sitemapID '" + decodedSegment.get(0)
482                                          + "'.");
483                      }
484                  }
485              }
486          } else if (decodedSegment.size() > 1) {
487              throw new CommandURIParseException("Illegal sitemapID '"
488                      + decodedSegment.get(0) + "'.");
489          }
490      }
491  
492      /**
493       * parses the pulse parameters.
494       * 
495       * @param parsable
496       *            the remaining part of the parsable URI fragment after
497       *            processing the {@code Locale}, {@code Bundle}, action and
498       *            sitemapID.
499       */
500      private void parsePulseParameters(final StringBuilder parsable) {
501          List<String> decodedSegment;
502          int i = 0;
503          String paramName = "";
504          while (parsable.length() > 0) {
505              decodedSegment = Command.decodePart(parsable);
506              if (i % 2 == 0) {
507                  if (decodedSegment.size() == 1) {
508                      paramName = decodedSegment.get(0);
509                  } else if (decodedSegment.size() > 1) {
510                      throw new CommandURIParseException("Illegal Parametername.");
511                  }
512              } else if (!paramName.equals("")) {
513                  addPulseParameter(paramName, decodedSegment);
514              } else {
515                  throw new CommandURIParseException("Illegal Parametername.");
516              }
517              i++;
518          }
519      }
520  
521      /**
522       * make locale, bundle, action overridable by HTTP parameters.
523       * <ul * * * type="disc">
524       * <li>{@code pulse.locale}: the locale</li>
525       * <li>
526       * {@code pulse.bundle}: the bundle</li>
527       * <li>{@code pulse.action}: the action</li>
528       * </ul>
529       * 
530       * @param req
531       *            the ServiceRequest
532       */
533      private void overrideWithRequestParameters(final ServiceRequest req) {
534          if ((this.httpParameters.get("pulse.locale") != null)
535                  && (this.httpParameters.get("pulse.locale").getValues().get(0)
536                          .length() > 1)) {
537              this.locale = checkLocale(this.httpParameters.get("pulse.locale")
538                      .getValues().get(0), req);
539          }
540          if (this.locale == null) {
541              this.locale = checkLocale(null, req);
542          }
543          if ((this.httpParameters.get("pulse.bundle") != null)
544                  && (this.httpParameters.get("pulse.bundle").getValues().get(0)
545                          .length() > 0)) {
546              this.bundle = this.httpParameters.get("pulse.bundle").getValues()
547                      .get(0);
548          }
549          if ((this.httpParameters.get("pulse.action") != null)
550                  && (this.httpParameters.get("pulse.action").getValues().get(0)
551                          .length() > 0)) {
552              this.action = this.httpParameters.get("pulse.action").getValues()
553                      .get(0);
554          }
555      }
556  
557      /**
558       * uses the string containing the serialized locale and tries to find a
559       * suitable locale. If the string has no direct matches, the browser
560       * settings will be parsed.
561       * 
562       * @param localeString
563       *            the {@code String}-representation of the locale
564       * @param request
565       *            the current {@code ServiceRequest}.
566       * @return the suitable {@code Locale}.
567       */
568      private Locale checkLocale(final String localeString,
569              final ServiceRequest request) {
570          Locale loc = LocaleUtils.localeFromString(localeString);
571          if (Lifecycle.getKnownLocales().contains(loc)) {
572              return loc;
573          }
574          return LocaleManager.getInstance().findBestMatchingLocale(localeString,
575                  request);
576      }
577  
578      /**
579       * used by toString() to create a parameter block.
580       * 
581       * @param str
582       *            the string builder used in toString()
583       * @param parameters
584       *            the parameters of the block
585       */
586      private void debugParameterList(final StringBuilder str,
587              final List<Parameter> parameters) {
588          if (!parameters.isEmpty()) {
589              str.append('(');
590          }
591          for (Parameter p : parameters) {
592              str.append(p.toString());
593          }
594          if (!parameters.isEmpty()) {
595              str.append(')');
596          }
597      }
598  
599      /**
600       * takes care of file uploads and adds them to the ServiceRequestImpl.
601       * 
602       * @param req
603       *            the current ServiceRequestImpl
604       * @param command
605       *            the command that is currently parsed
606       * @return a Map of the request parameters sent with the file upload(s).
607       * 
608       */
609      @SuppressWarnings("unchecked")
610      private static Map<String, Parameter> processFileUploads(
611              final ServiceRequestImpl req, final Command command) {
612  
613          /* check, whether uploads are allowed */
614          boolean uploadsAllowed = false;
615          try {
616              uploadsAllowed = Lifecycle.getBundle(command.getBundle())
617                      .getActionConfiguration(command.getAction())
618                      .isUploadAllowed();
619          } catch (Exception e) {
620              LOGGER.warn(
621                      "Could not determine upload allowance: "
622                              + e.getLocalizedMessage(), e);
623          }
624  
625          /* prepare upload handler */
626          ServletFileUpload upload = prepareUploadHandler(uploadsAllowed);
627  
628          /* add an upload progress, if requested and appropriate */
629          UploadProgress progress = addUploadProgress(req, upload);
630  
631          /* parse the request */
632          List<FileItem> items = new ArrayList<FileItem>();
633          try {
634              items = (List<FileItem>) upload.parseRequest(req
635                      .getHttpServletRequest());
636          } catch (FileUploadBase.SizeLimitExceededException e) {
637              req.getEventManager().addEvent(new ForbiddenEvent());
638          } catch (Exception e) {
639              throw new ConfigurationException(
640                      "Error parsing request for uploaded files.", e);
641          }
642  
643          /* process uploaded files and form fields */
644          Map<String, Parameter> requestParameterMap = new HashMap<String, Parameter>();
645          try {
646              processFilesAndFields(req, uploadsAllowed, requestParameterMap,
647                      items);
648          } catch (Exception e) {
649              progress.setError(e, command.locale);
650              throw new PulseException(e);
651          }
652          /* set progress to finished, uploads are processed */
653          progress.setFinished();
654  
655          return requestParameterMap;
656      }
657  
658      /**
659       * prepares the upload handler.
660       * 
661       * @param uploadsAllowed
662       *            {@code true}, if uploads are allowed for the current command
663       * @return the properly configured {@code ServletFileUpload}
664       */
665      private static ServletFileUpload prepareUploadHandler(
666              final boolean uploadsAllowed) {
667          /*
668           * configure the DiskFileItemFactory with the parameters set in the
669           * config of the service request
670           */
671          ServiceRequestImplConfig conf = (ServiceRequestImplConfig) PoorMansCache
672                  .getConfiguration(ServiceRequestImpl.class);
673          DiskFileItemFactory factory = new DiskFileItemFactory();
674          factory.setRepository(conf.getFileUploadTempDir());
675          factory.setSizeThreshold(0);
676  
677          /* Create a new file upload handler and configure it */
678          ServletFileUpload upload = new ServletFileUpload(factory);
679          if (uploadsAllowed) {
680              // apply configured limits
681              upload.setSizeMax(conf.getFileUploadMaxSize());
682          } else {
683              // strictly limit, but allow text fields some space
684              upload.setSizeMax(2048L);
685          }
686          return upload;
687      }
688  
689      /**
690       * adds an {@code UploadProgress} for the current upload, if appropriate and
691       * removes stale progresses.
692       * 
693       * @param req
694       *            the current request
695       * @param upload
696       *            the upload handler
697       * @return the {@code UploadProgress}
698       */
699      private static UploadProgress addUploadProgress(
700              final ServiceRequestImpl req, final ServletFileUpload upload) {
701          UploadProgress progress = new UploadProgress();
702          /* clean old progress items */
703          String progressBase = new StringBuilder(progress.getClass()
704                  .getCanonicalName()).append('#').toString();
705          Set<String> removableProgresses = new HashSet<String>();
706          for (String key : req.getSession().getAttributeNames()) {
707              if (key.startsWith(progressBase)) {
708                  Object value = req.getSession().getAttribute(key);
709                  if (value instanceof UploadProgress) {
710                      UploadProgress oldProgress = (UploadProgress) value;
711                      if (oldProgress.isStale()) {
712                          removableProgresses.add(key);
713                      }
714                  }
715              }
716          }
717          for (String key : removableProgresses) {
718              req.getSession().removeAttribute(key);
719          }
720  
721          /* add new progress */
722          if ((req.getCommand().getParameter("addUploadProgress") != null)
723                  && (!req.getCommand().getParameter("addUploadProgress")
724                          .getFirstValue().equals(""))) {
725              String progressId = new StringBuilder(progressBase).append(
726                      req.getCommand().getParameter("addUploadProgress")
727                              .getFirstValue()).toString();
728              req.getSession().setAttribute(progressId, progress);
729              upload.setProgressListener(progress);
730              LOGGER.debug("added UploadProgress ({})", progressId);
731          }
732          return progress;
733      }
734  
735      /**
736       * processes all uploaded files and form fields.
737       * 
738       * @param req
739       *            the current request
740       * @param uploadsAllowed
741       *            {@code true}, if uploads are allowed
742       * @param httpParameters
743       *            the map of HTTP parameters
744       * @param items
745       *            the uploaded items
746       * @throws IOException
747       *             on errors accessing the uploaded file
748       * @throws InacceptableStreamException
749       *             if any of the {@code InputStreamScannerChain}'s filters,
750       *             renders the uploaded file unacceptable
751       */
752      private static void processFilesAndFields(final ServiceRequestImpl req,
753              final boolean uploadsAllowed,
754              final Map<String, Parameter> httpParameters,
755              final List<FileItem> items) throws IOException,
756      // CHECKSTYLE:OFF (explicitly mentioned exception)
757              InacceptableStreamException {
758          // CHECKSTYLE:ON
759          for (FileItem item : items) {
760              if (!item.isFormField()) {
761                  if (!uploadsAllowed) {
762                      item.delete();
763                      if (item.getSize() > 0
764                              && (req.getEventManager().getStopEvent() != null)) {
765                          req.getEventManager().addEvent(new ForbiddenEvent());
766                      }
767                      continue;
768                  }
769                  /* add the uploaded file */
770                  UploadedFile uFile = new UploadedFile(item, req.getUser());
771  
772                  req.addUploadedFile(uFile);
773                  LOGGER.debug("Added uploaded file '{}' ({}).", uFile
774                          .getOriginalFileName(), uFile.getFile()
775                          .getAbsolutePath());
776              } else {
777                  /* add the parameter */
778                  httpParameters.put(
779                          item.getFieldName(),
780                          new Parameter(item.getFieldName(), new String(item
781                                  .get(), "utf-8")));
782              }
783          }
784      }
785  
786      /**
787       * encodes a {@code String} for the pulse-request-String. following
788       * characters will be escaped:
789       * <ul>
790       * <li>'-' to '--'</li>
791       * <li>'.' to '-.'</li>
792       * <li>'*' to '-*'</li>
793       * </ul>
794       * 
795       * @param toEncode
796       *            the {@code String} to encode.
797       * @return the encoded {@code String}.
798       */
799      private static StringBuilder encodePart(final StringBuilder toEncode) {
800          StringBuilder encoded = new StringBuilder();
801          for (int i = 0; i < toEncode.length(); i++) {
802              char c = toEncode.charAt(i);
803              if ((c == '-') || (c == '*') || (c == '.')) {
804                  encoded.append('-');
805              }
806              encoded.append(c);
807          }
808          return encoded;
809      }
810  
811      /**
812       * Decodes the {@code StringBuilder} buffer until the next {@code .} is
813       * reached.&nbsp;The processed part of the buffer will be cut off during the
814       * processing.
815       * 
816       * @param buffer
817       *            the {@code StringBuilder} to be decoded
818       * 
819       * @return the decoded strings
820       */
821      private static List<String> decodePart(final StringBuilder buffer) {
822          int pos = 0;
823          StringBuilder current = new StringBuilder();
824          List<String> decoded = new ArrayList<String>();
825          while (pos < buffer.length()) {
826              if (buffer.charAt(pos) == '.') {
827                  try {
828                      decoded.add(URLDecoder.decode(current.toString(), "utf-8"));
829                  } catch (UnsupportedEncodingException e) {
830                      LOGGER.warn(
831                              "Error while decoding pulse parameters. {}: {}",
832                              current, e.getLocalizedMessage());
833                  }
834                  buffer.delete(0, ++pos);
835                  return decoded;
836              } else if (buffer.charAt(pos) == '*') {
837                  try {
838                      decoded.add(URLDecoder.decode(current.toString(), "utf-8"));
839                  } catch (UnsupportedEncodingException e) {
840                      LOGGER.warn(
841                              "Error while decoding pulse parameters. {}: {}",
842                              current, e.getLocalizedMessage());
843                  }
844                  pos++;
845                  current = new StringBuilder();
846              } else if (buffer.charAt(pos) == '-') {
847                  pos++;
848                  current.append(buffer.charAt(pos));
849                  pos++;
850              } else {
851                  current.append(buffer.charAt(pos));
852                  pos++;
853              }
854          }
855          buffer.delete(0, pos);
856          if (current.length() > 0) {
857              try {
858                  decoded.add(URLDecoder.decode(current.toString(), "utf-8"));
859              } catch (UnsupportedEncodingException e) {
860                  LOGGER.warn("Error while decoding pulse parameters. {}: {}",
861                          current, e.getLocalizedMessage());
862              }
863          }
864          return decoded;
865      }
866  
867      /**
868       * generates the token for the current command based on the given secret.
869       * 
870       * @param secret
871       *            the secret
872       * @return the token for the current command
873       */
874      private String generateToken(final byte[] secret) {
875          byte[] tokenBase = new byte[secret.length + 4];
876          System.arraycopy(secret, 0, tokenBase, 0, secret.length);
877  
878          /* add the hash code */
879          System.arraycopy(ByteUtils.getBytes(hashCode()), 0, tokenBase,
880                  secret.length, 4);
881  
882          try {
883              String token = StringUtils.digest62(tokenBase);
884              if (token.charAt(0) == '-') {
885                  return token.substring(1);
886              } else {
887                  return token;
888              }
889          } catch (NoSuchAlgorithmException e) {
890              throw new PulseException("Error generating command token: "
891                      + e.getLocalizedMessage(), e);
892          }
893      }
894  
895      /**
896       * checks whether the current {@code Command} requires a token and if the
897       * token is valid.
898       * 
899       * @param session
900       *            the current session to access the secret
901       * @return {@code false}, if and only if, the current {@code Command} is
902       *         requiring a token for execution and the given token is wrong.
903       *         Otherwise {@code true}.
904       */
905      public boolean isTokenValid(final ServiceSession session) {
906          if ((this.bundle == null) || (this.action == null)) {
907              return false;
908          }
909          if (Lifecycle.getBundle(this.bundle).getActionsRequiringTokens()
910                  .contains(this.action)) {
911              if (this.tokenValue == null) {
912                  LOGGER.warn("No token in Command for "
913                          + "an Action requiring a token.");
914                  return false;
915              }
916              String computedToken = generateToken(session.getSecret());
917              if (!this.tokenValue.equals(computedToken)) {
918                  LOGGER.warn("Token mismatch (expected: {} / actual: {}.",
919                          computedToken, this.tokenValue);
920                  return false;
921              }
922          }
923          return true;
924      }
925  
926      /**
927       * returns the current security setting of the command.
928       * 
929       * @return the current security setting of the command
930       */
931      public Security getSecurity() {
932          return this.security;
933      }
934  
935      /**
936       * sets the level of security to be used when generating the command.
937       * 
938       * @param s
939       *            the level of security to be used when generating the command.
940       * @return the current command for method chaining
941       */
942      public Command setSecurity(final Security s) {
943          this.security = s;
944          return this;
945      }
946  
947      /**
948       * @see org.torweg.pulse.service.request.Command#getLocale()
949       * @return the locale of the command.
950       */
951      public Locale getLocale() {
952          return this.locale;
953      }
954  
955      /**
956       * sets the {@code Locale} of the {@code Command}.
957       * 
958       * @param loc
959       *            the locale to set
960       * @return the modified command
961       */
962      public Command setLocale(final Locale loc) {
963          this.locale = loc;
964          return this;
965      }
966  
967      /**
968       * set the locale for the Command from a locale string.
969       * 
970       * @param localeString
971       *            the locale string
972       * @return the command
973       */
974      public Command setLocaleFormString(final String localeString) {
975          this.locale = LocaleManager.getInstance().findBestMatchingLocale(
976                  localeString, null);
977          return this;
978      }
979  
980      /**
981       * @see org.torweg.pulse.service.request.Command#getBundle()
982       * @return the application of the command
983       */
984      public String getBundle() {
985          return this.bundle;
986      }
987  
988      /**
989       * sets the name of the {@code Bundle}.
990       * 
991       * @param name
992       *            the name of the {@code Bundle} to set
993       * @return the modified command
994       */
995      public Command setBundle(final String name) {
996          this.bundle = name;
997          return this;
998      }
999  
1000     /**
1001      * @see org.torweg.pulse.service.request.Command#getAction()
1002      * @return the action of the command
1003      */
1004     public String getAction() {
1005         return this.action;
1006     }
1007 
1008     /**
1009      * <strong>Use for testing only</strong>.
1010      * 
1011      * @param act
1012      *            the action to set
1013      * @return the modified command
1014      */
1015     public Command setAction(final String act) {
1016         this.action = act;
1017         return this;
1018     }
1019 
1020     /**
1021      * @return the ID of the {@code SitemapNode} reference by the Command or
1022      *         {@code null}, if no {@code SitemapNode} is referenced.
1023      */
1024     public Long getSitemapID() {
1025         return this.sitemapID;
1026     }
1027 
1028     /**
1029      * sets the id of the {@code SitemapNode} referenced by the {@code Command}.
1030      * 
1031      * @param id
1032      *            the id of the {@code SitemapNode} referenced by the
1033      *            {@code Command} or {@code null}, if no {@code SitemapNode} is
1034      *            referenced.
1035      * @return the modified command
1036      */
1037     public Command setSitemapID(final Long id) {
1038         this.sitemapID = id;
1039         return this;
1040     }
1041 
1042     /**
1043      * returns the pulse parameters of the {@code Command}.
1044      * 
1045      * @return the pulse parameters of the {@code Command}
1046      */
1047     public List<Parameter> getPulseParameters() {
1048         return new ArrayList<Parameter>(this.pulseParameters.values());
1049     }
1050 
1051     /**
1052      * sets the {@code Parameter}s of the {@code Command}.
1053      * 
1054      * @param pp
1055      *            the pulse parameters to set
1056      * @return the modified command
1057      */
1058     public Command setPulseParameters(final Map<String, Parameter> pp) {
1059         this.pulseParameters.clear();
1060         this.pulseParameters.putAll(pp);
1061         return this;
1062     }
1063 
1064     /**
1065      * returns the HTTP parameters of the {@code Command}.
1066      * 
1067      * @return the HTTP parameters of the {@code Command}
1068      */
1069     public List<Parameter> getHttpParameters() {
1070         return new ArrayList<Parameter>(this.httpParameters.values());
1071     }
1072 
1073     /**
1074      * sets the HttpParameters of the {@code Command}.
1075      * 
1076      * @param hp
1077      *            the HTTP parameters to set
1078      * @return the modified command
1079      */
1080     public Command setHttpParameters(final Map<String, Parameter> hp) {
1081         this.httpParameters.clear();
1082         this.httpParameters.putAll(hp);
1083         return this;
1084     }
1085 
1086     /**
1087      * @see org.torweg.pulse.service.request.Command#getParameter(java.lang.String)
1088      * @param name
1089      *            the name of the parameter to fetch
1090      * @return the named parameter of the command or null, if no such parameter
1091      *         exists
1092      */
1093     public Parameter getParameter(final String name) {
1094         Parameter toReturn = this.httpParameters.get(name);
1095         if (toReturn == null) {
1096             toReturn = this.pulseParameters.get(name);
1097         }
1098         return toReturn;
1099     }
1100 
1101     /**
1102      * @see org.torweg.pulse.service.request.Command#getParameterNames()
1103      * @return a list of all parameter names of the command
1104      */
1105     public List<String> getParameterNames() {
1106         ArrayList<String> params = new ArrayList<String>(
1107                 this.httpParameters.keySet());
1108         params.addAll(this.pulseParameters.keySet());
1109         return params;
1110     }
1111 
1112     /**
1113      * @see org.torweg.pulse.service.request.Command#getParameters()
1114      * @return a list of all parameters of the command
1115      */
1116     public Set<Parameter> getParameters() {
1117         Set<Parameter> params = new HashSet<Parameter>(
1118                 this.httpParameters.size() + this.pulseParameters.size());
1119         params.addAll(this.httpParameters.values());
1120         params.addAll(this.pulseParameters.values());
1121         return params;
1122     }
1123 
1124     /**
1125      * For JAXB only.
1126      * 
1127      * @return the parameters of the {@code Command}
1128      */
1129     @XmlElementWrapper(name = "parameters")
1130     @XmlElement(name = "parameter")
1131     protected Set<Parameter> getParametersJAXB() {
1132         return getParameters();
1133     }
1134 
1135     /**
1136      * adds a {@code Parameter} to the {@code Command}.
1137      * 
1138      * @param name
1139      *            the name
1140      * @param values
1141      *            the values
1142      * @return the {@code Command}
1143      */
1144     public Command addPulseParameter(final String name,
1145             final List<String> values) {
1146         this.pulseParameters.put(name, new Parameter(name, values));
1147         return this;
1148     }
1149 
1150     /**
1151      * adds an HTTP Parameter to the {@code Command}.
1152      * 
1153      * @param name
1154      *            the name
1155      * @param value
1156      *            the value
1157      * @return the {@code Command}
1158      */
1159     public Command addPulseParameter(final String name, final String value) {
1160         this.pulseParameters.put(name, new Parameter(name, value));
1161         return this;
1162     }
1163 
1164     /**
1165      * adds an HTTP Parameter to the {@code Command}.
1166      * 
1167      * @param p
1168      *            the parameter
1169      * @return the {@code Command}
1170      */
1171     public Command addPulseParameter(final Parameter p) {
1172         this.pulseParameters.put(p.getName(), p);
1173         return this;
1174     }
1175 
1176     /**
1177      * gets a pulse parameter by name.
1178      * 
1179      * @param name
1180      *            the name of the parameter
1181      * @return the parameter
1182      */
1183     public Parameter getPulseParameter(final String name) {
1184         return this.pulseParameters.get(name);
1185     }
1186 
1187     /**
1188      * removes a pulse parameter by name.
1189      * 
1190      * @param name
1191      *            the name of the parameter
1192      * @return the {@code Command}
1193      */
1194     public Command removePulseParameter(final String name) {
1195         this.pulseParameters.remove(name);
1196         return this;
1197     }
1198 
1199     /**
1200      * adds an HTTP Parameter to the {@code Command}.
1201      * 
1202      * @param name
1203      *            the name
1204      * @param values
1205      *            the values
1206      * @return the {@code Command}
1207      */
1208     public Command addHttpParameter(final String name, final List<String> values) {
1209         this.httpParameters.put(name, new Parameter(name, values));
1210         return this;
1211     }
1212 
1213     /**
1214      * adds an HTTP Parameter to the {@code Command}.
1215      * 
1216      * @param name
1217      *            the name
1218      * @param value
1219      *            the value
1220      * @return the {@code Command}
1221      */
1222     public Command addHttpParameter(final String name, final String value) {
1223         this.httpParameters.put(name, new Parameter(name, value));
1224         return this;
1225     }
1226 
1227     /**
1228      * adds an HTTP Parameter to the {@code Command}.
1229      * 
1230      * @param p
1231      *            the parameter
1232      * @return the {@code Command}
1233      */
1234     public Command addHttpParameter(final Parameter p) {
1235         this.httpParameters.put(p.getName(), p);
1236         return this;
1237     }
1238 
1239     /**
1240      * gets a pulse parameter by name.
1241      * 
1242      * @param name
1243      *            the name of the parameter
1244      * @return the parameter
1245      */
1246     public Parameter getHttpParameter(final String name) {
1247         return this.httpParameters.get(name);
1248     }
1249 
1250     /**
1251      * removes a pulse parameter by name.
1252      * 
1253      * @param name
1254      *            the name of the parameter
1255      * @return the {@code Command}
1256      */
1257     public Command removeHttpParameter(final String name) {
1258         this.httpParameters.remove(name);
1259         return this;
1260     }
1261 
1262     /**
1263      * Removes the {@code Parameter} parameter from the {@code Command}.
1264      * 
1265      * @param parameter
1266      *            the {@code Parameter} to be removed from the {@code Command}
1267      * @return the {@code Command}
1268      */
1269     public Command removeParameter(final Parameter parameter) {
1270         if (parameter != null) {
1271             this.httpParameters.remove(parameter.getName());
1272             this.pulseParameters.remove(parameter.getName());
1273         }
1274         return this;
1275     }
1276 
1277     /**
1278      * removes the {@code Parameter} with the given name from the
1279      * {@code Command}.
1280      * 
1281      * @param name
1282      *            the name of the {@code Parameter} to be removed
1283      * @return the modified {@code Command}
1284      */
1285     public Command removeParameter(final String name) {
1286         if (name != null) {
1287             this.httpParameters.remove(name);
1288             this.pulseParameters.remove(name);
1289         }
1290         return this;
1291     }
1292 
1293     /**
1294      * clears all parameters of the {@code Command}.
1295      * 
1296      * @return the {@code Command}
1297      */
1298     public Command clearParameters() {
1299         this.httpParameters.clear();
1300         this.pulseParameters.clear();
1301         return this;
1302     }
1303 
1304     /**
1305      * returns the suffix that is added after the parsable command URI.
1306      * 
1307      * @return the suffix that is added after the parsable command URI
1308      */
1309     public String getSuffix() {
1310         return this.suffix;
1311     }
1312 
1313     /**
1314      * sets the suffix that is added after the parsable command URI (default:
1315      * {@code index.html}).
1316      * 
1317      * @param s
1318      *            the suffix to be added
1319      * @return this command for method chaining
1320      */
1321     public Command setSuffix(final String s) {
1322         this.suffix = s;
1323         return this;
1324     }
1325 
1326     /**
1327      * @param newLocale
1328      *            the {@code Locale} for the new command
1329      * @param newBundle
1330      *            the application of the new command
1331      * @param newAction
1332      *            the action of the new command
1333      * @param params
1334      *            the parameters to add to the new command
1335      * @return the new command
1336      */
1337     public Command newInstance(final Locale newLocale, final String newBundle,
1338             final String newAction, final Set<Parameter> params) {
1339         Command newCommand = new Command();
1340         newCommand.locale = newLocale;
1341         newCommand.action = newAction;
1342         newCommand.bundle = newBundle;
1343         for (Parameter p : params) {
1344             newCommand.pulseParameters.put(p.getName(), p);
1345         }
1346 
1347         return newCommand;
1348     }
1349 
1350     /**
1351      * @param newLocale
1352      *            the {@code Locale} for the new command
1353      * @param newBundle
1354      *            the application of the new command
1355      * @param newAction
1356      *            the action of the new command
1357      * @param pulseParams
1358      *            the pulse parameters to add to the new command
1359      * @param httpParams
1360      *            the HTTP parameters to add to the new command
1361      * @return the new command
1362      */
1363     public Command newInstance(final Locale newLocale, final String newBundle,
1364             final String newAction, final List<Parameter> pulseParams,
1365             final List<Parameter> httpParams) {
1366         Command newCommand = new Command();
1367         newCommand.locale = newLocale;
1368         newCommand.action = newAction;
1369         newCommand.bundle = newBundle;
1370         for (Parameter p : pulseParams) {
1371             newCommand.pulseParameters.put(p.getName(), p);
1372         }
1373         for (Parameter p : httpParams) {
1374             newCommand.httpParameters.put(p.getName(), p);
1375         }
1376 
1377         return newCommand;
1378     }
1379 
1380     /**
1381      * @param request
1382      *            the service request
1383      * @return the URL representing the command with the current locale
1384      */
1385     public String toCommandURL(final ServiceRequest request) {
1386         return toCommandURL(request, this.locale);
1387     }
1388 
1389     /**
1390      * @param request
1391      *            the service request
1392      * @param localeParam
1393      *            the local for the command
1394      * @return the URL representing the command with the given locale
1395      */
1396     public String toCommandURL(final ServiceRequest request,
1397             final Locale localeParam) {
1398         StringBuilder commandURL = new StringBuilder();
1399         URI baseURI = null;
1400         try {
1401             baseURI = new URI(toCommandURL(request.getBaseURI(), localeParam,
1402                     request.getSession()).toString());
1403         } catch (URISyntaxException e) {
1404             throw new PulseException(e);
1405         }
1406 
1407         /* apply required security for action */
1408         considerActionConfiguration();
1409 
1410         /* consider security settings */
1411         Protocol protocol;
1412         if (baseURI.getHost() != null) {
1413             if (this.security.equals(Security.NEVER)
1414                     && Lifecycle.getWeakestCommandSecurityLevel().equals(
1415                             Security.NEVER)) {
1416                 protocol = Protocol.HTTP;
1417             } else if ((this.security.equals(Security.ALWAYS) || Lifecycle
1418                     .getWeakestCommandSecurityLevel().equals(Security.ALWAYS))
1419                     && Lifecycle.isTransportLayerSecurityAvailable()) {
1420                 protocol = Protocol.HTTPS;
1421             } else {
1422                 protocol = Protocol.fromScheme(baseURI);
1423             }
1424             if (Protocol.HTTP == protocol) {
1425                 commandURL.append("http://").append(baseURI.getHost());
1426                 if (Lifecycle.getDefaultPort() != 80) {
1427                     commandURL.append(':').append(Lifecycle.getDefaultPort());
1428                 }
1429             } else if (Protocol.HTTPS == protocol) {
1430                 commandURL.append("https://").append(baseURI.getHost());
1431                 if (Lifecycle.getSecurePort() != 443) {
1432                     commandURL.append(':').append(Lifecycle.getSecurePort());
1433                 }
1434             }
1435         }
1436 
1437         commandURL.append(baseURI.getPath());
1438         if (baseURI.getFragment() != null) {
1439             commandURL.append('#').append(baseURI.getFragment());
1440         }
1441         if (baseURI.getQuery() != null) {
1442             commandURL.append('?').append(baseURI.getQuery());
1443         }
1444 
1445         if ((request.getHttpServletResponse() != null)
1446                 && (request.getSessionMode() == SessionMode.USE_URL_ENCODED)) {
1447             return request.getHttpServletResponse().encodeURL(
1448                     commandURL.toString());
1449         } else {
1450             return commandURL.toString();
1451         }
1452     }
1453 
1454     /**
1455      * tries to determine the right security level according to the
1456      * {@code ActionConfiguration} matching the {@code Command}.
1457      */
1458     private void considerActionConfiguration() {
1459         if ((this.bundle != null) && (this.action != null)) {
1460             try {
1461                 ActionConfiguration actionConf = Lifecycle.getBundle(
1462                         this.bundle).getActionConfiguration(this.action);
1463                 if (actionConf == null) {
1464                     LOGGER.debug(
1465                             "Could not find action configuration for bundle '{}', action '{}'.",
1466                             this.bundle, this.action);
1467                 } else {
1468                     Security sec = actionConf.getSecurity();
1469                     if (sec.overrides(this.security)) {
1470                         this.security = sec;
1471                     }
1472                 }
1473             } catch (Exception e) {
1474                 LOGGER.warn("Exception while trying to "
1475                         + "determine security settings for " + toString() + ":"
1476                         + e.getLocalizedMessage(), e);
1477             }
1478         }
1479     }
1480 
1481     /**
1482      * <strong>This method cannot consider the security set for the
1483      * {@code Command}!</strong>
1484      * 
1485      * @see org.torweg.pulse.service.request.Command#toCommandURL(org.torweg.pulse.service.request.ServiceRequest,
1486      *      java.util.Locale)
1487      * @param baseURI
1488      *            the base URI
1489      * @param localeParam
1490      *            the local for the command
1491      * @param session
1492      *            the session for token generation or {@code null}
1493      * @return a string representing the command with the given locale
1494      */
1495     private StringBuilder toCommandURL(final String baseURI,
1496             final Locale localeParam, final ServiceSession session) {
1497 
1498         StringBuilder commandURL = new StringBuilder(baseURI);
1499 
1500         try {
1501             commandURL.append('/').append(
1502                     URLEncoder.encode(
1503                             generateParsableCommandURI(localeParam, session),
1504                             "utf-8"));
1505             commandURL.append('/').append(
1506                     URLEncoder.encode(this.suffix, "utf-8"));
1507         } catch (UnsupportedEncodingException e) {
1508             throw new PulseException("The encoding UTF-8 is not supported. "
1509                     + "This is a fatal error: " + e.getLocalizedMessage(), e);
1510         }
1511 
1512         if (this.httpParameters.size() > 0) {
1513             commandURL.append('?');
1514         }
1515 
1516         boolean addAmpersand = false;
1517         for (Parameter p : this.httpParameters.values()) {
1518             String name = p.getName();
1519             for (String value : p.getValues()) {
1520                 if (addAmpersand) {
1521                     commandURL.append('&');
1522                 }
1523                 try {
1524                     commandURL.append(URLEncoder.encode(name, "UTF-8"))
1525                             .append('=')
1526                             .append(URLEncoder.encode(value, "UTF-8"));
1527                 } catch (UnsupportedEncodingException e) {
1528                     throw new PulseException(
1529                             "The encoding UTF-8 is not supported. "
1530                                     + "This is a fatal error: "
1531                                     + e.getLocalizedMessage(), e);
1532                 }
1533                 addAmpersand = true;
1534             }
1535             addAmpersand = true;
1536         }
1537 
1538         return commandURL;
1539     }
1540 
1541     /**
1542      * is used by both {@code toSimpleString()} and {@code toString()} to create
1543      * a string representing the {@code Command} without its parameters.
1544      * 
1545      * @return a {@code StringBuilder} representing the {@code Command} without
1546      *         its parameters
1547      */
1548     private StringBuilder buildBaseStringBuilder() {
1549         StringBuilder str = new StringBuilder();
1550         if (this.locale != null) {
1551             str.append(this.locale.toString());
1552         } else {
1553             str.append("unset");
1554         }
1555         str.append(':').append(this.bundle).append(':').append(this.action)
1556                 .append(':');
1557         if (this.sitemapID != null) {
1558             str.append(this.sitemapID);
1559         }
1560         return str.append(':');
1561     }
1562 
1563     /**
1564      * @return Returns a String representation of the Command.
1565      */
1566     @Override
1567     public String toString() {
1568         StringBuilder str = buildBaseStringBuilder();
1569         List<Parameter> parameters = new ArrayList<Parameter>(
1570                 this.pulseParameters.values());
1571         debugParameterList(str, parameters);
1572         if (this.includeHTTPParametersInToString || LOGGER.isTraceEnabled()) {
1573             parameters = new ArrayList<Parameter>(this.httpParameters.values());
1574             debugParameterList(str, parameters);
1575         } else if (this.httpParameters.size() > 0) {
1576             str.append("({HTTP-parmeters-omitted})");
1577         }
1578         return str.toString();
1579     }
1580 
1581     /**
1582      * returns the hash code for the {@code Command}.
1583      * 
1584      * @return the hash code
1585      */
1586     @Override
1587     public int hashCode() {
1588         // CHECKSTYLE:OFF
1589         final int prime = 31; // NOPMD
1590         int result = 1;
1591         result = prime * result
1592                 + ((this.action == null) ? 0 : this.action.hashCode());
1593         result = prime * result
1594                 + ((this.bundle == null) ? 0 : this.bundle.hashCode());
1595         result = prime * result
1596                 + ((this.locale == null) ? 0 : this.locale.hashCode());
1597         result = prime * result
1598                 + ((this.sitemapID == null) ? 0 : this.sitemapID.hashCode());
1599         for (Map.Entry<String, Parameter> entry : this.pulseParameters
1600                 .entrySet()) {
1601             result = prime * result + entry.getValue().hashCode();
1602         }
1603         return result;
1604         // CHECKSTYLE:ON
1605     }
1606 
1607     /**
1608      * returns the ETag value for this command.
1609      * 
1610      * @return the ETag value
1611      */
1612     public long getETagValue() {
1613         // CHECKSTYLE:OFF
1614         final long prime = 31; // NOPMD
1615         long result = 1;
1616         result = prime * result
1617                 + ((this.action == null) ? 0 : this.action.hashCode());
1618         result = prime * result
1619                 + ((this.bundle == null) ? 0 : this.bundle.hashCode());
1620         result = prime * result
1621                 + ((this.locale == null) ? 0 : this.locale.hashCode());
1622         result = prime * result
1623                 + ((this.sitemapID == null) ? 0 : this.sitemapID.hashCode());
1624         for (Map.Entry<String, Parameter> entry : this.pulseParameters
1625                 .entrySet()) {
1626             result = prime * result + entry.getValue().hashCode();
1627         }
1628         for (Map.Entry<String, Parameter> entry : this.httpParameters
1629                 .entrySet()) {
1630             result = prime * result + entry.getValue().hashCode();
1631         }
1632         return result;
1633         // CHECKSTYLE:ON
1634     }
1635 
1636     /**
1637      * returns whether the given object is equal to the {@code Command}.
1638      * <p>
1639      * The given object is considered equal, if and only if, it is also a
1640      * {@code Command} with the same locale, bundle, action, sitemap ID and
1641      * parameters.
1642      * </p>
1643      * 
1644      * @param obj
1645      *            the object to compare against
1646      * @return {@code true}, if and only if, the given object is equal to this
1647      *         command. Otherwise {@code false}.
1648      */
1649     // CHECKSTYLE:OFF
1650     @Override
1651     public boolean equals(final Object obj) {
1652         // CHECKSTYLE:ON
1653         if (this == obj) {
1654             return true;
1655         }
1656         if (obj == null) {
1657             return false;
1658         }
1659         if (getClass() != obj.getClass()) {
1660             return false;
1661         }
1662         Command other = (Command) obj;
1663         if (this.action == null) {
1664             if (other.action != null) {
1665                 return false;
1666             }
1667         } else if (!this.action.equals(other.action)) {
1668             return false;
1669         }
1670         if (this.bundle == null) {
1671             if (other.bundle != null) {
1672                 return false;
1673             }
1674         } else if (!this.bundle.equals(other.bundle)) {
1675             return false;
1676         }
1677         if (this.locale == null) {
1678             if (other.locale != null) {
1679                 return false;
1680             }
1681         } else if (!this.locale.equals(other.locale)) {
1682             return false;
1683         }
1684 
1685         if (this.sitemapID == null) {
1686             if (other.sitemapID != null) {
1687                 return false;
1688             }
1689         } else if (!this.sitemapID.equals(other.sitemapID)) {
1690             return false;
1691         }
1692 
1693         if (!getParameters().equals(other.getParameters())) {
1694             return false;
1695         }
1696         return true;
1697     }
1698 
1699     /**
1700      * @return a JDOM {@code Element} representing the {@code Command} .
1701      */
1702     public Element deserializeToJDOM() {
1703         Element command = new Element("command");
1704         command.setAttribute("locale", getLocale().toString());
1705         command.setAttribute("bundle", getBundle());
1706         command.setAttribute("action", getAction());
1707         if (getSitemapID() != null) {
1708             command.setAttribute("sitemapID", getSitemapID().toString());
1709         }
1710 
1711         Element pulseParams = new Element("pulse-parameters");
1712         for (Parameter p : this.pulseParameters.values()) {
1713             if (p.getName().length() > 0) {
1714                 pulseParams.addContent(p.deserializeToJDOM());
1715             }
1716         }
1717         command.addContent(pulseParams);
1718 
1719         Element httpParams = new Element("http-parameters");
1720         for (Parameter p : this.httpParameters.values()) {
1721             httpParams.addContent(p.deserializeToJDOM());
1722         }
1723         command.addContent(httpParams);
1724 
1725         return command;
1726     }
1727 
1728     /**
1729      * returns a deep copy of the {@code Command}.
1730      * 
1731      * @return a deep copy of the {@code Command}
1732      */
1733     public Command createCopy() {
1734         Command command = createCopy(false);
1735         command.pulseParameters.putAll(this.pulseParameters);
1736         command.httpParameters.putAll(this.httpParameters);
1737         return command;
1738     }
1739 
1740     /**
1741      * returns a copy of the {@code Command} either with or without its
1742      * parameters.
1743      * 
1744      * @param withParams
1745      *            a flag indicating whether or not to copy the parameters as
1746      *            well
1747      * @return a copy of the {@code Command} either with or without its
1748      *         parameters
1749      */
1750     public Command createCopy(final boolean withParams) {
1751         if (!withParams) {
1752             Command command = new Command();
1753             command.bundle = this.bundle;
1754             command.action = this.action;
1755             command.locale = this.locale;
1756             command.sitemapID = this.sitemapID;
1757             command.suffix = this.suffix;
1758             return command;
1759         }
1760         return createCopy();
1761     }
1762 
1763     /**
1764      * utility method to build a {@code Command} from a parsable fragment.
1765      * 
1766      * @param parsable
1767      *            the fragment
1768      * @param sr
1769      *            the current request
1770      * @return the {@code Command}
1771      */
1772     public static Command fromParsable(final String parsable,
1773             final ServiceRequest sr) {
1774         Command c = new Command();
1775         c.processParsableURIFragment(new StringBuilder(parsable), sr);
1776         return c;
1777     }
1778 
1779     /**
1780      * shorthand for the protocol to be used.
1781      */
1782     private enum Protocol {
1783         /**
1784          * HTTP.
1785          */
1786         HTTP,
1787 
1788         /**
1789          * HTTPS.
1790          */
1791         HTTPS;
1792 
1793         /**
1794          * @param uri
1795          *            the URI which scheme will determine the protocol.
1796          * @return the {@code Protocol}
1797          */
1798         protected static Protocol fromScheme(final URI uri) {
1799             if (uri.getScheme().equalsIgnoreCase("http")) {
1800                 return HTTP;
1801             } else if (uri.getScheme().equalsIgnoreCase("https")) {
1802                 return HTTPS;
1803             }
1804             return null;
1805         }
1806     }
1807 
1808 }
1809