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    }