View Javadoc
1   /*
2    * This file is part of ***  M y C o R e  ***
3    * See http://www.mycore.de/ for details.
4    *
5    * MyCoRe is free software: you can redistribute it and/or modify
6    * it under the terms of the GNU General Public License as published by
7    * the Free Software Foundation, either version 3 of the License, or
8    * (at your option) any later version.
9    *
10   * MyCoRe is distributed in the hope that it will be useful,
11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   * GNU General Public License for more details.
14   *
15   * You should have received a copy of the GNU General Public License
16   * along with MyCoRe.  If not, see <http://www.gnu.org/licenses/>.
17   */
18  
19  package org.mycore.frontend.servlets;
20  
21  import static org.mycore.frontend.MCRFrontendUtil.BASE_URL_ATTRIBUTE;
22  
23  import java.io.IOException;
24  import java.net.MalformedURLException;
25  import java.net.URL;
26  import java.net.URLEncoder;
27  import java.net.UnknownHostException;
28  import java.nio.charset.StandardCharsets;
29  import java.text.MessageFormat;
30  import java.time.Instant;
31  import java.time.LocalDateTime;
32  import java.time.ZoneId;
33  import java.util.Enumeration;
34  import java.util.List;
35  import java.util.Locale;
36  import java.util.Optional;
37  import java.util.Properties;
38  
39  import javax.xml.transform.TransformerException;
40  
41  import org.apache.logging.log4j.LogManager;
42  import org.apache.logging.log4j.Logger;
43  import org.mycore.common.MCRException;
44  import org.mycore.common.MCRSession;
45  import org.mycore.common.MCRSessionMgr;
46  import org.mycore.common.MCRSessionResolver;
47  import org.mycore.common.MCRTransactionHelper;
48  import org.mycore.common.config.MCRConfiguration2;
49  import org.mycore.common.config.MCRConfigurationBase;
50  import org.mycore.common.config.MCRConfigurationDirSetup;
51  import org.mycore.common.xml.MCRLayoutService;
52  import org.mycore.common.xsl.MCRErrorListener;
53  import org.mycore.frontend.MCRFrontendUtil;
54  import org.mycore.services.i18n.MCRTranslation;
55  import org.xml.sax.SAXException;
56  import org.xml.sax.SAXParseException;
57  
58  import jakarta.servlet.ServletContext;
59  import jakarta.servlet.ServletException;
60  import jakarta.servlet.http.HttpServlet;
61  import jakarta.servlet.http.HttpServletRequest;
62  import jakarta.servlet.http.HttpServletResponse;
63  import jakarta.servlet.http.HttpSession;
64  
65  /**
66   * This is the superclass of all MyCoRe servlets. It provides helper methods for logging and managing the current
67   * session data. Part of the code has been taken from MilessServlet.java written by Frank Lützenkirchen.
68   * 
69   * @author Detlev Degenhardt
70   * @author Frank Lützenkirchen
71   * @author Thomas Scheffler (yagee)
72   * @version $Revision$ $Date$
73   */
74  public class MCRServlet extends HttpServlet {
75      public static final String ATTR_MYCORE_SESSION = "mycore.session";
76  
77      public static final String CURRENT_THREAD_NAME_KEY = "currentThreadName";
78  
79      public static final String INITIAL_SERVLET_NAME_KEY = "currentServletName";
80  
81      private static final long serialVersionUID = 1L;
82  
83      private static Logger LOGGER = LogManager.getLogger();
84  
85      private static String SERVLET_URL;
86  
87      private static final boolean ENABLE_BROWSER_CACHE = MCRConfiguration2.getBoolean("MCR.Servlet.BrowserCache.enable")
88          .orElse(false);
89  
90      private static MCRLayoutService LAYOUT_SERVICE;
91  
92      private static String ACCESS_CONTROL_ALLOW_ORIGIN = "Access-Control-Allow-Origin";
93  
94      public static MCRLayoutService getLayoutService() {
95          return LAYOUT_SERVICE;
96      }
97  
98      @Override
99      public void init() throws ServletException {
100         super.init();
101         if (LAYOUT_SERVICE == null) {
102             LAYOUT_SERVICE = MCRLayoutService.instance();
103         }
104     }
105 
106     /**
107      * Returns the servlet base URL of the mycore system
108      **/
109     public static String getServletBaseURL() {
110         MCRSession session = MCRSessionMgr.getCurrentSession();
111         Object value = session.get(BASE_URL_ATTRIBUTE);
112         if (value != null) {
113             LOGGER.debug("Returning BaseURL {}servlets/ from user session.", value);
114             return value + "servlets/";
115         }
116         return SERVLET_URL != null ? SERVLET_URL : MCRFrontendUtil.getBaseURL() + "servlets/";
117     }
118 
119     /**
120      * Initialisation of the static values for the base URL and servlet URL of the mycore system.
121      */
122     private static synchronized void prepareBaseURLs(ServletContext context, HttpServletRequest req) {
123         String contextPath = req.getContextPath() + "/";
124 
125         String requestURL = req.getRequestURL().toString();
126         int pos = requestURL.indexOf(contextPath, 9);
127         String baseURLofRequest = requestURL.substring(0, pos) + contextPath;
128 
129         prepareBaseURLs(baseURLofRequest);
130     }
131 
132     private static void prepareBaseURLs(String baseURLofRequest) {
133         MCRFrontendUtil.prepareBaseURLs(baseURLofRequest);
134         SERVLET_URL = MCRFrontendUtil.getBaseURL() + "servlets/";
135     }
136 
137     // The methods doGet() and doPost() simply call the private method
138     // doGetPost(),
139     // i.e. GET- and POST requests are handled by one method only.
140     @Override
141     public void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
142         try {
143             doGetPost(req, res);
144         } catch (SAXException | TransformerException e) {
145             throwIOException(e);
146         }
147     }
148 
149     private void throwIOException(Exception e) throws IOException {
150         if (e instanceof IOException) {
151             throw (IOException) e;
152         }
153         if (e instanceof TransformerException) {
154             TransformerException te = MCRErrorListener.unwrapException((TransformerException) e);
155             String myMessageAndLocation = MCRErrorListener.getMyMessageAndLocation(te);
156             throw new IOException("Error while XSL Transformation: " + myMessageAndLocation, e);
157         }
158         if (e instanceof SAXParseException) {
159             SAXParseException spe = (SAXParseException) e;
160             String id = spe.getSystemId() != null ? spe.getSystemId() : spe.getPublicId();
161             int line = spe.getLineNumber();
162             int column = spe.getColumnNumber();
163             String msg = new MessageFormat("Error on {0}:{1} while parsing {2}", Locale.ROOT)
164                 .format(new Object[] { line, column, id });
165             throw new IOException(msg, e);
166         }
167         throw new IOException(e);
168     }
169 
170     protected void doGet(MCRServletJob job) throws Exception {
171         doGetPost(job);
172     }
173 
174     @Override
175     public void doPost(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
176         try {
177             doGetPost(req, res);
178         } catch (SAXException | TransformerException e) {
179             throwIOException(e);
180         }
181     }
182 
183     protected void doPost(MCRServletJob job) throws Exception {
184         doGetPost(job);
185     }
186 
187     public static MCRSession getSession(HttpServletRequest req) {
188         boolean reusedSession = req.isRequestedSessionIdValid();
189         HttpSession theSession = req.getSession(true);
190         if (reusedSession) {
191             LOGGER.debug(() -> "Reused HTTP session: " + theSession.getId() + ", created: " + LocalDateTime
192                 .ofInstant(Instant.ofEpochMilli(theSession.getCreationTime()), ZoneId.systemDefault()));
193         } else {
194             LOGGER.info(() -> "Created new HTTP session: " + theSession.getId());
195         }
196         MCRSession session = null;
197 
198         MCRSession fromHttpSession = Optional
199             .ofNullable((MCRSessionResolver) theSession.getAttribute(ATTR_MYCORE_SESSION))
200             .flatMap(MCRSessionResolver::resolveSession)
201             .orElse(null);
202 
203         MCRSessionMgr.unlock();
204         if (fromHttpSession != null && fromHttpSession.getID() != null) {
205             // Take session from HttpSession with servlets
206             session = fromHttpSession;
207 
208             String lastIP = session.getCurrentIP();
209             String newIP = MCRFrontendUtil.getRemoteAddr(req);
210 
211             try {
212                 if (!MCRFrontendUtil.isIPAddrAllowed(lastIP, newIP)) {
213                     LOGGER.warn("Session steal attempt from IP {}, previous IP was {}. Session: {}", newIP, lastIP,
214                         session);
215                     MCRSessionMgr.releaseCurrentSession();
216                     session.close(); //MCR-1409 do not leak old session
217                     MCRSessionMgr.unlock();//due to release above
218                     session = MCRSessionMgr.getCurrentSession();
219                     session.setCurrentIP(newIP);
220                 }
221             } catch (UnknownHostException e) {
222                 throw new MCRException("Wrong transformation of IP address for this session.", e);
223             }
224         } else {
225             // Create a new session
226             session = MCRSessionMgr.getCurrentSession();
227         }
228 
229         // Store current session in HttpSession
230         theSession.setAttribute(ATTR_MYCORE_SESSION, new MCRSessionResolver(session));
231         // store the HttpSession ID in MCRSession
232         if (session.put("http.session", theSession.getId()) == null) {
233             //first request
234             //for MCRTranslation.getAvailableLanguages()
235             MCRTransactionHelper.beginTransaction();
236             try {
237                 String acceptLanguage = req.getHeader("Accept-Language");
238                 if (acceptLanguage != null) {
239                     List<Locale.LanguageRange> languageRanges = Locale.LanguageRange.parse(acceptLanguage);
240                     LOGGER.debug("accept languages: {}", languageRanges);
241                     MCRSession finalSession = session;
242                     Optional
243                         .ofNullable(Locale.lookupTag(languageRanges, MCRTranslation.getAvailableLanguages()))
244                         .ifPresent(selectedLanguage -> {
245                             LOGGER.debug("selected language: {}", selectedLanguage);
246                             finalSession.setCurrentLanguage(selectedLanguage);
247                         });
248                 }
249             } finally {
250                 if (MCRTransactionHelper.transactionRequiresRollback()) {
251                     MCRTransactionHelper.rollbackTransaction();
252                 }
253                 MCRTransactionHelper.commitTransaction();
254             }
255         }
256         // Forward MCRSessionID to XSL Stylesheets
257         req.setAttribute("XSL.MCRSessionID", session.getID());
258 
259         return session;
260     }
261 
262     private static void bindSessionToRequest(HttpServletRequest req, String servletName, MCRSession session) {
263         if (!isSessionBoundToRequest(req)) {
264             // Bind current session to this thread:
265             MCRSessionMgr.setCurrentSession(session);
266             req.setAttribute(CURRENT_THREAD_NAME_KEY, Thread.currentThread().getName());
267             req.setAttribute(INITIAL_SERVLET_NAME_KEY, servletName);
268         }
269     }
270 
271     private static boolean isSessionBoundToRequest(HttpServletRequest req) {
272         String currentThread = getProperty(req, CURRENT_THREAD_NAME_KEY);
273         // check if this is request passed the same thread before
274         // (RequestDispatcher)
275         return currentThread != null && currentThread.equals(Thread.currentThread().getName());
276     }
277 
278     /**
279      * This private method handles both GET and POST requests and is invoked by doGet() and doPost().
280      * 
281      * @param req
282      *            the HTTP request instance
283      * @param res
284      *            the HTTP response instance
285      * @exception IOException
286      *                for java I/O errors.
287      * @exception ServletException
288      *                for errors from the servlet engine.
289      */
290     private void doGetPost(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException,
291         SAXException, TransformerException {
292         initializeMCRSession(req, getServletName());
293 
294         if (SERVLET_URL == null) {
295             prepareBaseURLs(getServletContext(), req);
296         }
297 
298         MCRServletJob job = new MCRServletJob(req, res);
299         MCRSession session = MCRSessionMgr.getCurrentSession();
300 
301         try {
302             // transaction around 1st phase of request
303             Exception thinkException = processThinkPhase(job);
304             // first phase completed, start rendering phase
305             processRenderingPhase(job, thinkException);
306         } catch (Error error) {
307             if (getProperty(req, INITIAL_SERVLET_NAME_KEY).equals(getServletName())) {
308                 // current Servlet not called via RequestDispatcher
309                 MCRTransactionHelper.rollbackTransaction();
310             }
311             throw error;
312         } catch (Exception ex) {
313             if (getProperty(req, INITIAL_SERVLET_NAME_KEY).equals(getServletName())) {
314                 // current Servlet not called via RequestDispatcher
315                 MCRTransactionHelper.rollbackTransaction();
316             }
317             if (isBrokenPipe(ex)) {
318                 LOGGER.info("Ignore broken pipe.");
319                 return;
320             }
321             if (ex.getMessage() == null) {
322                 LOGGER.error("Exception while in rendering phase.", ex);
323             } else {
324                 LOGGER.error("Exception while in rendering phase: {}", ex.getMessage());
325             }
326             if (ex instanceof ServletException) {
327                 throw (ServletException) ex;
328             } else if (ex instanceof IOException) {
329                 throw (IOException) ex;
330             } else if (ex instanceof SAXException) {
331                 throw (SAXException) ex;
332             } else if (ex instanceof TransformerException) {
333                 throw (TransformerException) ex;
334             } else if (ex instanceof RuntimeException) {
335                 throw (RuntimeException) ex;
336             } else {
337                 throw new RuntimeException(ex);
338             }
339         } finally {
340             cleanupMCRSession(req, getServletName());
341         }
342     }
343 
344     /**
345      * Code to initialize a MyCoRe Session
346      * may be reused in ServletFilter, MVC controller, etc.
347      * 
348      * @param req - the HTTP request
349      * @param servletName - the servletName
350      * @throws IOException
351      */
352     public static void initializeMCRSession(HttpServletRequest req, String servletName) throws IOException {
353         // Try to set encoding of form values
354         String reqCharEncoding = req.getCharacterEncoding();
355 
356         if (reqCharEncoding == null) {
357             // Set default to UTF-8
358             reqCharEncoding = MCRConfiguration2.getString("MCR.Request.CharEncoding").orElse("UTF-8");
359             req.setCharacterEncoding(reqCharEncoding);
360             LOGGER.debug("Setting ReqCharEncoding to: {}", reqCharEncoding);
361         }
362 
363         if ("true".equals(req.getParameter("reload.properties"))) {
364             MCRConfigurationDirSetup setup = new MCRConfigurationDirSetup();
365             setup.startUp(req.getServletContext());
366         }
367         if (getProperty(req, INITIAL_SERVLET_NAME_KEY) == null) {
368             MCRSession session = getSession(req);
369             bindSessionToRequest(req, servletName, session);
370         }
371     }
372 
373     /**
374      * Code to cleanup a MyCoRe Session
375      * may be reused in ServletFilter, MVC controller, etc. 
376      * @param req - the HTTP Request
377      * @param servletName - the Servlet name
378      */
379     public static void cleanupMCRSession(HttpServletRequest req, String servletName) {
380         // Release current MCRSession from current Thread,
381         // in case that Thread pooling will be used by servlet engine
382         if (getProperty(req, INITIAL_SERVLET_NAME_KEY).equals(servletName)) {
383             // current Servlet not called via RequestDispatcher
384             MCRSessionMgr.releaseCurrentSession();
385         }
386     }
387 
388     private static boolean isBrokenPipe(Throwable throwable) {
389         String message = throwable.getMessage();
390         if (message != null && throwable instanceof IOException && message.contains("Broken pipe")) {
391             return true;
392         }
393         return throwable.getCause() != null && isBrokenPipe(throwable.getCause());
394     }
395 
396     private void configureSession(MCRServletJob job) {
397         MCRSession session = MCRSessionMgr.getCurrentSession();
398 
399         String longName = getClass().getName();
400         final String shortName = longName.substring(longName.lastIndexOf(".") + 1);
401 
402         LOGGER.info(() -> String
403             .format(Locale.ROOT, "%s ip=%s mcr=%s path=%s", shortName, MCRFrontendUtil.getRemoteAddr(job.getRequest()),
404                 session.getID(), job.getRequest().getPathInfo()));
405 
406         MCRFrontendUtil.configureSession(session, job.getRequest(), job.getResponse());
407     }
408 
409     private Exception processThinkPhase(MCRServletJob job) {
410         MCRSession session = MCRSessionMgr.getCurrentSession();
411         try {
412             if (getProperty(job.getRequest(), INITIAL_SERVLET_NAME_KEY).equals(getServletName())) {
413                 // current Servlet not called via RequestDispatcher
414                 MCRTransactionHelper.beginTransaction();
415             }
416             configureSession(job);
417             think(job);
418             if (getProperty(job.getRequest(), INITIAL_SERVLET_NAME_KEY).equals(getServletName())) {
419                 // current Servlet not called via RequestDispatcher
420                 MCRTransactionHelper.commitTransaction();
421             }
422         } catch (Exception ex) {
423             if (getProperty(job.getRequest(), INITIAL_SERVLET_NAME_KEY).equals(getServletName())) {
424                 // current Servlet not called via RequestDispatcher
425                 LOGGER.warn("Exception occurred, performing database rollback.");
426                 MCRTransactionHelper.rollbackTransaction();
427             } else {
428                 LOGGER.warn("Exception occurred, cannot rollback database transaction right now.");
429             }
430             return ex;
431         }
432         return null;
433     }
434 
435     /**
436      * 1st phase of doGetPost. This method has a seperate transaction. Per default id does nothing as a fallback to the
437      * old behaviour.
438      * 
439      * @see #render(MCRServletJob, Exception)
440      */
441     protected void think(MCRServletJob job) throws Exception {
442         // not implemented by default
443     }
444 
445     private void processRenderingPhase(MCRServletJob job, Exception thinkException) throws Exception {
446         if (allowCrossDomainRequests() && !job.getResponse().containsHeader(ACCESS_CONTROL_ALLOW_ORIGIN)) {
447             job.getResponse().setHeader(ACCESS_CONTROL_ALLOW_ORIGIN, "*");
448         }
449         MCRSession session = MCRSessionMgr.getCurrentSession();
450         if (getProperty(job.getRequest(), INITIAL_SERVLET_NAME_KEY).equals(getServletName())) {
451             // current Servlet not called via RequestDispatcher
452             MCRTransactionHelper.beginTransaction();
453         }
454         render(job, thinkException);
455         if (getProperty(job.getRequest(), INITIAL_SERVLET_NAME_KEY).equals(getServletName())) {
456             // current Servlet not called via RequestDispatcher
457             MCRTransactionHelper.commitTransaction();
458         }
459     }
460 
461     /**
462      * Returns true if this servlet allows Cross-domain requests. The default value defined by {@link MCRServlet} is
463      * <code>false</code>.
464      */
465     protected boolean allowCrossDomainRequests() {
466         return false;
467     }
468 
469     /**
470      * 2nd phase of doGetPost This method has a seperate transaction and gets the same MCRServletJob from the first
471      * phase (think) and any exception that occurs at the first phase. By default this method calls
472      * doGetPost(MCRServletJob) as a fallback to the old behaviour.
473      * 
474      * @param job
475      *            same instance as of think(MCRServlet job)
476      * @param ex
477      *            any exception thrown by think(MCRServletJob) or transaction commit
478      * @throws Exception
479      *             if render could not handle <code>ex</code> to produce a nice user page
480      */
481     protected void render(MCRServletJob job, Exception ex) throws Exception {
482         // no info here how to handle
483         if (ex != null) {
484             throw ex;
485         }
486         if (job.getRequest().getMethod().equals("POST")) {
487             doPost(job);
488         } else {
489             doGet(job);
490         }
491     }
492 
493     /**
494      * This method should be overwritten by other servlets. As a default response we indicate the HTTP 1.1 status code
495      * 501 (Not Implemented).
496      */
497     protected void doGetPost(MCRServletJob job) throws Exception {
498         job.getResponse().sendError(HttpServletResponse.SC_NOT_IMPLEMENTED);
499     }
500 
501     /** Handles an exception by reporting it and its embedded exception */
502     protected void handleException(Exception ex) {
503         try {
504             reportException(ex);
505         } catch (Exception ignored) {
506             LOGGER.error(ignored);
507         }
508     }
509 
510     /** Reports an exception to the log */
511     protected void reportException(Exception ex) throws Exception {
512         String cname = this.getClass().getName();
513         String servlet = cname.substring(cname.lastIndexOf(".") + 1);
514 
515         LOGGER.warn("Exception caught in : {}", servlet, ex);
516     }
517 
518     /**
519      * This method builds a URL that can be used to redirect the client browser to another page, thereby including http
520      * request parameters. The request parameters will be encoded as http get request.
521      * 
522      * @param baseURL
523      *            the base url of the target webpage
524      * @param parameters
525      *            the http request parameters
526      */
527     protected String buildRedirectURL(String baseURL, Properties parameters) {
528         StringBuilder redirectURL = new StringBuilder(baseURL);
529         boolean first = true;
530         for (Enumeration<?> e = parameters.keys(); e.hasMoreElements();) {
531             if (first) {
532                 redirectURL.append("?");
533                 first = false;
534             } else {
535                 redirectURL.append("&");
536             }
537 
538             String name = (String) e.nextElement();
539             String value = null;
540             value = URLEncoder.encode(parameters.getProperty(name), StandardCharsets.UTF_8);
541 
542             redirectURL.append(name).append("=").append(value);
543         }
544         LOGGER.debug("Sending redirect to {}", redirectURL);
545         return redirectURL.toString();
546     }
547 
548     /**
549      * allows browser to cache requests. This method is usefull as it allows browsers to cache content that is not
550      * changed. Please overwrite this method in every Servlet that depends on "remote" data.
551      */
552     @Override
553     protected long getLastModified(HttpServletRequest request) {
554         if (ENABLE_BROWSER_CACHE) {
555             // we can cache every (local) request
556             long lastModified = MCRSessionMgr.getCurrentSession().getLoginTime() > MCRConfigurationBase
557                 .getSystemLastModified() ? MCRSessionMgr.getCurrentSession().getLoginTime()
558                     : MCRConfigurationBase.getSystemLastModified();
559             LOGGER.info("LastModified: {}", lastModified);
560             return lastModified;
561         }
562         return -1; // time is not known
563     }
564 
565     public static String getProperty(HttpServletRequest request, String name) {
566         return MCRFrontendUtil.getProperty(request, name).orElse(null);
567     }
568 
569     /**
570      * returns a translated error message for the current Servlet. I18N keys are of form
571      * {prefix}'.'{SimpleServletClassName}'.'{subIdentifier}
572      * 
573      * @param prefix
574      *            a prefix of the message property like component.base.error
575      * @param subIdentifier
576      *            last part of I18n key
577      * @param args
578      *            any arguments that should be passed to {@link MCRTranslation#translate(String, Object...)}
579      */
580     protected String getErrorI18N(String prefix, String subIdentifier, Object... args) {
581         String key = new MessageFormat("{0}.{1}.{2}", Locale.ROOT)
582             .format(new Object[] { prefix, getClass().getSimpleName(), subIdentifier });
583         return MCRTranslation.translate(key, args);
584     }
585 
586     /**
587      * Returns the referer of the given request.
588      */
589     protected URL getReferer(HttpServletRequest request) {
590         String referer;
591         referer = request.getHeader("Referer");
592         if (referer == null) {
593             return null;
594         }
595         try {
596             return new URL(referer);
597         } catch (MalformedURLException e) {
598             //should not happen
599             LOGGER.error("Referer is not a valid URL: {}", referer, e);
600             return null;
601         }
602     }
603 
604     /**
605      * If a referrer is available this method redirects to the url given by the referrer otherwise method redirects to
606      * the application base url.
607      */
608     protected void toReferrer(HttpServletRequest request, HttpServletResponse response) throws IOException {
609         URL referrer = getReferer(request);
610         if (referrer != null) {
611             response.sendRedirect(response.encodeRedirectURL(referrer.toString()));
612         } else {
613             LOGGER.warn("Could not get referrer, returning to the application's base url");
614             response.sendRedirect(response.encodeRedirectURL(MCRFrontendUtil.getBaseURL()));
615         }
616     }
617 
618     /**
619      * If a referrer is available this method redirects to the url given by the referrer otherwise method redirects to
620      * the alternative-url.
621      */
622     protected void toReferrer(HttpServletRequest request, HttpServletResponse response, String altURL)
623         throws IOException {
624         URL referrer = getReferer(request);
625         if (referrer != null) {
626             response.sendRedirect(response.encodeRedirectURL(referrer.toString()));
627         } else {
628             LOGGER.warn("Could not get referrer, returning to {}", altURL);
629             response.sendRedirect(response.encodeRedirectURL(altURL));
630         }
631     }
632 }