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:
042: package org.netbeans.modules.websvc.core;
043:
044: import java.io.BufferedInputStream;
045: import java.io.ByteArrayInputStream;
046: import java.io.IOException;
047: import java.io.InputStream;
048:
049: import java.net.ConnectException;
050: import java.net.MalformedURLException;
051: import java.net.URL;
052: import java.net.URLConnection;
053: import java.security.cert.CertificateException;
054: import java.security.cert.X509Certificate;
055:
056: import java.text.MessageFormat;
057: import java.util.Iterator;
058:
059: import java.util.List;
060: import java.util.ArrayList;
061:
062: import javax.net.ssl.HostnameVerifier;
063: import javax.net.ssl.HttpsURLConnection;
064: import javax.net.ssl.SSLContext;
065: import javax.net.ssl.SSLSession;
066: import javax.net.ssl.TrustManager;
067: import javax.net.ssl.X509TrustManager;
068:
069: import javax.swing.SwingUtilities;
070: import org.netbeans.modules.xml.retriever.Retriever;
071: import org.openide.DialogDescriptor;
072: import org.openide.DialogDisplayer;
073: import org.openide.ErrorManager;
074:
075: import org.xml.sax.Attributes;
076: import org.xml.sax.InputSource;
077: import org.xml.sax.SAXException;
078: import org.xml.sax.helpers.DefaultHandler;
079: import javax.xml.parsers.SAXParser;
080: import javax.xml.parsers.SAXParserFactory;
081: import javax.xml.parsers.ParserConfigurationException;
082:
083: import org.openide.util.NbBundle;
084:
085: /** !PW FIXME This thread runs without a monitor thread ensuring a proper
086: * timeout and shutdown of the HttpConnection. It should use the timeout
087: * feature of JDK 1.5.0 and the timeout global properties in JDK 1.4.2.
088: *
089: * As is, it is probably possible for this thread to hang if it opens a
090: * connection to an HTTP server, sends a request, and that server never
091: * responds.
092: *
093: * @author Peter Williams
094: */
095: public class WsdlRetriever implements Runnable {
096:
097: public static final int STATUS_START = 0;
098: public static final int STATUS_CONNECTING = 1;
099: public static final int STATUS_DOWNLOADING = 2;
100: public static final int STATUS_COMPLETE = 3;
101: public static final int STATUS_FAILED = 4;
102: public static final int STATUS_TERMINATED = 5;
103: public static final int STATUS_BAD_WSDL = 6;
104:
105: static final String[] STATUS_MESSAGE = {
106: NbBundle.getMessage(WsdlRetriever.class, "LBL_Ready"), // NOI18N
107: NbBundle.getMessage(WsdlRetriever.class, "LBL_Connecting"), // NOI18N
108: NbBundle.getMessage(WsdlRetriever.class, "LBL_Downloading"), // NOI18N
109: NbBundle.getMessage(WsdlRetriever.class, "LBL_Complete"), // NOI18N
110: NbBundle.getMessage(WsdlRetriever.class, "LBL_Exception"), // NOI18N
111: NbBundle.getMessage(WsdlRetriever.class, "LBL_Terminated"), // NOI18N
112: NbBundle.getMessage(WsdlRetriever.class,
113: "LBL_UnknownFileType") // NOI18N
114: };
115:
116: // Thread plumbing
117: private volatile boolean shutdown;
118: private volatile int status;
119:
120: // Wsdl collection information
121: private String wsdlUrlName;
122: private byte[] wsdlContent;
123: private List<SchemaInfo> schemas; // List of imported schema/wsdl files
124:
125: private String wsdlFileName;
126: // Parent
127: private MessageReceiver receiver;
128:
129: public WsdlRetriever(MessageReceiver r, String url) {
130: this .shutdown = false;
131: this .status = STATUS_START;
132: this .wsdlUrlName = url;
133: this .wsdlContent = null;
134: this .wsdlFileName = null;
135: this .schemas = null;
136: this .receiver = r;
137: }
138:
139: // Properties
140: public byte[] getWsdl() {
141: return wsdlContent;
142: }
143:
144: public List<SchemaInfo> getSchemas() {
145: return schemas;
146: }
147:
148: public int getState() {
149: return status;
150: }
151:
152: public String getWsdlFileName() {
153: return wsdlFileName;
154: }
155:
156: public String getWsdlUrl() {
157: return wsdlUrlName;
158: }
159:
160: // not sure this is necessary -- for controller to signal shutdown in case
161: // interrupted() doesn't work.
162: public synchronized void stopRetrieval() {
163: shutdown = true;
164: }
165:
166: private URL wsdlUrl;
167: private URLConnection connection;
168: private InputStream in;
169:
170: public void run() {
171: // Set name of thread for easier debugging in case of deadlocks, etc.
172: Thread.currentThread().setName("WsdlRetrieval"); // NOI18N
173:
174: wsdlUrl = null;
175: connection = null;
176: in = null;
177:
178: try {
179: // !PW FIXME if we wanted to add an option to turn beautification of
180: // the URL off (because our algorithm conflicted with what the user
181: // need to enter), this is the method that such option would need to
182: // disable.
183: wsdlUrlName = beautifyUrlName(wsdlUrlName);
184: wsdlUrl = new URL(wsdlUrlName);
185: setState(STATUS_CONNECTING);
186: if (wsdlUrlName.startsWith("https")) { //NOI18N
187: setRetrieverTrustManager();
188: }
189: connection = wsdlUrl.openConnection();
190: in = connection.getInputStream();
191:
192: setState(STATUS_DOWNLOADING);
193:
194: // Download the wsdl file
195: wsdlContent = downloadWsdlFileEncoded(new BufferedInputStream(
196: in));
197:
198: WsdlInfo wsdlInfo = null;
199: // Dowdload all imported schemas
200: if (!shutdown) {
201: wsdlInfo = getWsdlInfo();
202: if (wsdlInfo != null) {
203: List /*String*/schemaNames = wsdlInfo
204: .getSchemaNames();
205: if (!schemaNames.isEmpty()) {
206: schemas = new ArrayList<SchemaInfo>();
207: Iterator it = schemaNames.iterator();
208: while (!shutdown && it.hasNext()) {
209: String schemaName = (String) it.next();
210: String schemaUrlName = getSchemaUrlName(
211: wsdlUrlName, schemaName);
212: URL schemaUrl = new URL(schemaUrlName);
213: connection = schemaUrl.openConnection();
214: in = connection.getInputStream();
215: schemas
216: .add(new SchemaInfo(
217: schemaName,
218: downloadWsdlFileEncoded(new BufferedInputStream(
219: in))));
220: }
221: }
222: } else {
223: throw new MalformedURLException();
224: }
225: }
226: if (!shutdown) {
227: // extract the (first) service name to use as a suggested filename.
228: List serviceNames = wsdlInfo.getServiceNameList();
229: if (serviceNames != null && serviceNames.size() > 0) {
230: wsdlFileName = wsdlUrl.getPath();
231: int slashIndex = wsdlFileName.lastIndexOf('/');
232: if (slashIndex != -1) {
233: wsdlFileName = wsdlFileName
234: .substring(slashIndex + 1);
235: }
236:
237: if (wsdlFileName.length() == 0) {
238: wsdlFileName = (String) serviceNames.get(0)
239: + ".wsdl"; // NOI18N
240: } else if (wsdlFileName.length() < 5
241: || !".wsdl"
242: .equals(wsdlFileName
243: .substring(wsdlFileName
244: .length() - 5))) { // NOI18N
245: wsdlFileName += ".wsdl"; // NOI18N
246: }
247: setState(STATUS_COMPLETE);
248: } else {
249: // !PW FIXME bad wsdl file -- can we save and return the parser error message?
250: setState(STATUS_BAD_WSDL);
251: }
252: } else {
253: setState(STATUS_TERMINATED);
254: }
255: } catch (ConnectException ex) {
256: setState(STATUS_FAILED, NbBundle.getMessage(
257: WsdlRetriever.class, "ERR_Connection"), ex); // NOI18N
258: log(ex.getMessage());
259: } catch (MalformedURLException ex) {
260: setState(STATUS_FAILED, NbBundle.getMessage(
261: WsdlRetriever.class, "ERR_BadUrl"), ex); // NOI18N
262: log(ex.getMessage());
263: } catch (IOException ex) {
264: setState(STATUS_FAILED, NbBundle.getMessage(
265: WsdlRetriever.class, "ERR_IOException"), ex); // NOI18N
266: log(ex.getMessage());
267: } finally {
268: if (in != null) {
269: try {
270: in.close();
271: } catch (IOException ex) {
272: }
273: }
274: }
275: }
276:
277: /** Retrieve the wsdl file from the specified inputstream. We don't know how big
278: * the file might be, and while many WSDL files are less than 30-40K, eBay's
279: * WSDL is over 1MB, so there is extra logic here to be very flexible on buffer
280: * space with minimal copying.
281: *
282: * This routine could possibly be cleaned up a bit, but might lose in readability.
283: * For example, 'chunksize' is probably redundant and could be replaced by 'i',
284: * but the java optimizer will do that for us anyway and the usage is more clear
285: * this way.
286: *
287: */
288: private byte[] downloadWsdlFileEncoded(InputStream in)
289: throws IOException {
290: ArrayList<Chunk> chunks = new ArrayList<Chunk>();
291: final int BUF = 65536;
292: boolean eof = false;
293: byte[] data = new byte[0];
294:
295: while (!shutdown && !eof) {
296: byte[] b = new byte[BUF]; // New buffer for this block
297: int i = 0; // index within this block we're writing at
298: int l = 0; // number of bytes read during last call to read().
299: int limit = b.length; // maximum number of bytes to read during call to read()
300: int chunksize = 0; // number of bytes read into this block. Should be always be BUF, except for last block of file.
301:
302: while (!shutdown && (l = in.read(b, i, limit)) != -1) {
303: limit -= l;
304: i += l;
305: chunksize += l;
306:
307: if (limit == 0) {
308: break;
309: }
310: }
311:
312: // if we downloaded any data, add a chunk containing the data to our list of chunks.
313: if (chunksize > 0) {
314: chunks.add(new Chunk(b, chunksize));
315: }
316:
317: eof = (l == -1);
318: }
319:
320: if (!shutdown) {
321: // calculate length for single byte array that contains the entire WSDL
322: int bufLen = 0;
323: Iterator<Chunk> iter = chunks.iterator();
324: while (iter.hasNext()) {
325: bufLen += iter.next().getLength();
326: }
327:
328: // Now fill the single byte array with all the chunks we downloaded.
329: data = new byte[bufLen];
330: int index = 0;
331: iter = chunks.iterator();
332: while (iter.hasNext()) {
333: Chunk c = iter.next();
334: System.arraycopy(c.getData(), 0, data, index, c
335: .getLength());
336: index += c.getLength();
337: }
338: }
339:
340: return data;
341: }
342:
343: public static String beautifyUrlName(String urlName) {
344: // 1. verify protocol, use http if not specified.
345: if (urlName.indexOf("://") == -1) { // NOI18N
346: urlName = "http://" + urlName; // NOI18N
347: }
348:
349: // 2. if this looks like a service, add a ?WSDL argument.
350: try {
351: URL testUrl = new URL(urlName);
352: String testName = testUrl.getPath();
353: boolean hasArguments = (testUrl.getFile().indexOf('?') != -1);
354: int slashIndex = testName.lastIndexOf('/');
355: if (slashIndex != -1) {
356: testName = testName.substring(slashIndex + 1);
357: }
358: int dotIndex = testName.lastIndexOf('.');
359: if (dotIndex != -1) {
360: String extension = testName.substring(dotIndex + 1);
361: if (!"xml".equals(extension)
362: && !"wsdl".equals(extension) && !hasArguments) { // NOI18N
363: urlName += "?WSDL"; // NOI18N
364: }
365: } else if (!hasArguments) {
366: // no file extension and no http arguments -- probably needs extension
367: urlName = urlName + "?WSDL"; // NOI18N
368: }
369: } catch (MalformedURLException ex) {
370: // do nothing about this here. This error will occur again for real
371: // in the caller and be handled there.
372: }
373:
374: return urlName;
375: }
376:
377: private String getSchemaUrlName(String wsdlUrl, String schemaName) {
378: int index = wsdlUrl.lastIndexOf("/"); //NOI18N
379: if (index >= 0)
380: return wsdlUrl.substring(0, index + 1) + schemaName;
381: else
382: return null;
383: }
384:
385: private void setState(int newState) {
386: status = newState;
387: log(STATUS_MESSAGE[newState]);
388: SwingUtilities.invokeLater(new MessageSender(receiver,
389: STATUS_MESSAGE[newState]));
390: }
391:
392: private void setState(int newState, String msg, Exception ex) {
393: status = newState;
394: Object[] args = new Object[] { msg, ex.getMessage() };
395: String message = MessageFormat.format(STATUS_MESSAGE[newState],
396: args);
397: log(message);
398: SwingUtilities
399: .invokeLater(new MessageSender(receiver, message));
400: }
401:
402: private void log(String message) {
403: // This method for debugging only.
404: // System.out.println(message);
405: }
406:
407: // private class used to cache a message and post to UI component on AWT Thread.
408: private static class MessageSender implements Runnable {
409: private MessageReceiver receiver;
410: private String message;
411:
412: public MessageSender(MessageReceiver r, String m) {
413: receiver = r;
414: message = m;
415: }
416:
417: public void run() {
418: receiver.setWsdlDownloadMessage(message);
419: }
420: }
421:
422: public interface MessageReceiver {
423: public void setWsdlDownloadMessage(String m);
424: }
425:
426: /** Private method to sanity check the overall format of the WSDL file and
427: * determine the names of the one or more services defined therein.
428: */
429: private WsdlInfo getWsdlInfo() {
430: WsdlInfo result = null;
431:
432: try {
433: SAXParserFactory factory = SAXParserFactory.newInstance();
434: factory.setNamespaceAware(true);
435: SAXParser saxParser = factory.newSAXParser();
436: ServiceNameParser handler = new ServiceNameParser();
437: saxParser.parse(new InputSource(new ByteArrayInputStream(
438: wsdlContent)), handler);
439: result = new WsdlInfo(handler.getServiceNameList(), handler
440: .getSchemaNames());
441: } catch (ParserConfigurationException ex) {
442: // Bogus WSDL, return null.
443: } catch (SAXException ex) {
444: // Bogus WSDL, return null.
445: } catch (IOException ex) {
446: // Bogus WSDL, return null.
447: }
448:
449: return result;
450: }
451:
452: private static final class ServiceNameParser extends DefaultHandler {
453:
454: private static final String W3C_WSDL_SCHEMA = "http://schemas.xmlsoap.org/wsdl"; // NOI18N
455: private static final String W3C_WSDL_SCHEMA_SLASH = "http://schemas.xmlsoap.org/wsdl/"; // NOI18N
456:
457: private List<String> serviceNameList;
458: private List<String> schemaNames;
459:
460: private boolean insideSchema;
461:
462: ServiceNameParser() {
463: serviceNameList = new ArrayList<String>();
464: schemaNames = new ArrayList<String>();
465: }
466:
467: @Override
468: public void startElement(String uri, String localname,
469: String qname, Attributes attributes)
470: throws SAXException {
471: if (W3C_WSDL_SCHEMA.equals(uri)
472: || W3C_WSDL_SCHEMA_SLASH.equals(uri)) {
473: if ("service".equals(localname)) { // NOI18N
474: serviceNameList.add(attributes.getValue("name")); // NOI18N
475: }
476: if ("types".equals(localname)) { // NOI18N
477: insideSchema = true;
478: }
479: if ("import".equals(localname)) { // NOI18N
480: String wsdlLocation = attributes
481: .getValue("location"); //NOI18N
482: if (wsdlLocation != null
483: && wsdlLocation.indexOf("/") < 0
484: && wsdlLocation.endsWith(".wsdl")) { //NOI18N
485: schemaNames.add(wsdlLocation);
486: }
487: }
488: }
489: if (insideSchema && "import".equals(localname)) { // NOI18N
490: String schemaLocation = attributes
491: .getValue("schemaLocation"); //NOI18N
492: if (schemaLocation != null
493: && schemaLocation.indexOf("/") < 0
494: && schemaLocation.endsWith(".xsd")) { //NOI18N
495: schemaNames.add(schemaLocation);
496: }
497: }
498: }
499:
500: @Override
501: public void endElement(String uri, String localname,
502: String qname) throws SAXException {
503: if (W3C_WSDL_SCHEMA.equals(uri)
504: || W3C_WSDL_SCHEMA_SLASH.equals(uri)) {
505: if ("types".equals(localname)) { // NOI18N
506: insideSchema = false;
507: }
508: }
509: }
510:
511: public List<String> getServiceNameList() {
512: return serviceNameList;
513: }
514:
515: public List<String> getSchemaNames() {
516: return schemaNames;
517: }
518: }
519:
520: /** Data chunk of downloaded WSDL.
521: */
522: private static class Chunk {
523: private int length;
524: private byte[] data;
525:
526: public Chunk(byte[] d, int l) {
527: data = d;
528: length = l;
529: }
530:
531: public byte[] getData() {
532: return data;
533: }
534:
535: public int getLength() {
536: return length;
537: }
538: }
539:
540: private static class WsdlInfo {
541: private List<String> serviceNameList;
542: private List<String> schemaNames;
543:
544: WsdlInfo(List<String> serviceNameList, List<String> schemaNames) {
545: this .serviceNameList = serviceNameList;
546: this .schemaNames = schemaNames;
547: }
548:
549: List<String> getSchemaNames() {
550: return schemaNames;
551: }
552:
553: List<String> getServiceNameList() {
554: return serviceNameList;
555: }
556: }
557:
558: public static class SchemaInfo {
559: private String schemaName;
560: private byte[] schemaContent;
561:
562: SchemaInfo(String schemaName, byte[] schemaContent) {
563: this .schemaName = schemaName;
564: this .schemaContent = schemaContent;
565: }
566:
567: public String getSchemaName() {
568: return schemaName;
569: }
570:
571: public byte[] getSchemaContent() {
572: return schemaContent;
573: }
574: }
575:
576: // Install the trust manager for retriever
577: private void setRetrieverTrustManager() {
578: TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() {
579: public X509Certificate[] getAcceptedIssuers() {
580: return new X509Certificate[0];
581: }
582:
583: public void checkClientTrusted(X509Certificate[] certs,
584: String authType) {
585: }
586:
587: public void checkServerTrusted(X509Certificate[] certs,
588: String authType) throws CertificateException {
589: // ask user to accept the unknown certificate
590: if (certs != null) {
591: for (int i = 0; i < certs.length; i++) {
592: DialogDescriptor desc = new DialogDescriptor(
593: Retriever
594: .getCertificationPanel(certs[i]),
595: NbBundle.getMessage(
596: WsdlRetriever.class,
597: "TTL_CertifiedWebSite"), true,
598: DialogDescriptor.YES_NO_OPTION,
599: DialogDescriptor.YES_OPTION, null);
600: DialogDisplayer.getDefault().notify(desc);
601: if (!DialogDescriptor.YES_OPTION.equals(desc
602: .getValue())) {
603: throw new CertificateException(
604: NbBundle
605: .getMessage(
606: WsdlRetriever.class,
607: "ERR_NotTrustedCertificate"));
608: }
609: } // end for
610: }
611: }
612: } };
613:
614: try {
615: SSLContext sslContext = SSLContext.getInstance("SSL"); //NOI18N
616: sslContext.init(null, trustAllCerts,
617: new java.security.SecureRandom());
618: HttpsURLConnection.setDefaultSSLSocketFactory(sslContext
619: .getSocketFactory());
620: HttpsURLConnection
621: .setDefaultHostnameVerifier(new HostnameVerifier() {
622: public boolean verify(String string,
623: SSLSession sSLSession) {
624: // accept all hosts
625: return true;
626: }
627: });
628: } catch (java.security.GeneralSecurityException e) {
629: ErrorManager.getDefault().notify(e);
630: }
631:
632: }
633: }
|