changeset 1196:2c448b6b21da

starting IIIF image api parser.
author robcast
date Wed, 17 Jul 2013 10:45:41 +0200
parents 2cba651b91ce
children e7608a7205e1
files common/src/main/java/digilib/conf/DigilibRequest.java servlet/src/main/java/digilib/conf/DigilibServletRequest.java
diffstat 2 files changed, 358 insertions(+), 297 deletions(-) [+]
line wrap: on
line diff
--- a/common/src/main/java/digilib/conf/DigilibRequest.java	Tue Jul 16 13:39:22 2013 +0200
+++ b/common/src/main/java/digilib/conf/DigilibRequest.java	Wed Jul 17 10:45:41 2013 +0200
@@ -33,6 +33,8 @@
 import java.net.URLDecoder;
 import java.util.StringTokenizer;
 
+import org.apache.log4j.Logger;
+
 import digilib.image.ImageJobDescription;
 import digilib.io.FileOps;
 import digilib.util.OptionsSet;
@@ -42,7 +44,7 @@
 /**
  * Class holding the parameters of a digilib user request. The parameters are
  * mostly named like the servlet parameters: <br>
- * request_path: url of the page/document. <br>
+ * request.path: url of the page/document. <br>
  * fn: url of the page/document. <br>
  * pn: page number. <br>
  * dw: width of result window in pixels. <br>
@@ -52,10 +54,7 @@
  * ww: width of image area(float from 0 to 1). <br>
  * wh: height of image area(float from 0 to 1). <br>
  * ws: scale factor. <br>
- * mo: special options like 'fit' for gifs. <br>
- * mk: list of marks. <br>
- * pt: total number of pages (generated by sevlet). <br>
- * baseURL: base URL (from http:// to below /servlet). <br>
+ * mo: special options like 'fit'. <br>
  * ...et alii
  * 
  * @author casties
@@ -63,19 +62,24 @@
  */
 public class DigilibRequest extends ParameterMap {
 
-    /**  ImageJobDescription for this request */
-	protected ImageJobDescription ticket;
-	
-	/** DigilibConfiguration for this request */
-	protected DigilibConfiguration config;
+    private static Logger logger = Logger.getLogger("digilib.request");
+
+    // TODO: make prefix configurable
+    public static final String iiifPrefix = "iiif";
+
+    /** ImageJobDescription for this request */
+    protected ImageJobDescription ticket;
 
-	public DigilibRequest() {
-		super(30);
-	}
+    /** DigilibConfiguration for this request */
+    protected DigilibConfiguration config;
 
-	/**
-	 * Create DigilibRequest with DigilibConfiguration.
-	 * 
+    public DigilibRequest() {
+        super(30);
+    }
+
+    /**
+     * Create DigilibRequest with DigilibConfiguration.
+     * 
      * @param config
      */
     public DigilibRequest(DigilibConfiguration config) {
@@ -83,90 +87,85 @@
         this.config = config;
     }
 
-    /** set up parameters.
-	 * 
-	 */
-	protected void initParams() {
-		/*
-		 * Definition of parameters and default values. Parameter of type 's'
-		 * are for the servlet.
-		 */
+    /**
+     * set up parameters.
+     * 
+     */
+    protected void initParams() {
+        /*
+         * Definition of parameters and default values. Parameter of type 's'
+         * are for the servlet.
+         */
 
-		// url of the page/document (second part)
-		newParameter("fn", "", null, 's');
-		// page number
-		newParameter("pn", new Integer(1), null, 's');
-		// width of client in pixels
-		newParameter("dw", new Integer(0), null, 's');
-		// height of client in pixels
-		newParameter("dh", new Integer(0), null, 's');
-		// left edge of image (float from 0 to 1)
-		newParameter("wx", new Float(0), null, 's');
-		// top edge in image (float from 0 to 1)
-		newParameter("wy", new Float(0), null, 's');
-		// width of image (float from 0 to 1)
-		newParameter("ww", new Float(1), null, 's');
-		// height of image (float from 0 to 1)
-		newParameter("wh", new Float(1), null, 's');
-		// scale factor
-		newParameter("ws", new Float(1), null, 's');
-		// special options like 'fit' for gifs
-		newParameter("mo", this.options, null, 's');
-		// rotation angle (degree)
-		newParameter("rot", new Float(0), null, 's');
-		// contrast enhancement factor
-		newParameter("cont", new Float(0), null, 's');
-		// brightness enhancement factor
-		newParameter("brgt", new Float(0), null, 's');
-		// color multiplicative factors
-		newParameter("rgbm", "0/0/0", null, 's');
-		// color additive factors
-		newParameter("rgba", "0/0/0", null, 's');
-		// display dpi resolution (total)
-		newParameter("ddpi", new Float(0), null, 's');
-		// display dpi X resolution
-		newParameter("ddpix", new Float(0), null, 's');
-		// display dpi Y resolution
-		newParameter("ddpiy", new Float(0), null, 's');
-		// scale factor for mo=ascale
-		newParameter("scale", new Float(1), null, 's');
-		// color conversion operation
-		newParameter("colop", "", null, 's');
+        // url of the page/document (second part)
+        newParameter("fn", "", null, 's');
+        // page number
+        newParameter("pn", new Integer(1), null, 's');
+        // width of client in pixels
+        newParameter("dw", new Integer(0), null, 's');
+        // height of client in pixels
+        newParameter("dh", new Integer(0), null, 's');
+        // left edge of image (float from 0 to 1)
+        newParameter("wx", new Float(0), null, 's');
+        // top edge in image (float from 0 to 1)
+        newParameter("wy", new Float(0), null, 's');
+        // width of image (float from 0 to 1)
+        newParameter("ww", new Float(1), null, 's');
+        // height of image (float from 0 to 1)
+        newParameter("wh", new Float(1), null, 's');
+        // scale factor
+        newParameter("ws", new Float(1), null, 's');
+        // special options like 'fit' for gifs
+        newParameter("mo", this.options, null, 's');
+        // rotation angle (degree)
+        newParameter("rot", new Float(0), null, 's');
+        // contrast enhancement factor
+        newParameter("cont", new Float(0), null, 's');
+        // brightness enhancement factor
+        newParameter("brgt", new Float(0), null, 's');
+        // color multiplicative factors
+        newParameter("rgbm", "0/0/0", null, 's');
+        // color additive factors
+        newParameter("rgba", "0/0/0", null, 's');
+        // display dpi resolution (total)
+        newParameter("ddpi", new Float(0), null, 's');
+        // display dpi X resolution
+        newParameter("ddpix", new Float(0), null, 's');
+        // display dpi Y resolution
+        newParameter("ddpiy", new Float(0), null, 's');
+        // scale factor for mo=ascale
+        newParameter("scale", new Float(1), null, 's');
+        // color conversion operation
+        newParameter("colop", "", null, 's');
 
-		/*
-		 * Parameters of type 'i' are not exchanged between client and server,
-		 * but are for the servlets or JSPs internal use.
-		 */
+        /*
+         * Parameters of type 'i' are not exchanged between client and server,
+         * but are for the servlets or JSPs internal use.
+         */
 
-		// url of the page/document (first part, may be empty)
-		newParameter("request.path", "", null, 'i');
-		// base URL (from http:// to below /servlet)
-		newParameter("base.url", null, null, 'i');
-		/*
-		 * Parameters of type 'c' are for the clients use
-		 */
+        // url of the page/document (first part, may be empty)
+        newParameter("request.path", "", null, 'i');
+        // base URL (from http:// to below /servlet)
+        newParameter("base.url", null, null, 'i');
+        /*
+         * Parameters of type 'c' are for the clients use
+         */
 
-		// "real" filename
-		newParameter("img.fn", "", null, 'c');
-		// image dpi x
-		newParameter("img.dpix", new Integer(0), null, 'c');
-		// image dpi y
-		newParameter("img.dpiy", new Integer(0), null, 'c');
-		// hires image size x
-		newParameter("img.pix_x", new Integer(0), null, 'c');
-		// hires image size y
-		newParameter("img.pix_y", new Integer(0), null, 'c');
-		// total number of pages
-		newParameter("pt", new Integer(0), null, 'c');
-		// display level of digilib (0 = just image, 1 = one HTML page
-		// 2 = in frameset, 3 = XUL-'frameset'
-		// 4 = XUL-Sidebar )
-		newParameter("lv", new Integer(2), null, 'c');
-		// marks
-		newParameter("mk", "", null, 'c');
-	}
+        // "real" filename
+        newParameter("img.fn", "", null, 'c');
+        // image dpi x
+        newParameter("img.dpix", new Integer(0), null, 'c');
+        // image dpi y
+        newParameter("img.dpiy", new Integer(0), null, 'c');
+        // hires image size x
+        newParameter("img.pix_x", new Integer(0), null, 'c');
+        // hires image size y
+        newParameter("img.pix_y", new Integer(0), null, 'c');
+    }
 
-    /* (non-Javadoc)
+    /*
+     * (non-Javadoc)
+     * 
      * @see digilib.servlet.ParameterMap#initOptions()
      */
     @Override
@@ -174,127 +173,238 @@
         options = (OptionsSet) getValue("mo");
     }
 
