001: /*
002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003: *
004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
005: *
006: * The contents of this file are subject to the terms of either the GNU
007: * General Public License Version 2 only ("GPL") or the Common
008: * Development and Distribution License("CDDL") (collectively, the
009: * "License"). You may not use this file except in compliance with the
010: * License. You can obtain a copy of the License at
011: * http://www.netbeans.org/cddl-gplv2.html
012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
013: * specific language governing permissions and limitations under the
014: * License. When distributing the software, include this License Header
015: * Notice in each file and include the License file at
016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
017: * particular file as subject to the "Classpath" exception as provided
018: * by Sun in the GPL Version 2 section of the License file that
019: * accompanied this code. If applicable, add the following below the
020: * License Header, with the fields enclosed by brackets [] replaced by
021: * your own identifying information:
022: * "Portions Copyrighted [year] [name of copyright owner]"
023: *
024: * Contributor(s):
025: *
026: * The Original Software is NetBeans. The Initial Developer of the Original
027: * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
028: * Microsystems, Inc. All Rights Reserved.
029: *
030: * If you wish your version of this file to be governed by only the CDDL
031: * or only the GPL Version 2, indicate your decision by adding
032: * "[Contributor] elects to include this software in this distribution
033: * under the [CDDL or GPL Version 2] license." If you do not indicate a
034: * single choice of license, a recipient has the option to distribute
035: * your version of this file under either the CDDL, the GPL Version 2 or
036: * to extend the choice of license to its licensees as provided above.
037: * However, if you add GPL Version 2 code and therefore, elected the GPL
038: * Version 2 license, then the option applies only if the new code is
039: * made subject to such option by the copyright holder.
040: */
041: package org.netbeans.spi.xml.cookies;
042:
043: import java.io.*;
044: import java.net.*;
045: import java.util.*;
046: import java.security.ProtectionDomain;
047: import java.security.CodeSource;
048:
049: import javax.xml.parsers.SAXParser;
050: import javax.xml.parsers.SAXParserFactory;
051: import javax.swing.text.Document;
052:
053: import org.openide.cookies.*;
054: import org.openide.util.*;
055: import org.openide.filesystems.FileStateInvalidException;
056: import org.openide.ErrorManager;
057:
058: import org.xml.sax.*;
059: import org.xml.sax.helpers.DefaultHandler;
060:
061: import org.netbeans.api.xml.cookies.*;
062: import org.netbeans.api.xml.services.*;
063: import org.netbeans.api.xml.parsers.*;
064:
065: /**
066: * <code>CheckXMLCookie</code> and <code>ValidateXMLCookie</code> cookie
067: * implementation support simplifing cookie providers based on
068: * <code>InputSource</code>s representing XML documents and entities.
069: *
070: * @author Petr Kuzel
071: * @see CheckXMLSupport
072: * @see ValidateXMLSupport
073: */
074: class SharedXMLSupport {
075:
076: // it will viasualize our results
077: private CookieObserver console;
078:
079: // associated input source
080: private final InputSource inputSource;
081:
082: // one of above modes CheckXMLSupport modes
083: private final int mode;
084:
085: // error locator or null
086: private Locator locator;
087:
088: // fatal error counter
089: private int fatalErrors;
090:
091: // error counter
092: private int errors;
093:
094: /**
095: * Xerces parser tries to search for every namespace declaration
096: * related Schema. It causes trouble it DTD like XHTML defines
097: * default xmlns attribute. It is then inherited by all descendants
098: * and grammar is loaded again and again. Entity resolver set this
099: * flag once it spots (null, null) resolution request that is typical
100: * for bogus Schema location resolution requests.
101: */
102: private boolean bogusSchemaRequest;
103:
104: // If true then the first bogust schema grammar request is reported
105: // all subsequent ones are supressed.
106: private boolean reportBogusSchemaRequest = Boolean
107: .getBoolean("netbeans.xml.reportBogusSchemaLocation"); // NOI18N
108:
109: /**
110: * Create new CheckXMLSupport for given InputSource in DOCUMENT_MODE.
111: * @param inputSource Supported InputSource.
112: */
113: public SharedXMLSupport(InputSource inputSource) {
114: this (inputSource, CheckXMLSupport.DOCUMENT_MODE);
115: }
116:
117: /**
118: * Create new CheckXMLSupport for given data object
119: * @param inputSource Supported InputSource.
120: * @param mode one of <code>*_MODE</code> constants
121: */
122: public SharedXMLSupport(InputSource inputSource, int mode) {
123:
124: if (inputSource == null)
125: throw new NullPointerException();
126: if (mode < CheckXMLSupport.CHECK_ENTITY_MODE
127: || mode > CheckXMLSupport.DOCUMENT_MODE) {
128: throw new IllegalArgumentException();
129: }
130:
131: this .inputSource = inputSource;
132: this .mode = mode;
133: }
134:
135: // inherit JavaDoc
136: boolean checkXML(CookieObserver l) {
137: try {
138: console = l;
139:
140: parse(false);
141:
142: return fatalErrors == 0;
143: } finally {
144: console = null;
145: locator = null;
146: }
147: }
148:
149: // inherit JavaDoc
150: boolean validateXML(CookieObserver l) {
151: try {
152: console = l;
153:
154: if (mode != CheckXMLSupport.DOCUMENT_MODE) {
155: sendMessage(Util.THIS.getString("MSG_not_a_doc"));
156: return false;
157: } else {
158: parse(true);
159: return errors == 0 && fatalErrors == 0;
160: }
161: } finally {
162: console = null;
163: locator = null;
164: }
165: }
166:
167: /**
168: * Perform parsing in current thread.
169: */
170: private void parse(boolean validate) {
171:
172: fatalErrors = 0;
173: errors = 0;
174:
175: String checkedFile = inputSource.getSystemId();
176: sendMessage(Util.THIS.getString("MSG_checking", checkedFile));
177:
178: Handler handler = new Handler();
179:
180: InputSource input = null;
181:
182: try {
183: // set up parser
184: XMLReader parser = createParser(validate);
185: if (parser == null) {
186: fatalErrors++;
187: console.receive(new CookieMessage(Util.THIS
188: .getString("MSG_cannot_create_parser"),
189: CookieMessage.FATAL_ERROR_LEVEL));
190: return;
191: }
192:
193: if (validate) {
194: // get all naemspaces for the parser
195: input = ShareableInputSource
196: .create(createInputSource());
197: String[] schemaLocations = getSchemaLocations(input);
198: try {
199: ((ShareableInputSource) input).reset();
200: } catch (IOException e) {
201: //mark invalidated - we overlapped the buffer size. Ok, recreate the InputSource
202: //no need to use the shareable - it is read only once
203: input = createInputSource();
204: }
205: if (schemaLocations != null
206: && schemaLocations.length > 0) {
207: boolean first = true;
208: StringBuffer sb = new StringBuffer();
209: for (int i = 0; i < schemaLocations.length; i++) {
210: sb.append(first ? schemaLocations[i] : " "
211: + schemaLocations[i]);
212: first = false;
213: }
214: parser
215: .setProperty(
216: "http://apache.org/xml/properties/schema/external-schemaLocation",
217: sb.toString()); //NOI18N
218: }
219: } else
220: input = createInputSource();
221:
222: parser.setErrorHandler(handler);
223: parser.setContentHandler(handler);
224:
225: if (Util.THIS.isLoggable()) {
226: Util.THIS.debug(checkedFile + ":"
227: + parserDescription(parser));
228: }
229:
230: // parse
231: if (mode == CheckXMLSupport.CHECK_ENTITY_MODE) {
232: new SAXEntityParser(parser, true).parse(input);
233: } else if (mode == CheckXMLSupport.CHECK_PARAMETER_ENTITY_MODE) {
234: new SAXEntityParser(parser, false).parse(input);
235: } else {
236: parser.parse(input);
237: }
238:
239: } catch (SAXException ex) {
240:
241: // same as one catched by ErrorHandler
242: // because we do not have content handler
243:
244: } catch (FileStateInvalidException ex) {
245:
246: // bad luck report as fatal error
247: handler.fatalError(new SAXParseException(ex
248: .getLocalizedMessage(), locator, ex));
249:
250: } catch (IOException ex) {
251:
252: // bad luck probably because cannot resolve entity
253: // report as error at -1,-1 if we do not have Locator
254: handler.fatalError(new SAXParseException(ex
255: .getLocalizedMessage(), locator, ex));
256:
257: } catch (RuntimeException ex) {
258:
259: handler.runtimeError(ex);
260: } finally {
261: if (input instanceof ShareableInputSource)
262: try {
263: ((ShareableInputSource) input).closeAll();
264: } catch (IOException ex) {
265: }
266: }
267:
268: }
269:
270: /**
271: * Parametrizes default parser creatin process. Default implementation
272: * takes user's catalog entity resolver.
273: * @return EntityResolver entity resolver or <code>null</code>
274: */
275: protected EntityResolver createEntityResolver() {
276: UserCatalog catalog = UserCatalog.getDefault();
277: return catalog == null ? null : catalog.getEntityResolver();
278: }
279:
280: /**
281: * Create InputSource to be checked.
282: * @throws IOException if I/O error occurs.
283: * @return InputSource never <code>null</code>
284: */
285: protected InputSource createInputSource() throws IOException {
286: return inputSource;
287: }
288:
289: /**
290: * Create and preconfigure new parser. Default implementation uses JAXP.
291: * @param validate true if validation module is required
292: * @return SAX reader that is used for command performing or <code>null</code>
293: * @see #createEntityResolver
294: */
295: protected XMLReader createParser(boolean validate) {
296:
297: XMLReader ret = null;
298: final String XERCES_FEATURE_PREFIX = "http://apache.org/xml/features/"; // NOI18N
299: final String XERCES_PROPERTY_PREFIX = "http://apache.org/xml/properties/"; // NOI18N
300:
301: // JAXP plugin parser (bastarded by core factories!)
302:
303: SAXParserFactory factory = SAXParserFactory.newInstance();
304: factory.setNamespaceAware(true);
305: factory.setValidating(validate);
306:
307: //??? It is Xerces specifics, but no general API for XML Schema based validation exists
308: if (validate) {
309: try {
310: factory.setFeature(XERCES_FEATURE_PREFIX
311: + "validation/schema", validate); // NOI18N
312: } catch (Exception ex) {
313: sendMessage(Util.THIS.getString("MSG_parser_no_schema"));
314: }
315: }
316:
317: try {
318: SAXParser parser = factory.newSAXParser();
319: ret = parser.getXMLReader();
320: } catch (Exception ex) {
321: sendMessage(Util.THIS.getString("MSG_parser_err_1"));
322: return null;
323: }
324:
325: if (ret != null) {
326: EntityResolver res = createEntityResolver();
327: if (res != null)
328: ret.setEntityResolver(new VerboseEntityResolver(res));
329: }
330:
331: return ret;
332:
333: }
334:
335: /**
336: * It may be helpfull for tracing down some oddities.
337: */
338: private String parserDescription(XMLReader parser) {
339:
340: // report which parser implementation is used
341:
342: Class klass = parser.getClass();
343: try {
344: ProtectionDomain domain = klass.getProtectionDomain();
345: CodeSource source = domain.getCodeSource();
346:
347: if (source == null
348: && (klass.getClassLoader() == null || klass
349: .getClassLoader().equals(
350: Object.class.getClassLoader()))) {
351: return Util.THIS.getString("MSG_platform_parser");
352: } else if (source == null) {
353: return Util.THIS.getString("MSG_unknown_parser", klass
354: .getName());
355: } else {
356: URL location = source.getLocation();
357: return Util.THIS.getString("MSG_parser_plug", location
358: .toExternalForm());
359: }
360:
361: } catch (SecurityException ex) {
362: return Util.THIS.getString("MSG_unknown_parser", klass
363: .getName());
364: }
365:
366: }
367:
368: // Content & ErrorHandler implementation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
369:
370: private class Handler extends DefaultHandler {
371:
372: public void warning(SAXParseException ex) {
373:
374: // heuristics to detect bogus schema loading requests
375:
376: String msg = ex.getLocalizedMessage();
377: if (bogusSchemaRequest) {
378: bogusSchemaRequest = false;
379: if (msg != null
380: && msg.indexOf("schema_reference.4") != -1) { // NOI18N
381: if (reportBogusSchemaRequest) {
382: reportBogusSchemaRequest = false;
383: } else {
384: return;
385: }
386: }
387: }
388:
389: CookieMessage message = new CookieMessage(msg,
390: CookieMessage.WARNING_LEVEL,
391: new DefaultXMLProcessorDetail(ex));
392: if (console != null)
393: console.receive(message);
394: }
395:
396: /**
397: * Report maximally getMaxErrorCount() errors then stop the parser.
398: */
399: public void error(SAXParseException ex) throws SAXException {
400: if (Util.THIS.isLoggable()) /* then */
401: Util.THIS.debug("Just diagnostic exception", ex); // NOI18N
402: if (errors++ == getMaxErrorCount()) {
403: String msg = Util.THIS.getString("MSG_too_many_errs");
404: sendMessage(msg);
405: throw ex; // stop the parser
406: } else {
407: CookieMessage message = new CookieMessage(ex
408: .getLocalizedMessage(),
409: CookieMessage.ERROR_LEVEL,
410: new DefaultXMLProcessorDetail(ex));
411: if (console != null)
412: console.receive(message);
413: }
414: }
415:
416: /**
417: * Log runtime exception cause
418: */
419: private void runtimeError(RuntimeException ex) {
420: Util.THIS.debug("Parser runtime exception", ex);
421:
422: // probably an internal parser error
423: String msg = Util.THIS.getString("EX_parser_ierr", ex
424: .getMessage());
425: fatalError(new SAXParseException(msg,
426: SharedXMLSupport.this .locator, ex));
427: }
428:
429: public void fatalError(SAXParseException ex) {
430: if (Util.THIS.isLoggable()) /* then */
431: Util.THIS.debug("Just diagnostic exception", ex); // NOI18N
432: fatalErrors++;
433: CookieMessage message = new CookieMessage(ex
434: .getLocalizedMessage(),
435: CookieMessage.FATAL_ERROR_LEVEL,
436: new DefaultXMLProcessorDetail(ex));
437: if (console != null)
438: console.receive(message);
439: }
440:
441: public void setDocumentLocator(Locator locator) {
442: SharedXMLSupport.this .locator = locator;
443: }
444:
445: private int getMaxErrorCount() {
446: return 20; //??? load from option
447: }
448:
449: }
450:
451: /**
452: * EntityResolver that reports unresolved entities.
453: */
454: private class VerboseEntityResolver implements EntityResolver {
455:
456: private final EntityResolver peer;
457:
458: public VerboseEntityResolver(EntityResolver res) {
459: if (res == null)
460: throw new NullPointerException();
461: peer = res;
462: }
463:
464: public InputSource resolveEntity(String pid, String sid)
465: throws SAXException, IOException {
466:
467: InputSource result = peer.resolveEntity(pid, sid);
468:
469: // null result may be suspicious, may be no Schema location found etc.
470:
471: if (result == null) {
472: bogusSchemaRequest = pid == null && sid == null;
473: if (bogusSchemaRequest)
474: return null;
475:
476: String warning;
477: String pidLabel = pid != null ? pid : Util.THIS
478: .getString("MSG_no_pid");
479: try {
480: String file = new URL(sid).getFile();
481: if (file != null) {
482: warning = Util.THIS.getString("MSG_resolver_1",
483: pidLabel, sid);
484: } else { // probably NS id
485: warning = Util.THIS.getString("MSG_resolver_2",
486: pidLabel, sid);
487: }
488: } catch (MalformedURLException ex) {
489: warning = Util.THIS.getString("MSG_resolver_3",
490: pidLabel, sid);
491: }
492: sendMessage(warning);
493: }
494: return result;
495: }
496:
497: }
498:
499: private void sendMessage(String message) {
500: if (console != null) {
501: console.receive(new CookieMessage(message));
502: }
503: }
504:
505: private String[] getSchemaLocations(InputSource is) {
506: EntityResolver res = createEntityResolver();
507: if (res == null)
508: return null;
509: NsHandler nsHandler = getNamespaces(is);
510: String[] namespaces = nsHandler.getNamespaces();
511: List loc = new ArrayList();
512: for (int i = 0; i < namespaces.length; i++) {
513: String ns = namespaces[i];
514: if (nsHandler.mapping.containsKey(ns)) {
515: loc.add(ns + " " + nsHandler.mapping.get(ns)); //NOI18N
516: } else {
517: try {
518: javax.xml.transform.Source src = ((javax.xml.transform.URIResolver) res)
519: .resolve(ns, null);
520: if (src != null)
521: loc.add(ns + " " + src.getSystemId()); //NOI18N
522: } catch (Exception ex) {
523: }
524: }
525: }
526: String[] schemaLocations = new String[loc.size()];
527: loc.toArray(schemaLocations);
528: return schemaLocations;
529: }
530:
531: private NsHandler getNamespaces(InputSource is) {
532: NsHandler handler = new NsHandler();
533: try {
534: XMLReader xmlReader = org.openide.xml.XMLUtil
535: .createXMLReader(false, true);
536: xmlReader.setContentHandler(handler);
537:
538: // XXX dumb resolver always returning empty stream would be better but
539: // parsing could fail on resolving general entities defined in DTD.
540: // Check XML spec if non-validation parser must resolve general entities
541: // Ccc: I think so, there is Xerces property to relax it but we get here Crimson
542: UserCatalog userCatalog = UserCatalog.getDefault();
543: if (userCatalog != null) {
544: EntityResolver resolver = userCatalog
545: .getEntityResolver();
546: if (resolver != null) {
547: xmlReader.setEntityResolver(resolver);
548: }
549: }
550: xmlReader.parse(is);
551: } catch (IOException ex) {
552: ErrorManager.getDefault().notify(
553: ErrorManager.INFORMATIONAL, ex);
554: } catch (SAXException ex) {
555: ErrorManager.getDefault().notify(
556: ErrorManager.INFORMATIONAL, ex);
557: }
558: return handler;
559: }
560:
561: private static class NsHandler extends
562: org.xml.sax.helpers.DefaultHandler {
563: Set namespaces;
564: private Map mapping;
565:
566: NsHandler() {
567: namespaces = new HashSet();
568: mapping = new HashMap();
569: }
570:
571: public void startElement(String uri, String localName,
572: String rawName, Attributes atts) throws SAXException {
573: if (atts.getLength() > 0) { //NOI18N
574: // parse XMLSchema location attribute
575: String locations = atts.getValue(
576: "http://www.w3.org/2001/XMLSchema-instance",
577: "schemaLocation"); // NOI18N
578: if (locations != null) {
579: StringTokenizer tokenizer = new StringTokenizer(
580: locations);
581: if ((tokenizer.countTokens() % 2) == 0) {
582: while (tokenizer.hasMoreElements()) {
583: String nsURI = tokenizer.nextToken();
584: String nsLocation = tokenizer.nextToken();
585: mapping.put(nsURI, nsLocation);
586: }
587: }
588: }
589: }
590: }
591:
592: public void startPrefixMapping(String prefix, String uri)
593: throws SAXException {
594: if ("http://www.w3.org/2001/XMLSchema-instance".equals(uri)) { // NOIi8N
595: return; // it's build in into parser
596: }
597: namespaces.add(uri);
598: }
599:
600: String[] getNamespaces() {
601: String[] ns = new String[namespaces.size()];
602: namespaces.toArray(ns);
603: return ns;
604: }
605:
606: }
607:
608: }
|