001    /*
002     * 
003     * $Revision: 14750 $ $Date: 2009-02-17 16:36:38 +0100 (Tue, 17 Feb 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.common;
025    
026    import static org.mycore.common.MCRConstants.DATE_FORMAT;
027    import static org.mycore.common.MCRConstants.DEFAULT_ENCODING;
028    
029    import java.io.BufferedOutputStream;
030    import java.io.ByteArrayOutputStream;
031    import java.io.File;
032    import java.io.FileOutputStream;
033    import java.io.IOException;
034    import java.io.InputStream;
035    import java.io.OutputStream;
036    import java.io.Reader;
037    import java.io.Writer;
038    import java.text.DateFormat;
039    import java.text.ParseException;
040    import java.text.SimpleDateFormat;
041    import java.util.ArrayList;
042    import java.util.Calendar;
043    import java.util.GregorianCalendar;
044    import java.util.HashSet;
045    import java.util.Iterator;
046    import java.util.Locale;
047    import java.util.Properties;
048    
049    import javax.xml.parsers.SAXParser;
050    import javax.xml.parsers.SAXParserFactory;
051    
052    import org.apache.log4j.Logger;
053    import org.jdom.Document;
054    import org.jdom.Element;
055    import org.jdom.output.Format;
056    import org.jdom.output.XMLOutputter;
057    import org.xml.sax.Attributes;
058    import org.xml.sax.InputSource;
059    import org.xml.sax.helpers.DefaultHandler;
060    
061    /**
062     * This class represent a general set of external methods to support the
063     * programming API.
064     * 
065     * @author Jens Kupferschmidt
066     * @author Frank L\u00fctzenkirchen
067     * @author Thomas Scheffler (yagee)
068     * 
069     * @version $Revision: 14750 $ $Date: 2009-02-17 16:36:38 +0100 (Tue, 17 Feb 2009) $
070     */
071    public class MCRUtils {
072        // The file slash
073        private static String SLASH = System.getProperty("file.separator");;
074    
075        public final static char COMMAND_OR = 'O';
076    
077        public final static char COMMAND_AND = 'A';
078    
079        public final static char COMMAND_XOR = 'X';
080    
081        // public constant data
082        private static final Logger LOGGER = Logger.getLogger(MCRUtils.class);
083    
084        // Language lists
085        private static ArrayList<String> langlist = new ArrayList<String>();
086        private static ArrayList<String> countrylist = new ArrayList<String>();
087        
088        /**
089         * Load two static arrays for fast search of ISO-639/ISO-3166 strings.
090         */
091        static {
092            StringBuffer sb;
093            // add id as workaround
094            langlist.add("id");
095            countrylist.add("ID");
096            // add codes from locale
097            for ( Locale l : Locale.getAvailableLocales()) {
098                sb = new StringBuffer(l.getLanguage());
099                langlist.add(sb.toString());
100                sb.append('-').append(l.getCountry());
101                countrylist.add(sb.toString());
102            }
103        }
104        
105        /**
106         * This method check the language string base on RFC 1766 to the supported
107         * languages in mycore.
108         * 
109         * @param lang
110         *            the language string
111         * @return true if the language was supported, otherwise false
112         */
113        public static final boolean isSupportedLang(String lang) {
114            if ((lang == null) || ((lang = lang.trim()).length() == 0)) {
115                return false;
116            }
117            if (lang.startsWith("x-")) { return true; }
118            if (langlist.contains(lang)) { return true; }
119            if (countrylist.contains(lang)) { return true; }
120            return false;
121        }
122    
123        /**
124         * The methode convert the input date string to the ISO output string. If
125         * the input can't convert, the output is null.
126         * 
127         * @param indate
128         *            the date input
129         * @return the ISO output or null
130         */
131        public static final String covertDateToISO(String indate) {
132            if ((indate == null) || ((indate = indate.trim()).length() == 0)) {
133                return null;
134            }
135    
136            GregorianCalendar calendar = new GregorianCalendar();
137            boolean test = false;
138            SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");
139            formatter.setLenient(false);
140    
141            try {
142                calendar.setTime(formatter.parse(indate));
143                test = true;
144            } catch (ParseException e) {
145            }
146    
147            if (!test) {
148                for (int i = 0; i < DATE_FORMAT.length; i++) {
149                    DateFormat df = DATE_FORMAT[i];
150                    df.setLenient(false);
151    
152                    try {
153                        calendar.setTime(df.parse(indate));
154                        test = true;
155                    } catch (ParseException e) {
156                    }
157    
158                    if (test) {
159                        break;
160                    }
161                }
162            }
163    
164            if (!test) {
165                return null;
166            }
167    
168            formatter.setCalendar(calendar);
169    
170            return formatter.format(calendar.getTime());
171        }
172    
173        /**
174         * The methode convert the input date string to the GregorianCalendar. If
175         * the input can't convert, the output is null.
176         * 
177         * @param indate
178         *            the date input
179         * @return the GregorianCalendar or null
180         */
181        public static final GregorianCalendar covertDateToGregorianCalendar(String indate) {
182            if ((indate == null) || ((indate = indate.trim()).length() == 0)) {
183                return null;
184            }
185    
186            boolean era = true;
187            int start = 0;
188    
189            if (indate.substring(0, 2).equals("AD")) {
190                era = true;
191                start = 2;
192            }
193    
194            if (indate.substring(0, 2).equals("BC")) {
195                era = false;
196                start = 2;
197            }
198    
199            GregorianCalendar calendar = new GregorianCalendar();
200            boolean test = false;
201            SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");
202    
203            try {
204                calendar.setTime(formatter.parse(indate.substring(start, indate.length())));
205    
206                if (!era) {
207                    calendar.set(Calendar.ERA, GregorianCalendar.BC);
208                }
209    
210                test = true;
211            } catch (ParseException e) {
212            }
213    
214            if (!test) {
215                for (int i = 0; i < DATE_FORMAT.length; i++) {
216                    DateFormat df = DATE_FORMAT[i];
217    
218                    try {
219                        calendar.setTime(df.parse(indate.substring(start, indate.length())));
220    
221                        if (!era) {
222                            calendar.set(Calendar.ERA, GregorianCalendar.BC);
223                        }
224    
225                        test = true;
226                    } catch (ParseException e) {
227                    }
228    
229                    if (test) {
230                        break;
231                    }
232                }
233            }
234    
235            if (!test) {
236                return null;
237            }
238    
239            return calendar;
240        }
241    
242        /**
243         * This methode replace any characters to XML entity references.
244         * <p>
245         * <ul>
246         * <li>&lt; to &amp;lt;
247         * <li>&gt; to &amp;gt;
248         * <li>&amp; to &amp;amp;
249         * <li>&quot; to &amp;quot;
250         * <li>&apos; to &amp;apos;
251         * </ul>
252         * 
253         * @param in
254         *            a string
255         * @return the converted string.
256         */
257        public static final String stringToXML(String in) {
258            if (in == null) {
259                return "";
260            }
261    
262            StringBuffer sb = new StringBuffer(2048);
263    
264            for (int i = 0; i < in.length(); i++) {
265                if (in.charAt(i) == '<') {
266                    sb.append("&lt;");
267    
268                    continue;
269                }
270    
271                if (in.charAt(i) == '>') {
272                    sb.append("&gt;");
273    
274                    continue;
275                }
276    
277                if (in.charAt(i) == '&') {
278                    sb.append("&amp;");
279    
280                    continue;
281                }
282    
283                if (in.charAt(i) == '\"') {
284                    sb.append("&quot;");
285    
286                    continue;
287                }
288    
289                if (in.charAt(i) == '\'') {
290                    sb.append("&apos;");
291    
292                    continue;
293                }
294    
295                sb.append(in.charAt(i));
296            }
297    
298            return sb.toString();
299        }
300    
301        /**
302         * This method convert a JDOM tree to a byte array.
303         * 
304         * @param jdom
305         *            the JDOM tree
306         * @return a byte array of the JDOM tree
307         */
308        public static final byte[] getByteArray(org.jdom.Document jdom) throws MCRPersistenceException {
309            MCRConfiguration conf = MCRConfiguration.instance();
310            String mcr_encoding = conf.getString("MCR.Metadata.DefaultEncoding", DEFAULT_ENCODING);
311            ByteArrayOutputStream outb = new ByteArrayOutputStream();
312    
313            try {
314                XMLOutputter outp = new XMLOutputter(Format.getRawFormat().setEncoding(mcr_encoding));
315                outp.output(jdom, outb);
316            } catch (Exception e) {
317                throw new MCRPersistenceException("Can't produce byte array.");
318            }
319    
320            return outb.toByteArray();
321        }
322    
323        /**
324         * Converts an Array of Objects to an Array of Strings using the toString()
325         * method.
326         * 
327         * @param objects
328         *            Array of Objects to be converted
329         * @return Array of Strings representing Objects
330         */
331        public static final String[] getStringArray(Object[] objects) {
332            String[] returns = new String[objects.length];
333    
334            for (int i = 0; i < objects.length; i++)
335                returns[i] = objects[i].toString();
336    
337            return returns;
338        }
339    
340        /**
341         * Converts an Array of Objects to an Array of Strings using the toString()
342         * method.
343         * 
344         * @param objects
345         *            Array of Objects to be converted
346         * @param maxitems
347         *            The maximum of items to convert
348         * @return Array of Strings representing Objects
349         */
350        public static final String[] getStringArray(Object[] objects, int maxitems) {
351            String[] returns = new String[maxitems];
352    
353            for (int i = 0; i < maxitems; i++)
354                returns[i] = objects[i].toString();
355    
356            return returns;
357        }
358    
359        /**
360         * Copies all content read from the given input stream to the given output
361         * stream. Note that this method will NOT close the streams when finished
362         * copying.
363         * 
364         * @param source
365         *            the InputStream to read the bytes from
366         * @param target
367         *            out the OutputStream to write the bytes to, may be null
368         * @return true if Inputstream copied successfully to OutputStream
369         */
370        public static boolean copyStream(InputStream source, OutputStream target) {
371            if (source == null) {
372                throw new MCRException("InputStream source is null.");
373            }
374            
375            try {
376                // R E A D / W R I T E by chunks
377                int chunkSize = 63 * 1024;
378    
379                // code will work even when chunkSize = 0 or chunks = 0;
380                // Even for small files, we allocate a big buffer, since we
381                // don't know the size ahead of time.
382                byte[] ba = new byte[chunkSize];
383    
384                // keep reading till hit eof
385                while (true) {
386                    int bytesRead = readBlocking(source, ba, 0, chunkSize);
387    
388                    if (LOGGER.isDebugEnabled()) {
389                        LOGGER.debug(MCRUtils.class.getName() + ".copyStream(): " + bytesRead + " bytes read");
390                    }
391    
392                    if (bytesRead > 0) {
393                        if (target != null) {
394                            target.write(ba, 0 /* offset in ba */, bytesRead /*
395                                                                                 * bytes
396                                                                                 * to
397                                                                                 * write
398                                                                                 */);
399                        }
400                    } else {
401                        break; // hit eof
402                    }
403                } // end while
404    
405                // C L O S E, done by caller if wanted.
406            } catch (IOException e) {
407                LOGGER.debug("IOException caught while copying streams:");
408                LOGGER.debug(e.getClass().getName() + ": " + e.getMessage());
409                LOGGER.debug(e);
410                return false;
411            }
412    
413            // all was ok
414            return true;
415        } // end copy
416    
417        /**
418         * Copies all content read from the given input stream to the given output
419         * stream. Note that this method will NOT close the streams when finished
420         * copying.
421         * 
422         * @param source
423         *            the InputStream to read the bytes from
424         * @param target
425         *            out the OutputStream to write the bytes to, may be null
426         * @return true if Inputstream copied successfully to OutputStream
427         */
428        public static boolean copyReader(Reader source, Writer target) {
429            if (source == null) {
430                throw new MCRException("Reader source is null.");
431            }
432            
433            try {
434                // R E A D / W R I T E by chunks
435                int chunkSize = 63 * 1024;
436    
437                // code will work even when chunkSize = 0 or chunks = 0;
438                // Even for small files, we allocate a big buffer, since we
439                // don't know the size ahead of time.
440                char[] ca = new char[chunkSize];
441    
442                // keep reading till hit eof
443                while (true) {
444                    int charsRead = readBlocking(source, ca, 0, chunkSize);
445    
446                    if (LOGGER.isDebugEnabled()) {
447                        LOGGER.debug(MCRUtils.class.getName() + ".copyReader(): " + charsRead + " characters read");
448                    }
449    
450                    if (charsRead > 0) {
451                        if (target != null) {
452                            target.write(ca, 0 /* offset in ba */, charsRead /*
453                                                                                 * bytes
454                                                                                 * to
455                                                                                 * write
456                                                                                 */);
457                        }
458                    } else {
459                        break; // hit eof
460                    }
461                } // end while
462    
463                // C L O S E, done by caller if wanted.
464            } catch (IOException e) {
465                return false;
466            }
467    
468            // all was ok
469            return true;
470        } // end copy
471    
472        /**
473         * merges to HashSets of MyCoreIDs after specific rules
474         * 
475         * @see #COMMAND_OR
476         * @see #COMMAND_AND
477         * @see #COMMAND_XOR
478         * @param set1
479         *            1st HashSet to be merged
480         * @param set2
481         *            2nd HashSet to be merged
482         * @param operation
483         *            available COMMAND_XYZ
484         * @return merged HashSet
485         */
486        public static final <T> HashSet<T> mergeHashSets(HashSet<? extends T> set1, HashSet<? extends T> set2, char operation) {
487            HashSet<T> merged = new HashSet<T>();
488            T id;
489    
490            switch (operation) {
491            case COMMAND_OR:
492                merged.addAll(set1);
493                merged.addAll(set2);
494    
495                break;
496    
497            case COMMAND_AND:
498    
499                for (Iterator<? extends T> it = set1.iterator(); it.hasNext();) {
500                    id = it.next();
501    
502                    if (set2.contains(id)) {
503                        merged.add(id);
504                    }
505                }
506    
507                break;
508    
509            case COMMAND_XOR:
510    
511                for (Iterator<? extends T> it = set1.iterator(); it.hasNext();) {
512                    id = it.next();
513    
514                    if (!set2.contains(id)) {
515                        merged.add(id);
516                    }
517                }
518    
519                for (Iterator<? extends T> it = set2.iterator(); it.hasNext();) {
520                    id = it.next();
521    
522                    if (!set1.contains(id) && !merged.contains(id)) {
523                        merged.add(id);
524                    }
525                }
526    
527                break;
528    
529            default:
530                throw new IllegalArgumentException("operation not permited: " + operation);
531            }
532    
533            return merged;
534        }
535    
536        /**
537         * The method cut an ArrayList for a maximum of items.
538         * 
539         * @param arrayin The incoming ArrayList
540         * @param maxitem The maximum number of items
541         * @return the cutted ArrayList
542         */
543        public static final <T> ArrayList<T> cutArrayList(ArrayList<? extends T> arrayin, int maxitems) {
544            if (arrayin == null) {
545                throw new MCRException("Input ArrayList is null.");
546            }
547    
548            if (maxitems < 1) {
549                LOGGER.warn("The maximum items are lower then 1.");
550            }
551    
552            ArrayList<T> arrayout = new ArrayList<T>();
553            int i = 0;
554    
555            for (Iterator<? extends T> it = arrayin.iterator(); it.hasNext() && (i < maxitems); i++) {
556                arrayout.add(it.next());
557            }
558            return arrayout;
559        }
560    
561        /**
562         * Reads exactly <code>len</code> bytes from the input stream into the
563         * byte array. This method reads repeatedly from the underlying stream until
564         * all the bytes are read. InputStream.read is often documented to block
565         * like this, but in actuality it does not always do so, and returns early
566         * with just a few bytes. readBlockiyng blocks until all the bytes are read,
567         * the end of the stream is detected, or an exception is thrown. You will
568         * always get as many bytes as you asked for unless you get an eof or other
569         * exception. Unlike readFully, you find out how many bytes you did get.
570         * 
571         * @param b
572         *            the buffer into which the data is read.
573         * @param off
574         *            the start offset of the data.
575         * @param len
576         *            the number of bytes to read.
577         * @return number of bytes actually read.
578         * @exception IOException
579         *                if an I/O error occurs.
580         * 
581         */
582        public static final int readBlocking(InputStream in, byte[] b, int off, int len) throws IOException {
583            int totalBytesRead = 0;
584    
585            while (totalBytesRead < len) {
586                int bytesRead = in.read(b, off + totalBytesRead, len - totalBytesRead);
587    
588                if (bytesRead < 0) {
589                    break;
590                }
591    
592                totalBytesRead += bytesRead;
593            }
594    
595            return totalBytesRead;
596        } // end readBlocking
597    
598        /**
599         * Reads exactly <code>len</code> bytes from the input stream into the
600         * byte array. This method reads repeatedly from the underlying stream until
601         * all the bytes are read. Reader.read is often documented to block like
602         * this, but in actuality it does not always do so, and returns early with
603         * just a few bytes. readBlockiyng blocks until all the bytes are read, the
604         * end of the stream is detected, or an exception is thrown. You will always
605         * get as many bytes as you asked for unless you get an eof or other
606         * exception. Unlike readFully, you find out how many bytes you did get.
607         * 
608         * @param c
609         *            the buffer into which the data is read.
610         * @param off
611         *            the start offset of the data.
612         * @param len
613         *            the number of bytes to read.
614         * @return number of bytes actually read.
615         * @exception IOException
616         *                if an I/O error occurs.
617         * 
618         */
619        public static final int readBlocking(Reader in, char[] c, int off, int len) throws IOException {
620            int totalCharsRead = 0;
621    
622            while (totalCharsRead < len) {
623                int charsRead = in.read(c, off + totalCharsRead, len - totalCharsRead);
624    
625                if (charsRead < 0) {
626                    break;
627                }
628    
629                totalCharsRead += charsRead;
630            }
631    
632            return totalCharsRead;
633        } // end readBlocking
634    
635        /**
636         * <p>
637         * Returns String in with newStr substituted for find String.
638         * 
639         * @param in
640         *            String to edit
641         * @param find
642         *            string to match
643         * @param newStr
644         *            string to substitude for find
645         */
646        public static String replaceString(String in, String find, String newStr) {
647            char[] working = in.toCharArray();
648            StringBuffer sb = new StringBuffer();
649    
650            int startindex = in.indexOf(find);
651    
652            if (startindex < 0) {
653                return in;
654            }
655    
656            int currindex = 0;
657    
658            while (startindex > -1) {
659                for (int i = currindex; i < startindex; i++) {
660                    sb.append(working[i]);
661                } // for
662    
663                currindex = startindex;
664                sb.append(newStr);
665                currindex += find.length();
666                startindex = in.indexOf(find, currindex);
667            } // while
668    
669            for (int i = currindex; i < working.length; i++) {
670                sb.append(working[i]);
671            } // for
672    
673            return sb.toString();
674        }
675    
676        /**
677         * The method wrap the org.jdom.Element in a org.jdom.Document and write it
678         * to a file.
679         * 
680         * @param elm
681         *            the JDOM Document
682         * @param xml
683         *            the File instance
684         */
685        public static final void writeElementToFile(Element elm, File xml) {
686            writeJDOMToFile((new Document()).addContent(elm), xml);
687        }
688    
689        /**
690         * The method write a given JDOM Document to a file.
691         * 
692         * @param jdom
693         *            the JDOM Document
694         * @param xml
695         *            the File instance
696         */
697        public static final void writeJDOMToFile(Document jdom, File xml) {
698            try {
699                XMLOutputter xout = new XMLOutputter(Format.getPrettyFormat());
700                BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(xml));
701                xout.output(jdom, out);
702                out.close();
703            } catch (IOException ioe) {
704                if (LOGGER.isDebugEnabled()) {
705                    ioe.printStackTrace();
706                } else {
707                    LOGGER.error("Can't write org.jdom.Document to file "+xml.getName()+".");
708                }
709            }
710        }
711    
712        /**
713         * The method wrap the org.jdom.Element in a org.jdom.Document and write it
714         * to Sysout.
715         * 
716         * @param elm
717         *            the JDOM Document
718         */
719        public static final void writeElementToSysout(Element elm) {
720            writeJDOMToSysout((new Document()).addContent(elm));
721        }
722    
723        /**
724         * The method write a given JDOM Document to the system output.
725         * 
726         * @param jdom
727         *            the JDOM Document
728         */
729        public static final void writeJDOMToSysout(Document jdom) {
730            try {
731                XMLOutputter xout = new XMLOutputter(Format.getPrettyFormat());
732                BufferedOutputStream out = new BufferedOutputStream(System.out);
733                xout.output(jdom, out);
734                out.flush();
735            } catch (IOException ioe) {
736                if (LOGGER.isDebugEnabled()) {
737                    ioe.printStackTrace();
738                } else {
739                    LOGGER.error("Can't write org.jdom.Document to Sysout.");
740                }
741            }
742        }
743    
744        /**
745         * The method return a list of all file names under the given directory and
746         * subdirectories of itself.
747         * 
748         * @param basedir
749         *            the File instance of the basic directory
750         * @return an ArrayList with file names as pathes
751         */
752        public static ArrayList<String> getAllFileNames(File basedir) {
753            ArrayList<String> out = new ArrayList<String>();
754            File[] stage = basedir.listFiles();
755    
756            for (int i = 0; i < stage.length; i++) {
757                if (stage[i].isFile()) {
758                    out.add(stage[i].getName());
759                }
760    
761                if (stage[i].isDirectory()) {
762                    out.addAll(getAllFileNames(stage[i], stage[i].getName() + SLASH));
763                }
764            }
765    
766            return out;
767        }
768    
769        /**
770         * The method return a list of all file names under the given directory and
771         * subdirectories of itself.
772         * 
773         * @param basedir
774         *            the File instance of the basic directory
775         * @param path
776         *            the part of directory path
777         * @return an ArrayList with file names as pathes
778         */
779        public static ArrayList<String> getAllFileNames(File basedir, String path) {
780            ArrayList<String> out = new ArrayList<String>();
781            File[] stage = basedir.listFiles();
782    
783            for (int i = 0; i < stage.length; i++) {
784                if (stage[i].isFile()) {
785                    out.add(path + stage[i].getName());
786                }
787    
788                if (stage[i].isDirectory()) {
789                    out.addAll(getAllFileNames(stage[i], path + stage[i].getName() + SLASH));
790                }
791            }
792    
793            return out;
794        }
795    
796        /**
797         * The method return a list of all directory names under the given directory
798         * and subdirectories of itself.
799         * 
800         * @param basedir
801         *            the File instance of the basic directory
802         * @return an ArrayList with directory names as pathes
803         */
804        public static ArrayList<String> getAllDirectoryNames(File basedir) {
805            ArrayList<String> out = new ArrayList<String>();
806            File[] stage = basedir.listFiles();
807    
808            for (int i = 0; i < stage.length; i++) {
809                if (stage[i].isDirectory()) {
810                    out.add(stage[i].getName());
811                    out.addAll(getAllDirectoryNames(stage[i], stage[i].getName() + SLASH));
812                }
813            }
814    
815            return out;
816        }
817    
818        /**
819         * The method return a list of all directory names under the given directory
820         * and subdirectories of itself.
821         * 
822         * @param basedir
823         *            the File instance of the basic directory
824         * @param path
825         *            the part of directory path
826         * @return an ArrayList with directory names as pathes
827         */
828        public static ArrayList<String> getAllDirectoryNames(File basedir, String path) {
829            ArrayList<String> out = new ArrayList<String>();
830            File[] stage = basedir.listFiles();
831    
832            for (int i = 0; i < stage.length; i++) {
833                if (stage[i].isDirectory()) {
834                    out.add(path + stage[i].getName());
835                    out.addAll(getAllDirectoryNames(stage[i], path + stage[i].getName() + SLASH));
836                }
837            }
838    
839            return out;
840        }
841    
842        public static String arrayToString(Object[] objArray, String seperator) {
843            StringBuffer buf = new StringBuffer();
844    
845            for (int i = 0; i < objArray.length; i++) {
846                buf.append(objArray[i]).append(seperator);
847            }
848    
849            if (objArray.length > 0) {
850                buf.setLength(buf.length() - seperator.length());
851            }
852    
853            return buf.toString();
854        }
855    
856        public static String parseDocumentType(InputStream in) {
857            SAXParser parser = null;
858    
859            try {
860                parser = SAXParserFactory.newInstance().newSAXParser();
861            } catch (Exception ex) {
862                String msg = "Could not build a SAX Parser for processing XML input";
863                throw new MCRConfigurationException(msg, ex);
864            }
865    
866            final Properties detected = new Properties();
867            final String forcedInterrupt = "mcr.forced.interrupt";
868    
869            DefaultHandler handler = new DefaultHandler() {
870                public void startElement(String uri, String localName, String qName, Attributes attributes) {
871                    LOGGER.debug("MCRLayoutService detected root element = " + qName);
872                    detected.setProperty("docType", qName);
873                    throw new MCRException(forcedInterrupt);
874                }
875    
876                // We would need SAX 2.0 to be able to do this, for later use:
877                public void startDTD(String name, String publicId, String systemId) {
878                    if (LOGGER.isDebugEnabled()) {
879                        LOGGER.debug(new StringBuffer(1024).append("MCRUtils detected DOCTYPE declaration = ").append(name).append(" publicId = ").append(publicId).append(" systemId = ").append(systemId).toString());
880                    }
881                    detected.setProperty("docType", name);
882                    throw new MCRException(forcedInterrupt);
883                }
884            };
885    
886            try {
887                parser.parse(new InputSource(in), handler);
888            } catch (Exception ex) {
889                if (!forcedInterrupt.equals(ex.getMessage())) {
890                    String msg = "Error while detecting XML document type from input source";
891                    throw new MCRException(msg, ex);
892                }
893            }
894    
895            return detected.getProperty("docType");
896        }
897    
898    }