-	/**
-	 * Return the request parameters as a String in the parameter form
-	 * 'fn=/icons&amp;pn=1'. Empty (undefined) fields are not included.
-	 * 
-	 * @return String of request parameters in parameter form.
-	 */
-	public String getAsString() {
-		return getAsString(0);
-	}
+    /**
+     * Return the request parameters as a String in the parameter form
+     * 'fn=/icons&amp;pn=1'. Empty (undefined) fields are not included.
+     * 
+     * @return String of request parameters in parameter form.
+     */
+    public String getAsString() {
+        return getAsString(0);
+    }
 
-	/**
-	 * Return the request parameters of a given type type as a String in the
-	 * parameter form 'fn=/icons&amp;pn=1'. Empty (undefined) fields are not
-	 * included.
-	 * 
-	 * @return String of request parameters in parameter form.
-	 */
-	public String getAsString(int type) {
-		StringBuffer s = new StringBuffer(50);
-		// go through all values
-		for (Parameter p: params.values()) {
-			if ((type > 0) && (p.getType() != type)) {
-				// skip the wrong types
-				continue;
-			}
-			String name = p.getName();
-			/*
-			 * handling special cases
-			 */
-			// request_path adds to fn
-			if (name.equals("fn")) {
-				s.append("&fn=" + getAsString("request.path")
-						+ getAsString("fn"));
-				continue;
-			}
-			/*
-			 * the rest is sent with its name
-			 */
-			// parameters that are not set or internal are not sent
-			if ((!p.hasValue()) || (p.getType() == 'i')) {
-				continue;
-			}
-			s.append("&" + name + "=" + p.getAsString());
-		}
-		// kill first "&"
-		s.deleteCharAt(0);
-		return s.toString();
-	}
+    /**
+     * Return the request parameters of a given type type as a String in the
+     * parameter form 'fn=/icons&amp;pn=1'. Empty (undefined) fields are not
+     * included.
+     * 
+     * @return String of request parameters in parameter form.
+     */
+    public String getAsString(int type) {
+        StringBuffer s = new StringBuffer(50);
+        // go through all values
+        for (Parameter p : params.values()) {
+            if ((type > 0) && (p.getType() != type)) {
+                // skip the wrong types
+                continue;
+            }
+            String name = p.getName();
+            /*
+             * handling special cases
+             */
+            // request_path adds to fn
+            if (name.equals("fn")) {
+                s.append("&fn=" + getAsString("request.path") + getAsString("fn"));
+                continue;
+            }
+            /*
+             * the rest is sent with its name
+             */
+            // parameters that are not set or internal are not sent
+            if ((!p.hasValue()) || (p.getType() == 'i')) {
+                continue;
+            }
+            s.append("&" + name + "=" + p.getAsString());
+        }
+        // kill first "&"
+        s.deleteCharAt(0);
+        return s.toString();
+    }
+
+    /**
+     * Set request parameters from query string. Uses the separator string qs to
+     * get 'fn=foo' style parameters.
+     * 
+     * @param qs
+     *            query string
+     * @param sep
+     *            parameter-separator string
+     */
+    public void setWithParamString(String qs, String sep) {
+        // go through all request parameters
+        String[] qa = qs.split(sep);
+        for (int i = 0; i < qa.length; i++) {
+            // split names and values on "="
+            String[] nv = qa[i].split("=");
+            try {
+                String name = URLDecoder.decode(nv[0], "UTF-8");
+                String val = URLDecoder.decode(nv[1], "UTF-8");
+                // is this a known parameter?
+                if (params.containsKey(name)) {
+                    Parameter p = (Parameter) this.get(name);
+                    // internal parameters are not set
+                    if (p.getType() == 'i') {
+                        continue;
+                    }
+                    p.setValueFromString(val);
+                    continue;
+                }
+                // unknown parameters are just added with type 'r'
+                newParameter(name, null, val, 'r');
+            } catch (UnsupportedEncodingException e) {
+                // this shouldn't happen anyway
+                e.printStackTrace();
+            }
+        }
+    }
 
