001: /*
002:
003: This software is OSI Certified Open Source Software.
004: OSI Certified is a certification mark of the Open Source Initiative.
005:
006: The license (Mozilla version 1.0) can be read at the MMBase site.
007: See http://www.MMBase.org/license
008:
009: */
010: package org.mmbase.util;
011:
012: import java.io.InputStream;
013: import java.io.InputStreamReader;
014: import java.io.ByteArrayInputStream;
015: import java.util.*;
016: import java.util.concurrent.*;
017:
018: import java.lang.reflect.*;
019:
020: import org.mmbase.util.xml.ApplicationReader;
021: import org.mmbase.util.xml.BuilderReader;
022: import org.mmbase.util.xml.ModuleReader;
023: import org.mmbase.util.logging.*;
024:
025: import org.xml.sax.EntityResolver;
026: import org.xml.sax.InputSource;
027:
028: /**
029: * Take the systemId and converts it into a local file, using the MMBase config path
030: *
031: * @move org.mmbase.util.xml
032: * @rename EntityResolver
033: * @author Gerard van Enk
034: * @author Michiel Meeuwissen
035: * @version $Id: XMLEntityResolver.java,v 1.68 2007/12/06 08:19:29 michiel Exp $
036: */
037: public class XMLEntityResolver implements EntityResolver {
038:
039: public static final String DOMAIN = "http://www.mmbase.org/";
040: public static final String DTD_SUBPATH = "dtd/";
041: public static final String XMLNS_SUBPATH = "xmlns/";
042: private static final String XSD_SUBPATH = "xsd/"; // deprecated
043:
044: private static Logger log = Logging
045: .getLoggerInstance(XMLEntityResolver.class);
046: static {
047: //log.setLevel(Level.DEBUG);
048: }
049:
050: private static final String MMRESOURCES = "/org/mmbase/resources/";
051:
052: private static Map<String, Resource> publicIDtoResource = new ConcurrentHashMap<String, Resource>();
053: // This maps public id's to classes which are know to be able to parse this XML's.
054: // The package of these XML's will also contain the resources with the DTD.
055:
056: /**
057: * XSD's have only system ID
058: */
059: private static Map<String, Resource> systemIDtoResource = new ConcurrentHashMap<String, Resource>();
060:
061: /**
062: * Container for dtd resources information
063: */
064: static abstract class Resource {
065: abstract InputStream getStream();
066: }
067:
068: static class StringResource extends Resource {
069: private String string;
070:
071: StringResource(String s) {
072: string = s;
073: }
074:
075: InputStream getStream() {
076: return new ByteArrayInputStream(string.getBytes());
077: }
078: }
079:
080: static class FileResource extends Resource {
081: private final Class<?> clazz;
082: private final String file;
083:
084: FileResource(Class<?> c, String f) {
085: clazz = c;
086: file = f;
087: }
088:
089: String getResource() {
090: return "resources/" + file;
091: }
092:
093: String getFileName() {
094: return file;
095: }
096:
097: InputStream getStream() {
098: InputStream stream = null;
099: if (file != null) {
100: stream = ResourceLoader.getConfigurationRoot()
101: .getResourceAsStream(
102: DTD_SUBPATH + getFileName());
103: if (stream == null) {
104: stream = ResourceLoader.getConfigurationRoot()
105: .getResourceAsStream(
106: XMLNS_SUBPATH + getFileName());
107: }
108: if (stream == null) {
109: // XXX I think this was deprecated in favour in xmlns/ (all in 1.8), so perhaps this can be dropped
110: stream = ResourceLoader.getConfigurationRoot()
111: .getResourceAsStream(
112: XSD_SUBPATH + getFileName());
113: }
114: }
115: if (stream == null && clazz != null) {
116: stream = clazz.getResourceAsStream(getResource());
117: }
118:
119: return stream;
120: }
121:
122: public String toString() {
123: return file + ": " + clazz;
124: }
125:
126: }
127:
128: static {
129: // ask known (core) xml readers to register their public ids and dtds
130: // the advantage of doing it this soon, is that the 1DTD are know as early as possible.
131: org.mmbase.util.xml.DocumentReader.registerPublicIDs();
132: BuilderReader.registerPublicIDs();
133: BuilderReader.registerSystemIDs();
134: ApplicationReader.registerPublicIDs();
135: ModuleReader.registerPublicIDs();
136: org.mmbase.util.xml.UtilReader.registerPublicIDs();
137: org.mmbase.bridge.util.xml.query.QueryReader
138: .registerSystemIDs();
139:
140: registerSystemID("http://www.w3.org/2001/03/xml.xsd",
141: "xml.xsd", null);
142: registerSystemID("http://www.w3.org/2001/03/XMLSchema.dtd",
143: "XMLSchema.dtd", null);
144: registerSystemID("http://www.w3.org/2001/03/datatypes.dtd",
145: "datatypes.dtd", null);
146:
147: //registerSystemID("http://www.oasis-open.org/docbook/xml/4.1.2/docbookx.dtd", "docbookx.dtd", null);
148: //registerSystemID("http://www.oasis-open.org/docbook/xml/4.1.2/dbnotnx.mod", "dbnotnx.mod", null);
149: }
150:
151: /**
152: * Register a given publicID, binding it to a resource determined by a given class and resource filename
153: * @param publicID the Public ID to register
154: * @param dtd the name of the resourcefile
155: * @param c the class indicating the location of the resource in the pacakage structure. The
156: * resource is to be found in the 'resources' package under the package of the class.
157: * @since MMBase-1.7
158: */
159: public static void registerPublicID(String publicID, String dtd,
160: Class<?> c) {
161: publicIDtoResource.put(publicID, new FileResource(c, dtd));
162: if (log.isDebugEnabled())
163: log.debug("publicIDtoResource: " + publicID + " " + dtd
164: + c.getName());
165: }
166:
167: /**
168: * It seems that in XSD's you don't have public id's. So, this makes it possible to use system id.
169: * @todo EXPERIMENTAL
170: * @since MMBase-1.8
171: */
172: public static void registerSystemID(String systemID, String xsd,
173: Class<?> c) {
174: systemIDtoResource.put(systemID, new FileResource(c, xsd));
175: }
176:
177: private String definitionPath;
178:
179: private boolean hasDefinition; // tells whether or not a DTD/XSD is set - if not, no validition can take place
180:
181: private boolean validate;
182: private Class<?> resolveBase;
183:
184: /**
185: * empty constructor
186: */
187: public XMLEntityResolver() {
188: this (true);
189: }
190:
191: public XMLEntityResolver(boolean v) {
192: this (v, null);
193: }
194:
195: public XMLEntityResolver(boolean v, Class<?> base) {
196: hasDefinition = false;
197: definitionPath = null;
198: validate = v;
199: resolveBase = base;
200: }
201:
202: protected static StringBuilder camelAppend(StringBuilder sb,
203: String s) {
204: for (int i = 0; i < s.length(); i++) {
205: char c = s.charAt(i);
206: if (Character.isUpperCase(c)) {
207: sb.append(Character.toLowerCase(c));
208: } else {
209: sb.append(s.substring(i));
210: break;
211: }
212: }
213: return sb;
214: }
215:
216: protected static void appendEntities(StringBuilder sb, Object o,
217: String prefix, int level, Set<Object> os) {
218: os.add(o);
219: org.mmbase.util.transformers.Identifier identifier = new org.mmbase.util.transformers.Identifier();
220: if (o instanceof Map) {
221: Set<Map.Entry<?, ?>> map = ((Map) o).entrySet();
222: for (Map.Entry<?, ?> entry : map) {
223: Object value = entry.getValue();
224: if (value != null
225: && Casting.isStringRepresentable(value
226: .getClass())
227: && entry.getKey() instanceof String) {
228: sb.append("<!ENTITY ");
229: sb.append(prefix);
230: sb.append('.');
231: String k = identifier.transform((String) entry
232: .getKey());
233: k = k.replaceAll("\\s", "");
234: sb.append(k);
235: sb
236: .append(" \""
237: + org.mmbase.util.transformers.Xml
238: .XMLAttributeEscape(""
239: + value, '"')
240: + "\">\n");
241: }
242: if (level < 3
243: && value != null
244: && !os.contains(value)
245: && !value.getClass().getName().startsWith(
246: "java.lang")) { // recursion to acces also properties of this
247: appendEntities(sb, value, prefix + "."
248: + entry.getKey(), level + 1, os);
249: }
250: }
251: } else {
252: for (Method m : o.getClass().getMethods()) {
253: String name = m.getName();
254: if (m.getParameterTypes().length == 0
255: && !name.equals("getNodes")
256: && !name.equals("getConnection")
257: && // see MMB-1490, we should not call
258: // getConnection, while we won't close it.
259: name.length() > 3 && name.startsWith("get")
260: && Character.isUpperCase(name.charAt(3))) {
261: try {
262: Class<?> rt = m.getReturnType();
263: boolean invoked = false;
264: Object value = null;
265: if (Casting.isStringRepresentable(rt)) {
266: if (!Map.class.isAssignableFrom(rt)
267: && !Collection.class
268: .isAssignableFrom(rt)) {
269: value = m.invoke(o);
270: invoked = true;
271: sb.append("<!ENTITY ");
272: sb.append(prefix);
273: sb.append('.');
274: camelAppend(sb, name.substring(3));
275: sb
276: .append(" \""
277: + org.mmbase.util.transformers.Xml
278: .XMLAttributeEscape(
279: ""
280: + value,
281: '"')
282: + "\">\n");
283: }
284: }
285: if (!rt.getName().startsWith("java.lang")) {
286: if (!invoked)
287: value = m.invoke(o);
288: if (level < 3 && value != null
289: && !os.contains(value)) {
290: appendEntities(sb, value, prefix
291: + "."
292: + camelAppend(
293: new StringBuilder(),
294: name.substring(3)),
295: level + 1, os);
296: }
297: }
298: } catch (IllegalAccessException ia) {
299: log.debug(ia);
300: } catch (InvocationTargetException ite) {
301: log.debug(ite);
302: } catch (AbstractMethodError ame) {
303: log.debug(ame);
304: }
305: }
306: }
307: }
308: }
309:
310: protected static String ents = null;
311: protected static boolean logEnts = true;
312:
313: protected static synchronized String getMMEntities() {
314: if (ents == null) {
315: StringBuilder sb = new StringBuilder();
316: try {
317: org.mmbase.module.Module mmbase = org.mmbase.module.Module
318: .getModule("mmbaseroot", false);
319: if (mmbase != null) {
320: appendEntities(sb, mmbase, "mmbase", 0,
321: new HashSet<Object>());
322: } else {
323: return sb.toString();
324: }
325: } catch (Throwable ie) {
326: log.warn(ie.getMessage());
327: return sb.toString();
328: }
329: ents = sb.toString();
330: if (logEnts) {
331: log.debug("Using entities\n" + ents);
332: }
333: }
334: return ents;
335: }
336:
337: public static void clearMMEntities(boolean le) {
338: ents = null;
339: logEnts = le;
340: }
341:
342: /**
343: * Takes the systemId and returns the local location of the dtd/xsd
344: */
345: public InputSource resolveEntity(final String publicId,
346: final String systemId) {
347: if (log.isDebugEnabled()) {
348: log.debug("resolving PUBLIC " + publicId + " SYSTEM "
349: + systemId);
350: }
351:
352: InputStream definitionStream = null;
353:
354: if ("http://www.mmbase.org/mmentities.ent".equals(systemId)) {
355: //StringBuilder sb = new StringBuilder();
356: //Class c = org.mmbase.framework.Framework.class;
357: String ents = getMMEntities();
358: if (log.isDebugEnabled()) {
359: log.debug("Using entities\n" + ents);
360: }
361: definitionStream = new StringResource(ents).getStream();
362: } else if (publicId != null) {
363: // first try with publicID or namespace
364: Resource res = publicIDtoResource.get(publicId);
365: log.debug("Found publicId " + publicId + " -> " + res);
366: definitionStream = res == null ? null : res.getStream();
367: }
368:
369: log.debug("Get definition stream by public id: "
370: + definitionStream);
371:
372: if (definitionStream == null) {
373: Resource res = systemIDtoResource.get(systemId);
374: if (res != null) {
375: definitionStream = res.getStream();
376: }
377: }
378:
379: if (definitionStream == null) { // not succeeded with publicid, go trying with systemId
380:
381: //does systemId contain a mmbase-dtd
382: if ((systemId == null) || (!systemId.startsWith(DOMAIN))) {
383: // it's a systemId we can't do anything with,
384: // so let the parser decide what to do
385:
386: if (validate) {
387: log
388: .debug("Cannot resolve "
389: + systemId
390: + ", but needed for validation leaving to parser.");
391: log.debug("Find culpit: ", new Exception());
392: return null;
393: } else {
394: // perhaps this should not be done if it is about resolving _entities_ rather then dtd.
395: log
396: .debug("Not validating, no need to resolve DTD (?), returning empty resource for "
397: + systemId);
398: return new InputSource(new ByteArrayInputStream(
399: new byte[0]));
400: }
401: } else {
402: log.debug("mmbase resource");
403: String mmResource = systemId.substring(22);
404: // first, try MMBase config directory (if initialized)
405: definitionStream = ResourceLoader
406: .getConfigurationRoot().getResourceAsStream(
407: mmResource);
408: if (definitionStream == null) {
409: Class<?> base = resolveBase; // if resolveBase was specified, use that.
410: Resource res = null;
411: if (base != null) {
412: if (mmResource.startsWith("xmlns/")) {
413: res = new FileResource(base, mmResource
414: .substring(6));
415: } else {
416: res = new FileResource(base, mmResource
417: .substring(4)); // dtd or xsd
418: }
419: }
420: if (res != null) {
421: definitionStream = res.getStream();
422: if (definitionStream == null) {
423: log.warn("Could not find " + res.toString()
424: + " in " + base.getName()
425: + ", falling back to "
426: + MMRESOURCES + " while resolving "
427: + systemId + " " + publicId);
428: base = null; // try it in org.mmbase.resources too.
429: }
430: }
431:
432: if (base == null) {
433: String resource = MMRESOURCES + mmResource;
434: if (log.isDebugEnabled())
435: log
436: .debug("Getting document definition as resource "
437: + resource);
438: definitionStream = getClass()
439: .getResourceAsStream(resource);
440: }
441: }
442: if (definitionStream == null) {
443: if (resolveBase != null) {
444: log
445: .error("Could not find MMBase entity '"
446: + publicId
447: + " "
448: + systemId
449: + "' (did you make a typo?), returning null, system id will be used (needing a connection, or put in config dir)");
450: } else {
451: log
452: .service("Could not find MMBase entity '"
453: + publicId
454: + " "
455: + systemId
456: + "' (did you make a typo?), returning null, system id will be used (needing a connection, or put in config dir)");
457: }
458: // not sure, probably should return 'null' after all, then it will be resolved with internet.
459: // but this can not happen, in fact...
460: //return new InputSource(new StringReader(""));
461: // FAILED
462: return null;
463: }
464: }
465: }
466: hasDefinition = true;
467:
468: InputStreamReader definitionInputStreamReader = new InputStreamReader(
469: definitionStream);
470: InputSource definitionInputSource = new InputSource();
471: if (systemId != null) {
472: definitionInputSource.setSystemId(systemId);
473: }
474: if (publicId != null) {
475: definitionInputSource.setPublicId(publicId);
476: }
477: definitionInputSource
478: .setCharacterStream(definitionInputStreamReader);
479: return definitionInputSource;
480: }
481:
482: /**
483: * @return whether the resolver has determined a DTD
484: */
485: public boolean hasDTD() {
486: return hasDefinition;
487: }
488:
489: /**
490: * @return The actually used path to the DTD
491: */
492: public String getDTDPath() {
493: return definitionPath;
494: }
495: }
|