001 /*
002 *
003 * $Revision: 14944 $ $Date: 2009-03-18 12:19:32 +0100 (Wed, 18 Mar 2009) $
004 *
005 * This file is part of *** M y C o R e ***
006 * See http://www.mycore.de/ for details.
007 *
008 * This program is free software; you can use it, redistribute it
009 * and / or modify it under the terms of the GNU General Public License
010 * (GPL) as published by the Free Software Foundation; either version 2
011 * of the License or (at your option) any later version.
012 *
013 * This program is distributed in the hope that it will be useful, but
014 * WITHOUT ANY WARRANTY; without even the implied warranty of
015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
016 * GNU General Public License for more details.
017 *
018 * You should have received a copy of the GNU General Public License
019 * along with this program, in a file called gpl.txt or license.txt.
020 * If not, write to the Free Software Foundation Inc.,
021 * 59 Temple Place - Suite 330, Boston, MA 02111-1307 USA
022 */
023
024 package org.mycore.frontend.editor;
025
026 import java.io.ByteArrayOutputStream;
027 import java.io.IOException;
028 import java.io.UnsupportedEncodingException;
029 import java.lang.reflect.*;
030 import java.text.DateFormat;
031 import java.text.NumberFormat;
032 import java.text.ParseException;
033 import java.text.SimpleDateFormat;
034 import java.util.Date;
035 import java.util.Locale;
036 import java.util.regex.Pattern;
037
038 import javax.xml.transform.Source;
039 import javax.xml.transform.Transformer;
040 import javax.xml.transform.TransformerConfigurationException;
041 import javax.xml.transform.TransformerException;
042 import javax.xml.transform.TransformerFactory;
043 import javax.xml.transform.stream.StreamResult;
044
045 import org.jdom.Document;
046 import org.jdom.Element;
047 import org.jdom.transform.JDOMSource;
048 import org.mycore.common.MCRCache;
049 import org.mycore.common.MCRConfigurationException;
050 import org.mycore.common.MCRConstants;
051 import org.mycore.common.MCRException;
052
053 /**
054 * This class provides input validation methods for editor data.
055 *
056 * @author Frank Lützenkirchen
057 */
058 public class MCRInputValidator {
059 /** Template stylesheet for checking XSL conditions * */
060 private Document stylesheet = null;
061
062 /** XSL transformer factory * */
063 private TransformerFactory factory = null;
064
065 /** Creates a new, reusable input validator * */
066 private MCRInputValidator() {
067 stylesheet = prepareStylesheet();
068 factory = TransformerFactory.newInstance();
069 }
070
071 private static MCRInputValidator singleton;
072
073 public static synchronized MCRInputValidator instance() {
074 if (singleton == null) {
075 singleton = new MCRInputValidator();
076 }
077
078 return singleton;
079 }
080
081 /** Cache of reusable stylesheets for checking XSL conditions * */
082 private MCRCache xslcondCache = new MCRCache(20, "InputValidator XSL conditions");
083
084 /**
085 * Checks the input string against an XSL condition. The syntax of the
086 * condition string is same as it would be usable in a xsl:if condition. The
087 * input string can be referenced by "." or "text()" in the condition, for
088 * example a condition could be "starts-with(.,'http://')". If input string
089 * is null, false is returned.
090 *
091 * @param input
092 * the string that should be validated
093 * @param condition
094 * the XSL condition as it would be used in xsl:when or xsl:if
095 * @return false if input is null, otherwise the result of the test is
096 * returned
097 * @throws MCRConfigurationException
098 * if XSL condition has syntax errors
099 */
100 public boolean validateXSLCondition(String input, String condition) {
101 if (input == null) {
102 input = "";
103 }
104
105 Document xml = new Document(new Element("input").addContent(input));
106 return validateXSLCondition(xml,condition);
107 }
108
109 private boolean validateXSLCondition(Document xml, String condition) {
110 Source xmlsrc = new JDOMSource(xml);
111
112 Document xsl = (Document) (xslcondCache.get(condition));
113
114 if (xsl == null) {
115 xsl = (Document) (stylesheet.clone());
116
117 Element when = xsl.getRootElement().getChild("template", MCRConstants.XSL_NAMESPACE).getChild("choose", MCRConstants.XSL_NAMESPACE).getChild("when", MCRConstants.XSL_NAMESPACE);
118 when.setAttribute("test", condition);
119 xslcondCache.put(condition, xsl);
120 }
121
122 try {
123 Transformer transformer = factory.newTransformer(new JDOMSource(xsl));
124 ByteArrayOutputStream out = new ByteArrayOutputStream();
125 transformer.transform(xmlsrc, new StreamResult(out));
126 out.close();
127
128 return "t".equals(out.toString("UTF-8"));
129 } catch (TransformerConfigurationException e) {
130 String msg = "Could not build XSL transformer";
131 throw new org.mycore.common.MCRConfigurationException(msg, e);
132 } catch (UnsupportedEncodingException e) {
133 String msg = "UTF-8 encoding seems not to be supported?";
134 throw new org.mycore.common.MCRConfigurationException(msg, e);
135 } catch (TransformerException e) {
136 String msg = "Probably syntax error in this XSL condition: " + condition;
137 throw new org.mycore.common.MCRConfigurationException(msg, e);
138 } catch (IOException e) {
139 String msg = "IOException in memory, this should never happen";
140 throw new org.mycore.common.MCRConfigurationException(msg, e);
141 }
142 }
143
144 public boolean validateXSLCondition(Element input, String condition) {
145 Document xml = new Document( (Element)(input.clone()) );
146 return validateXSLCondition(xml,condition);
147 }
148
149 /** Prepares a template stylesheet that is used for checking XSL conditions * */
150 private synchronized Document prepareStylesheet() {
151 Element stylesheet = new Element("stylesheet").setAttribute("version", "1.0");
152 stylesheet.setNamespace(MCRConstants.XSL_NAMESPACE);
153
154 Element output = new Element("output", MCRConstants.XSL_NAMESPACE);
155 output.setAttribute("method", "text");
156 stylesheet.addContent(output);
157
158 Element template = new Element("template", MCRConstants.XSL_NAMESPACE).setAttribute("match", "/*");
159 stylesheet.addContent(template);
160
161 Element choose = new Element("choose", MCRConstants.XSL_NAMESPACE);
162 template.addContent(choose);
163
164 Element when = new Element("when", MCRConstants.XSL_NAMESPACE);
165 when.addContent("t");
166
167 Element otherwise = new Element("otherwise", MCRConstants.XSL_NAMESPACE);
168 otherwise.addContent("f");
169 choose.addContent(when).addContent(otherwise);
170
171 return new Document(stylesheet);
172 }
173
174 /** Cache of reusable compiled regular expressions * */
175 private MCRCache regexpCache = new MCRCache(20, "InputValidator compiled reqular expressions");
176
177 /**
178 * Checks the input string against a regular expression.
179 *
180 * @see java.util.regex.Pattern#compile(java.lang.String)
181 *
182 * @param input
183 * the string that should be validated
184 * @param regexp
185 * the regular expression using the syntax of the
186 * java.util.regex.Pattern class
187 * @return false if input is null, otherwise the result of the test is
188 * returned
189 */
190 public boolean validateRegularExpression(String input, String regexp) {
191 if (input == null) {
192 input = "";
193 }
194
195 Pattern p = (Pattern) (regexpCache.get(regexp));
196
197 if (p == null) {
198 p = Pattern.compile(regexp);
199 regexpCache.put(regexp, p);
200 }
201
202 return p.matcher(input).matches();
203 }
204
205 /**
206 * Checks an input string for minimum and/or maximum length. The minimum and
207 * maximum length must be given as a string that contains the actual int
208 * number, both arguments are optional if one of the limits should not be
209 * checked.
210 *
211 * @param input
212 * the input string thats length should be checked
213 * @param smin
214 * minimum length as a string, or null if min lenght should not
215 * be checked
216 * @param smax
217 * maximum length as a string, or null if max length should not
218 * be checked
219 * @return true, if the string matches the given min and max lengths
220 */
221 public boolean validateLength(String input, String smin, String smax) {
222 if (input == null) {
223 input = "";
224 }
225
226 int min = ((smin == null) ? Integer.MIN_VALUE : Integer.parseInt(smin));
227 int max = ((smax == null) ? Integer.MAX_VALUE : Integer.parseInt(smax));
228
229 return (input.length() >= min) && (input.length() <= max);
230 }
231
232 /** Cache of reusable DateFormat objects * */
233 private MCRCache formatCache = new MCRCache(20, "InputValidator DateFormat objects");
234
235 /**
236 * Returns a reusable DateFormat object for the given format string. That
237 * object may come from a cache.
238 */
239 private DateFormat getDateTimeFormat(String format) {
240 DateFormat df = (DateFormat) (formatCache.get(format));
241
242 if (df == null) {
243 df = new SimpleDateFormat(format);
244 df.setLenient(false);
245 formatCache.put(format, df);
246 }
247
248 return df;
249 }
250
251 /**
252 * Checks if input is null or empty or just contains whitespace.
253 *
254 * @param input
255 * the string to be checked
256 * @return false if input is null or empty or just blanks
257 */
258 public boolean validateRequired(String input) {
259 return ((input != null) && (input.trim().length() > 0));
260 }
261
262 /**
263 * Checks input for correct data type and minimum/maximum value. Possible
264 * data types are string, integer, decimal or datetime. The min and max
265 * arguments are optional and must be expressed as strings. The min and max
266 * value are used inclusive in the allowed range of values. If no check for
267 * min or max value should be performed, null can be given for that
268 * argument. For datetime input, the format of the string must be given as
269 * defined in SimpleDateFormat. For decimal input, the format argument
270 * should contain a two-character, lowercase language code as defined by ISO
271 * 639. This code determines the locale that is used to parse decimal
272 * values. If null is given, the default locale will be used. Ffor other
273 * data types null should be used as the format argument.
274 *
275 * Usage examples:
276 * <ul>
277 * <li>validateMinMaxType( input, "integer", "15", "20", null )</li>
278 * <li>validateMinMaxType( input, "datetime", "01.01.2000", null,
279 * "dd.MM.yyyy" )</li>
280 * <li>validateMinMaxType( input, "decimal", "3,1", "4,0", "de" )</li>
281 * </ul>
282 *
283 * @see java.text.SimpleDateFormat
284 * @see java.util.Locale
285 * @see java.text.NumberFormat#getInstance(java.util.Locale)
286 *
287 * @param input
288 * the input string to check
289 * @param type
290 * one of "string", "integer", "decimal" or "datetime"
291 * @param min
292 * the minimum value as a string, or null if min should not be
293 * tested
294 * @param max
295 * the maximum value as a string, or null if max should not be
296 * tested
297 * @param format
298 * for datetime input, a java.text.SimpleDateFormat pattern; for
299 * decimal input, a ISO-639 language code
300 * @return true if input matches the given data type, min, max value and
301 * date time format
302 */
303 public boolean validateMinMaxType(String input, String type, String min, String max, String format) {
304 if (input == null) {
305 input = "";
306 }
307
308 if (type.equals("string")) {
309 boolean ok = true;
310
311 if (min != null) {
312 ok = (min.compareTo(input) <= 0);
313 }
314
315 if (max != null) {
316 ok = ok && (max.compareTo(input) >= 0);
317 }
318
319 return ok;
320 } else if (type.equals("integer")) {
321 long lmin = Long.MIN_VALUE;
322 long lmax = Long.MAX_VALUE;
323 long lval = 0;
324
325 try {
326 if (min != null) {
327 lmin = Long.parseLong(min);
328 }
329
330 if (max != null) {
331 lmax = Long.parseLong(max);
332 }
333 } catch (NumberFormatException ex) {
334 String msg = "Could not parse min/max value for input validation";
335 throw new MCRConfigurationException(msg, ex);
336 }
337
338 try {
339 lval = Long.parseLong(input);
340 } catch (NumberFormatException ex) {
341 return false;
342 }
343
344 return (lmin <= lval) && (lmax >= lval);
345 } else if (type.equals("decimal")) {
346 Locale locale = ((format == null) ? Locale.getDefault() : new Locale(format));
347 NumberFormat nf = NumberFormat.getNumberInstance(locale);
348
349 double dmin = Double.MIN_VALUE;
350 double dval = 0.0;
351 double dmax = Double.MAX_VALUE;
352
353 try {
354 if (min != null) {
355 dmin = nf.parse(min).doubleValue();
356 }
357
358 if (max != null) {
359 dmax = nf.parse(max).doubleValue();
360 }
361 } catch (ParseException ex) {
362 String msg = "Could not parse min/max value for input validation";
363 throw new MCRConfigurationException(msg, ex);
364 }
365
366 try {
367 dval = nf.parse(input).doubleValue();
368 } catch (ParseException e) {
369 return false;
370 }
371
372 return (dmin <= dval) && (dmax >= dval);
373 } else if (type.equals("datetime")) {
374 DateFormat df = getDateTimeFormat(format);
375
376 Date dmin = null;
377 Date dmax = null;
378 Date dval = null;
379
380 try {
381 dval = df.parse(input);
382 } catch (ParseException ex) {
383 return false;
384 }
385
386 try {
387 if (min != null) {
388 dmin = df.parse(min);
389 }
390
391 if (max != null) {
392 dmax = df.parse(max);
393 }
394 } catch (ParseException ex) {
395 String msg = "Could not parse min/max value for input validation";
396 throw new MCRConfigurationException(msg, ex);
397 }
398
399 if ((dmin != null) && (dmin.after(dval))) {
400 return false;
401 }
402
403 if ((dmax != null) && (dmax.before(dval))) {
404 return false;
405 }
406
407 return true;
408 } else {
409 throw new MCRConfigurationException("Unknown input data type: " + type);
410 }
411 }
412
413 /**
414 * Compares two input fields using a comparison operator.
415 *
416 * @param valueA
417 * the first input string to check
418 * @param valueB
419 * the second input string to check
420 * @param type
421 * one of "string", "integer", "decimal" or "datetime"
422 * @param operator
423 * One of =, <, >, <=, >=, !=
424 * @param format
425 * for datetime input, a java.text.SimpleDateFormat pattern; for
426 * decimal input, a ISO-639 language code
427 *
428 * @return true if the compare result is true OR one of the input fields is
429 * empty OR one of the input fields is in wrong format.
430 */
431 public boolean compare(String valueA, String valueB, String operator, String type, String format) {
432 try {
433 if ((valueA == null) || (valueA.trim().length() == 0)) {
434 return true;
435 }
436
437 if ((valueB == null) || (valueB.trim().length() == 0)) {
438 return true;
439 }
440
441 if (type.equals("string")) {
442 int res = valueA.compareTo(valueB);
443
444 if ("=".equals(operator)) {
445 return (res == 0);
446 } else if ("<".equals(operator)) {
447 return (res < 0);
448 } else if (">".equals(operator)) {
449 return (res > 0);
450 } else if ("<=".equals(operator)) {
451 return (res <= 0);
452 } else if (">=".equals(operator)) {
453 return (res >= 0);
454 } else if ("!=".equals(operator)) {
455 return !(res == 0);
456 } else {
457 throw new MCRConfigurationException("Unknown compare operator: " + operator);
458 }
459 } else if (type.equals("integer")) {
460 long vA = Long.parseLong(valueA.trim());
461 long vB = Long.parseLong(valueB.trim());
462
463 if ("=".equals(operator)) {
464 return (vA == vB);
465 } else if ("<".equals(operator)) {
466 return (vA < vB);
467 } else if (">".equals(operator)) {
468 return (vA > vB);
469 } else if ("<=".equals(operator)) {
470 return (vA <= vB);
471 } else if (">=".equals(operator)) {
472 return (vA >= vB);
473 } else if ("!=".equals(operator)) {
474 return !(vA == vB);
475 } else {
476 throw new MCRConfigurationException("Unknown compare operator: " + operator);
477 }
478 } else if (type.equals("decimal")) {
479 Locale locale = ((format == null) ? Locale.getDefault() : new Locale(format));
480 NumberFormat nf = NumberFormat.getNumberInstance(locale);
481 double vA = nf.parse(valueA.trim()).doubleValue();
482 double vB = nf.parse(valueB.trim()).doubleValue();
483
484 if ("=".equals(operator)) {
485 return (vA == vB);
486 } else if ("<".equals(operator)) {
487 return (vA < vB);
488 } else if (">".equals(operator)) {
489 return (vA > vB);
490 } else if ("<=".equals(operator)) {
491 return (vA <= vB);
492 } else if (">=".equals(operator)) {
493 return (vA >= vB);
494 } else if ("!=".equals(operator)) {
495 return !(vA == vB);
496 } else {
497 throw new MCRConfigurationException("Unknown compare operator: " + operator);
498 }
499 } else if (type.equals("datetime")) {
500 DateFormat df = getDateTimeFormat(format);
501 Date vA = df.parse(valueA.trim());
502 Date vB = df.parse(valueB.trim());
503
504 if ("=".equals(operator)) {
505 return (vA.equals(vB));
506 } else if ("<".equals(operator)) {
507 return (vA.before(vB));
508 } else if (">".equals(operator)) {
509 return (vA.after(vB));
510 } else if ("<=".equals(operator)) {
511 return (vA.before(vB) || vA.equals(vB));
512 } else if (">=".equals(operator)) {
513 return (vA.after(vB) || vA.equals(vB));
514 } else if ("!=".equals(operator)) {
515 return !(vA.equals(vB));
516 } else {
517 throw new MCRConfigurationException("Unknown compare operator: " + operator);
518 }
519 } else {
520 throw new MCRConfigurationException("Unknown input data type: " + type);
521 }
522 } catch (ParseException ex) {
523 return true;
524 } catch (NumberFormatException ex) {
525 return true;
526 }
527 }
528
529 /**
530 * Calls a "public static boolean" method in the given class and validates
531 * the value externally using the given method in that class.
532 *
533 * @param clazz
534 * the name of the class that contains the validation method
535 * @param method
536 * the name of the public static boolean method that should be
537 * called
538 * @param value
539 * the value to validate
540 *
541 * @return true, if the value validates
542 */
543 public boolean validateExternally(String clazz, String method, String value) {
544 Class[] argTypes = new Class[1];
545 argTypes[0] = String.class;
546 Object[] args = new Object[1];
547 args[0] = value;
548 Object result = new Boolean(false);
549 try {
550 Method m = Class.forName(clazz).getMethod(method, argTypes);
551 result = m.invoke(null, args);
552 } catch (Exception ex) {
553 String msg = "Exception while validating input using external method";
554 throw new MCRException(msg, ex);
555 }
556 return ((Boolean) result).booleanValue();
557 }
558
559 /**
560 * Calls a "public static boolean" method in the given class and validates
561 * the two values externally using the given method in that class.
562 *
563 * @param clazz
564 * the name of the class that contains the validation method
565 * @param method
566 * the name of the public static boolean method that should be
567 * called
568 * @param value1
569 * the first value to validate
570 * @param value2
571 * the second value to validate
572 *
573 * @return true, if the two values validate
574 */
575 public boolean validateExternally(String clazz, String method, String value1, String value2) {
576 Class[] argTypes = new Class[2];
577 argTypes[0] = String.class;
578 argTypes[1] = String.class;
579 Object[] args = new Object[2];
580 args[0] = value1;
581 args[1] = value2;
582 Object result = new Boolean(false);
583 try {
584 Method m = Class.forName(clazz).getMethod(method, argTypes);
585 result = m.invoke(null, args);
586 } catch (Exception ex) {
587 String msg = "Exception while validating input using external method";
588 throw new MCRException(msg, ex);
589 }
590 return ((Boolean) result).booleanValue();
591 }
592
593 /**
594 * Calls a "public static boolean" method in the given class and validates
595 * an XML element
596 *
597 * @param clazz
598 * the name of the class that contains the validation method
599 * @param method
600 * the name of the public static boolean method that should be
601 * called
602 * @param elem
603 * the XML element to validate
604 *
605 * @return true, if the XML element validates
606 */
607 public boolean validateExternally(String clazz, String method, Element elem) {
608 Class[] argTypes = new Class[1];
609 argTypes[0] = Element.class;
610 Object[] args = new Object[1];
611 args[0] = elem;
612 Object result = new Boolean(false);
613 try {
614 Method m = Class.forName(clazz).getMethod(method, argTypes);
615 result = m.invoke(null, args);
616 } catch (Exception ex) {
617 String msg = "Exception while validating input using external method";
618 throw new MCRException(msg, ex);
619 }
620 return ((Boolean) result).booleanValue();
621 }
622
623 public static void main(String[] args) {
624 MCRInputValidator iv = MCRInputValidator.instance();
625 System.out.println(true == iv.validateXSLCondition("bingo@bongo.com", "contains(.,'@')"));
626 System.out.println(false == iv.validateLength("john doe", "20", null));
627 System.out.println(false == iv.validateRequired(" \t"));
628 System.out.println(false == iv.validateRegularExpression("aacab", "a*b"));
629 System.out.println(true == iv.validateMinMaxType("4711", "integer", "100", null, null));
630 System.out.println(true == iv.validateMinMaxType("Frank", "string", "AAAAA", "zzzzz", null));
631 System.out.println(true == iv.validateMinMaxType("13:58", "datetime", null, "14:00", "HH:mm"));
632 System.out.println(false == iv.validateMinMaxType("27:58", "datetime", null, null, "HH:mm"));
633 System.out.println(false == iv.validateMinMaxType("30.02.2005", "datetime", null, null, "dd.MM.yyyy"));
634 System.out.println(true == iv.validateMinMaxType("26.02.2005", "datetime", null, null, "dd.MM.yyyy"));
635 System.out.println(true == iv.validateMinMaxType("3,5", "decimal", "1", "4", "de"));
636 System.out.println(true == iv.validateMinMaxType("3.5", "decimal", "1", null, "en"));
637 }
638 }