-	/**
-	 * Set request parameters from query string. Uses the separator string qs to
-	 * get 'fn=foo' style parameters.
-	 * 
-	 * @param qs
-	 *            query string
-	 * @param sep
-	 *            parameter-separator string
-	 */
-	public void setWithParamString(String qs, String sep) {
-		// go through all request parameters
-		String[] qa = qs.split(sep);
-		for (int i = 0; i < qa.length; i++) {
-			// split names and values on "="
-			String[] nv = qa[i].split("=");
-			try {
-				String name = URLDecoder.decode(nv[0], "UTF-8");
-				String val = URLDecoder.decode(nv[1], "UTF-8");
-				// is this a known parameter?
-				if (params.containsKey(name)) {
-					Parameter p = (Parameter) this.get(name);
-					// internal parameters are not set
-					if (p.getType() == 'i') {
-						continue;
-					}
-					p.setValueFromString(val);
-					continue;
-				}
-				// unknown parameters are just added with type 'r'
-				newParameter(name, null, val, 'r');
-			} catch (UnsupportedEncodingException e) {
-				// this shouldn't happen anyway
-				e.printStackTrace();
-			}
-		}
-	}
+    /**
+     * Populate a request from a string with an IIIF Image API path.
+     * 
+     * @param path
+     *            String with IIIF Image API path.
+     * 
+     * @see <a href="http://www-sul.stanford.edu/iiif/image-api/1.1/">IIIF Image
+     *      API</a>
+     */
+    public void setWithIiifPath(String path) {
+        if (path == null) {
+            return;
+        }
+        // enable the passing of delimiter to get empty parameters
+        StringTokenizer query = new StringTokenizer(path, "/", true);
+        String token;
+        // first parameter prefix
+        if (query.hasMoreTokens()) {
+            token = query.nextToken();
+            if (!token.equals(iiifPrefix)) {
+                logger.error("IIIF path doesn't start with prefix!");
+                // what now?
+            }
+            // skip /
+            if (query.hasMoreTokens()) {
+                query.nextToken();
+            }
+        }
+        // second parameter FN (encoded)
+        if (query.hasMoreTokens()) {
+            token = query.nextToken();
+            if (!token.equals("/")) {
+                try {
+                    setValueFromString("fn", URLDecoder.decode(token, "UTF-8"));
+                } catch (UnsupportedEncodingException e) {
+                    logger.error("Error decoding identifier in IIIF path!");
+                }
+                // skip /
+                if (query.hasMoreTokens()) {
+                    query.nextToken();
+                }
+            }
+        }
+        // third parameter region
+        if (query.hasMoreTokens()) {
+            token = query.nextToken();
+            if (!token.equals("/")) {
+                if (token.equals("full")) {
+                    // full region -- default
+                } else if (token.startsWith("pct:")){
+                    // region in % of original image
+                    String[] params = token.substring(4).split(",");
+                    try {
+                        float x = Float.parseFloat(params[0]);
+                        setValue("wx", x / 100f);
+                        float y = Float.parseFloat(params[1]);
+                        setValue("wy", y / 100f);
+                        float w = Float.parseFloat(params[2]);
+                        setValue("ww", w / 100f);
+                        float h = Float.parseFloat(params[3]);
+                        setValue("wh", h / 100f);
+                    } catch (Exception e) {
+                        logger.error("Error parsing range parameter in IIIF path!");
+                    }
+                } else {
+                    // region in pixel of original image :-(
+                    logger.error("pixel region not yet implemented");
+                }
+                // skip /
+                if (query.hasMoreTokens()) {
+                    query.nextToken();
+                }
+            }
+        }
+        // fourth parameter size
+        if (query.hasMoreTokens()) {
+            token = query.nextToken();
+            if (!token.equals("/")) {
+                // TODO
+                setValueFromString("mo", token);
+                // skip /
+                if (query.hasMoreTokens()) {
+                    query.nextToken();
+                }
+            }
+        }
+        // fifth parameter rotation
+        if (query.hasMoreTokens()) {
+            token = query.nextToken();
+            if (!token.equals("/")) {
+                setValueFromString("rot", token);
+                // skip /
+                if (query.hasMoreTokens()) {
+                    query.nextToken();
+                }
+            }
+        }
+        // sixth parameter quality.format
+        if (query.hasMoreTokens()) {
+            token = query.nextToken();
+            if (!token.equals("/")) {
+                // TODO
+                setValueFromString("wx", token);
+                // skip /
+                if (query.hasMoreTokens()) {
+                    query.nextToken();
+                }
+            }
+        }
+    }
 
