1/*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements.  See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License.  You may obtain a copy of the License at
8 *
9 *     http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17// $Id: SchemaFactoryFinder.java 727367 2008-12-17 13:05:26Z mrglavas $
18
19package javax.xml.validation;
20
21import java.io.BufferedReader;
22import java.io.File;
23import java.io.FileInputStream;
24import java.io.IOException;
25import java.io.InputStream;
26import java.io.InputStreamReader;
27import java.net.URL;
28import java.util.Collections;
29import java.util.Enumeration;
30import java.util.Iterator;
31import java.util.Properties;
32import javax.xml.XMLConstants;
33import libcore.io.IoUtils;
34
35/**
36 * Implementation of {@link SchemaFactory#newInstance(String)}.
37 *
38 * @author <a href="Kohsuke.Kawaguchi@Sun.com">Kohsuke Kawaguchi</a>
39 * @version $Revision: 727367 $, $Date: 2008-12-17 05:05:26 -0800 (Wed, 17 Dec 2008) $
40 * @since 1.5
41 */
42final class SchemaFactoryFinder  {
43
44    /** XML Schema language identifiers. */
45    private static final String W3C_XML_SCHEMA10_NS_URI = "http://www.w3.org/XML/XMLSchema/v1.0";
46    private static final String W3C_XML_SCHEMA11_NS_URI = "http://www.w3.org/XML/XMLSchema/v1.1";
47
48    /** debug support code. */
49    private static boolean debug = false;
50
51    /**
52     * <p>Cache properties for performance.</p>
53     */
54    private static Properties cacheProps = new Properties();
55
56    /**
57     * <p>First time requires initialization overhead.</p>
58     */
59    private static boolean firstTime = true;
60
61    /**
62     * Default columns per line.
63     */
64    private static final int DEFAULT_LINE_LENGTH = 80;
65
66    static {
67        String val = System.getProperty("jaxp.debug");
68        // Allow simply setting the prop to turn on debug
69        debug = val != null && (! "false".equals(val));
70    }
71
72    /**
73     * <p>Conditional debug printing.</p>
74     *
75     * @param msg to print
76     */
77    private static void debugPrintln(String msg) {
78        if (debug) {
79            System.err.println("JAXP: " + msg);
80        }
81    }
82
83    /**
84     * <p><code>ClassLoader</code> to use to find <code>SchemaFactory</code>.</p>
85     */
86    private final ClassLoader classLoader;
87
88    /**
89     * <p>Constructor that specifies <code>ClassLoader</code> to use
90     * to find <code>SchemaFactory</code>.</p>
91     *
92     * @param loader
93     *      to be used to load resource, {@link SchemaFactory}, and
94     *      {@link SchemaFactoryLoader} implementations during
95     *      the resolution process.
96     *      If this parameter is null, the default system class loader
97     *      will be used.
98     */
99    public SchemaFactoryFinder(ClassLoader loader) {
100        this.classLoader = loader;
101        if( debug ) {
102            debugDisplayClassLoader();
103        }
104    }
105
106    private void debugDisplayClassLoader() {
107        if (classLoader == Thread.currentThread().getContextClassLoader()) {
108            debugPrintln("using thread context class loader ("+classLoader+") for search");
109            return;
110        }
111
112        if (classLoader == ClassLoader.getSystemClassLoader()) {
113            debugPrintln("using system class loader ("+classLoader+") for search");
114            return;
115        }
116
117        debugPrintln("using class loader (" + classLoader + ") for search");
118    }
119
120    /**
121     * <p>Creates a new {@link SchemaFactory} object for the specified
122     * schema language.</p>
123     *
124     * @param schemaLanguage
125     *      See {@link SchemaFactory Schema Language} table in <code>SchemaFactory</code>
126     *      for the list of available schema languages.
127     *
128     * @return <code>null</code> if the callee fails to create one.
129     *
130     * @throws NullPointerException
131     *      If the <tt>schemaLanguage</tt> parameter is null.
132     */
133    public SchemaFactory newFactory(String schemaLanguage) {
134        if (schemaLanguage == null) {
135            throw new NullPointerException("schemaLanguage == null");
136        }
137        SchemaFactory f = _newFactory(schemaLanguage);
138        if (debug) {
139            if (f != null) {
140                debugPrintln("factory '" + f.getClass().getName() + "' was found for " + schemaLanguage);
141            } else {
142                debugPrintln("unable to find a factory for " + schemaLanguage);
143            }
144        }
145        return f;
146    }
147
148    /**
149     * <p>Lookup a <code>SchemaFactory</code> for the given <code>schemaLanguage</code>.</p>
150     *
151     * @param schemaLanguage Schema language to lookup <code>SchemaFactory</code> for.
152     *
153     * @return <code>SchemaFactory</code> for the given <code>schemaLanguage</code>.
154     */
155    private SchemaFactory _newFactory(String schemaLanguage) {
156        SchemaFactory sf;
157        String propertyName = SERVICE_CLASS.getName() + ":" + schemaLanguage;
158
159        // system property look up
160        try {
161            if (debug) debugPrintln("Looking up system property '"+propertyName+"'" );
162            String r = System.getProperty(propertyName);
163            if (r != null && r.length() > 0) {
164                if (debug) debugPrintln("The value is '"+r+"'");
165                sf = createInstance(r);
166                if(sf!=null)    return sf;
167            }
168            else if (debug) {
169                debugPrintln("The property is undefined.");
170            }
171        }
172        // The VM ran out of memory or there was some other serious problem. Re-throw.
173        catch (VirtualMachineError vme) {
174            throw vme;
175        }
176        // ThreadDeath should always be re-thrown
177        catch (ThreadDeath td) {
178            throw td;
179        }
180        catch (Throwable t) {
181            if( debug ) {
182                debugPrintln("failed to look up system property '"+propertyName+"'" );
183                t.printStackTrace();
184            }
185        }
186
187        String javah = System.getProperty("java.home");
188        String configFile = javah + File.separator +
189        "lib" + File.separator + "jaxp.properties";
190
191        String factoryClassName = null ;
192
193        // try to read from $java.home/lib/jaxp.properties
194        try {
195            if(firstTime){
196                synchronized(cacheProps){
197                    if(firstTime){
198                        File f=new File( configFile );
199                        firstTime = false;
200                        if(f.exists()){
201                            if (debug) debugPrintln("Read properties file " + f);
202                            cacheProps.load(new FileInputStream(f));
203                        }
204                    }
205                }
206            }
207            factoryClassName = cacheProps.getProperty(propertyName);
208            if (debug) debugPrintln("found " + factoryClassName + " in $java.home/jaxp.properties");
209
210            if (factoryClassName != null) {
211                sf = createInstance(factoryClassName);
212                if(sf != null){
213                    return sf;
214                }
215            }
216        } catch (Exception ex) {
217            if (debug) {
218                ex.printStackTrace();
219            }
220        }
221
222        // try META-INF/services files
223        for (URL resource : createServiceFileIterator()) {
224            if (debug) debugPrintln("looking into " + resource);
225            try {
226                sf = loadFromServicesFile(schemaLanguage,resource.toExternalForm(),
227                        resource.openStream());
228                if(sf!=null)    return sf;
229            } catch(IOException e) {
230                if( debug ) {
231                    debugPrintln("failed to read "+resource);
232                    e.printStackTrace();
233                }
234            }
235        }
236
237        // platform defaults
238        if (schemaLanguage.equals(XMLConstants.W3C_XML_SCHEMA_NS_URI) || schemaLanguage.equals(W3C_XML_SCHEMA10_NS_URI)) {
239            if (debug) debugPrintln("attempting to use the platform default XML Schema 1.0 validator");
240            return createInstance("org.apache.xerces.jaxp.validation.XMLSchemaFactory");
241        }
242        else if (schemaLanguage.equals(W3C_XML_SCHEMA11_NS_URI)) {
243            if (debug) debugPrintln("attempting to use the platform default XML Schema 1.1 validator");
244            return createInstance("org.apache.xerces.jaxp.validation.XMLSchema11Factory");
245        }
246
247        if (debug) debugPrintln("all things were tried, but none was found. bailing out.");
248        return null;
249    }
250
251    /**
252     * <p>Creates an instance of the specified and returns it.</p>
253     *
254     * @param className
255     *      fully qualified class name to be instantiated.
256     *
257     * @return null
258     *      if it fails. Error messages will be printed by this method.
259     */
260    SchemaFactory createInstance( String className ) {
261        try {
262            if (debug) debugPrintln("instantiating "+className);
263            Class clazz;
264            if( classLoader!=null )
265                clazz = classLoader.loadClass(className);
266            else
267                clazz = Class.forName(className);
268            if(debug)       debugPrintln("loaded it from "+which(clazz));
269            Object o = clazz.newInstance();
270
271            if( o instanceof SchemaFactory )
272                return (SchemaFactory)o;
273
274            if (debug) debugPrintln(className+" is not assignable to "+SERVICE_CLASS.getName());
275        }
276        // The VM ran out of memory or there was some other serious problem. Re-throw.
277        catch (VirtualMachineError vme) {
278            throw vme;
279        }
280        // ThreadDeath should always be re-thrown
281        catch (ThreadDeath td) {
282            throw td;
283        }
284        catch (Throwable t) {
285            debugPrintln("failed to instantiate "+className);
286            if(debug)   t.printStackTrace();
287        }
288        return null;
289    }
290
291    /**
292     * Returns an {@link Iterator} that enumerates all
293     * the META-INF/services files that we care.
294     */
295    private Iterable<URL> createServiceFileIterator() {
296        if (classLoader == null) {
297            ClassLoader classLoader = SchemaFactoryFinder.class.getClassLoader();
298            return Collections.singleton(classLoader.getResource(SERVICE_ID));
299        } else {
300            try {
301                Enumeration<URL> e = classLoader.getResources(SERVICE_ID);
302                if (debug && !e.hasMoreElements()) {
303                    debugPrintln("no "+SERVICE_ID+" file was found");
304                }
305
306                // wrap it into an Iterator.
307                return Collections.list(e);
308            } catch (IOException e) {
309                if (debug) {
310                    debugPrintln("failed to enumerate resources "+SERVICE_ID);
311                    e.printStackTrace();
312                }
313                return Collections.emptySet();
314            }
315        }
316    }
317
318    /** Searches for a SchemaFactory for a given schema language in a META-INF/services file. */
319    private SchemaFactory loadFromServicesFile(String schemaLanguage, String resourceName, InputStream in) {
320
321        if (debug) debugPrintln("Reading "+resourceName );
322
323        // Read the service provider name in UTF-8 as specified in
324        // the jar spec.  Unfortunately this fails in Microsoft
325        // VJ++, which does not implement the UTF-8
326        // encoding. Theoretically, we should simply let it fail in
327        // that case, since the JVM is obviously broken if it
328        // doesn't support such a basic standard.  But since there
329        // are still some users attempting to use VJ++ for
330        // development, we have dropped in a fallback which makes a
331        // second attempt using the platform's default encoding. In
332        // VJ++ this is apparently ASCII, which is a subset of
333        // UTF-8... and since the strings we'll be reading here are
334        // also primarily limited to the 7-bit ASCII range (at
335        // least, in English versions), this should work well
336        // enough to keep us on the air until we're ready to
337        // officially decommit from VJ++. [Edited comment from
338        // jkesselm]
339        BufferedReader rd;
340        try {
341            rd = new BufferedReader(new InputStreamReader(in, "UTF-8"), DEFAULT_LINE_LENGTH);
342        } catch (java.io.UnsupportedEncodingException e) {
343            rd = new BufferedReader(new InputStreamReader(in), DEFAULT_LINE_LENGTH);
344        }
345
346        String factoryClassName = null;
347        SchemaFactory resultFactory = null;
348        // See spec for provider-configuration files: http://java.sun.com/j2se/1.5.0/docs/guide/jar/jar.html#Provider%20Configuration%20File
349        while (true) {
350            try {
351                factoryClassName = rd.readLine();
352            } catch (IOException x) {
353                // No provider found
354                break;
355            }
356            if (factoryClassName != null) {
357                // Ignore comments in the provider-configuration file
358                int hashIndex = factoryClassName.indexOf('#');
359                if (hashIndex != -1) {
360                    factoryClassName = factoryClassName.substring(0, hashIndex);
361                }
362
363                // Ignore leading and trailing whitespace
364                factoryClassName = factoryClassName.trim();
365
366                // If there's no text left or if this was a blank line, go to the next one.
367                if (factoryClassName.length() == 0) {
368                    continue;
369                }
370
371                try {
372                    // Found the right SchemaFactory if its isSchemaLanguageSupported(schemaLanguage) method returns true.
373                    SchemaFactory foundFactory = (SchemaFactory) createInstance(factoryClassName);
374                    if (foundFactory.isSchemaLanguageSupported(schemaLanguage)) {
375                        resultFactory = foundFactory;
376                        break;
377                    }
378                }
379                catch (Exception ignored) {}
380            }
381            else {
382                break;
383            }
384        }
385
386        IoUtils.closeQuietly(rd);
387
388        return resultFactory;
389    }
390
391    private static final Class SERVICE_CLASS = SchemaFactory.class;
392    private static final String SERVICE_ID = "META-INF/services/" + SERVICE_CLASS.getName();
393
394    private static String which( Class clazz ) {
395        return which( clazz.getName(), clazz.getClassLoader() );
396    }
397
398    /**
399     * <p>Search the specified classloader for the given classname.</p>
400     *
401     * @param classname the fully qualified name of the class to search for
402     * @param loader the classloader to search
403     *
404     * @return the source location of the resource, or null if it wasn't found
405     */
406    private static String which(String classname, ClassLoader loader) {
407        String classnameAsResource = classname.replace('.', '/') + ".class";
408
409        if (loader == null)  loader = ClassLoader.getSystemClassLoader();
410
411        URL it = loader.getResource(classnameAsResource);
412        return it != null ? it.toString() : null;
413    }
414}
415