-	/**
-	 * Test if option string <code>opt</code> is set. Checks if the substring
-	 * <code>opt</code> is contained in the options string <code>param</code>.
-	 * Deprecated! use hasOption(String opt) for "mo"-options.
-	 * 
-	 * @param opt
-	 *            Option string to be tested.
-	 * @return boolean
-	 */
-	public boolean hasOption(String param, String opt) {
-		String s = getAsString(param);
-		if (s != null) {
-			StringTokenizer i = new StringTokenizer(s, ",");
-			while (i.hasMoreTokens()) {
-				if (i.nextToken().equals(opt)) {
-					return true;
-				}
-			}
-		}
-		return false;
-	}
+    /**
+     * Test if option string <code>opt</code> is set. Checks if the substring
+     * <code>opt</code> is contained in the options string <code>param</code>.
+     * 
+     * @param opt
+     *            Option string to be tested.
+     * @return boolean
+     * 
+     * @deprecated use hasOption(String opt) for "mo"-options.
+     */
+    public boolean hasOption(String param, String opt) {
+        String s = getAsString(param);
+        if (s != null) {
+            StringTokenizer i = new StringTokenizer(s, ",");
+            while (i.hasMoreTokens()) {
+                if (i.nextToken().equals(opt)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
 
-	/**
-	 * The image file path to be accessed.
-	 * 
-	 * The mage file path is assembled from the servlets RequestPath and
-	 * Parameter fn and normalized.
-	 * 
-	 * @return String the effective filepath.
-	 */
-	public String getFilePath() {
-		String s = getAsString("request.path");
-		s += getAsString("fn");
-		return FileOps.normalName(s);
-	}
+    /**
+     * The image file path to be accessed.
+     * 
+     * The image file path is assembled from the servlets RequestPath and
+     * Parameter fn and normalized.
+     * 
+     * @return String the effective filepath.
+     */
+    public String getFilePath() {
+        String s = getAsString("request.path");
+        s += getAsString("fn");
+        return FileOps.normalName(s);
+    }
 
     /**
      * @return the ticket
@@ -304,7 +414,8 @@
     }
 
     /**
-     * @param ticket the ticket to set
+     * @param ticket
+     *            the ticket to set
      */
     public void setJobDescription(ImageJobDescription ticket) {
         this.ticket = ticket;
@@ -318,7 +429,8 @@
     }
 
     /**
-     * @param config the config to set
+     * @param config
+     *            the config to set
      */
     public void setDigilibConfig(DigilibConfiguration config) {
         this.config = config;
--- a/servlet/src/main/java/digilib/conf/DigilibServletRequest.java	Tue Jul 16 13:39:22 2013 +0200
+++ b/servlet/src/main/java/digilib/conf/DigilibServletRequest.java	Wed Jul 17 10:45:41 2013 +0200
@@ -27,8 +27,6 @@
  *         Christian Luginbuehl
  */
 
-import java.io.UnsupportedEncodingException;
-import java.net.URLDecoder;
 import java.util.Enumeration;
 import java.util.StringTokenizer;
 
@@ -171,10 +169,13 @@
         newParameter("img.pix_x", new Integer(0), null, 'c');
         // hires image size y
         newParameter("img.pix_y", new Integer(0), null, 'c');
-        // total number of pages
-        newParameter("pt", new Integer(0), null, 'c');
-        // marks
-        newParameter("mk", "", null, 'c');
+        /*
+         * TODO: check if we can remove these
+         * // total number of pages
+         * newParameter("pt", new Integer(0), null, 'c');
+         * // marks
+         * newParameter("mk", "", null, 'c');
+         */
     }
 
     /*
@@ -195,25 +196,32 @@
      */
     public void setWithRequest(HttpServletRequest request) {
         servletRequest = request;
-        // decide if it's old-style or new-style
-        String qs = ((HttpServletRequest) request).getQueryString();
-        if (qs != null) {
-            if (qs.indexOf("&amp;") > -1) {
-                // &amp; separator
-                setWithParamString(qs, "&amp;");
-            } else if (qs.indexOf(";") > -1) {
-                // ; separator
-                setWithParamString(qs, ";");
-            } else if (qs.indexOf('=') > -1) {
-                // standard '&' parameters
-                setWithParamRequest(request);
-            } else {
-                setWithOldString(qs);
+        setValue("servlet.request", request);
+        // request path (after servlet, before "?")
+        String path = ((HttpServletRequest) request).getPathInfo();
+        // decide if its IIIF API
+        if (path != null && path.startsWith(iiifPrefix, 1)) {
+            setWithIiifPath(path.substring(1));
+        } else {
+            // decide if it's old-style or new-style digilib
+            String qs = ((HttpServletRequest) request).getQueryString();
+            if (qs != null) {
+                if (qs.indexOf("&amp;") > -1) {
+                    // &amp; separator
+                    setWithParamString(qs, "&amp;");
+                } else if (qs.indexOf(";") > -1) {
+                    // ; separator
+                    setWithParamString(qs, ";");
+                } else if (qs.indexOf('=') > -1) {
+                    // standard '&' parameters
+                    setWithParamRequest(request);
+                } else {
+                    setWithOldString(qs);
+                }
             }
+            // add path from request
+            setValue("request.path", path);
         }
-        setValue("servlet.request", request);
-        // add path from request
-        setValue("request.path", ((HttpServletRequest) request).getPathInfo());
         // set the baseURL
         setBaseURL((HttpServletRequest) request);
     }
@@ -423,65 +431,6 @@
         setValue("request.path", ((HttpServletRequest) request).getPathInfo());
     }
 
-    /**
-     * Set request parameters from query string. Uses the separator string qs to
-     * get 'fn=foo' style parameters.
-     * 
-     * @param qs
-     *            query string
-     * @param sep
-     *            parameter-separator string
-     */
-    public void setWithParamString(String qs, String sep) {
-        // go through all request parameters
-        String[] qa = qs.split(sep);
-        for (int i = 0; i < qa.length; i++) {
-            // split names and values on "="
-            String[] nv = qa[i].split("=");
-            try {
-                String name = URLDecoder.decode(nv[0], "UTF-8");
-                String val = URLDecoder.decode(nv[1], "UTF-8");
-                // is this a known parameter?
-                if (params.containsKey(name)) {
-                    Parameter p = (Parameter) this.get(name);
-                    // internal parameters are not set
-                    if (p.getType() == 'i') {
-                        continue;
-                    }
-                    p.setValueFromString(val);
-                    continue;
-                }
-                // unknown parameters are just added with type 'r'
-                newParameter(name, null, val, 'r');
-            } catch (UnsupportedEncodingException e) {
-                // this shouldn't happen anyway
-                e.printStackTrace();
-            }
-        }
-    }
-
-    /**
-     * Test if option string <code>opt</code> is set. Checks if the substring
-     * <code>opt</code> is contained in the options string <code>param</code>.
-     * Deprecated! use hasOption(String opt) for "mo"-options.
-     * 
-     * @param opt
-     *            Option string to be tested.
-     * @return boolean
-     */
-    public boolean hasOption(String param, String opt) {
-        String s = getAsString(param);
-        if (s != null) {
-            StringTokenizer i = new StringTokenizer(s, ",");
-            while (i.hasMoreTokens()) {
-                if (i.nextToken().equals(opt)) {
-                    return true;
-                }
-            }
-        }
-        return false;
-    }
-
     /* Property getter and setter */
 
     /**
@@ -495,7 +444,7 @@
         String baseURL = null;
         // calculate base URL string from request until webapp
         String s = request.getRequestURL().toString();
-        // get name of webapp 
+        // get name of webapp
         String wn = request.getContextPath();
         int eop = s.lastIndexOf(wn);
         if (eop > 0) {