1 /**
2 * Licensed to the Apache Software Foundation (ASF) under one
3 * or more contributor license agreements. See the NOTICE file
4 * distributed with this work for additional information
5 * regarding copyright ownership. The ASF licenses this file
6 * to you under the Apache License, Version 2.0 (the
7 * "License"); you may not use this file except in compliance
8 * with the License. You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing,
13 * software distributed under the License is distributed on an
14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 * KIND, either express or implied. See the License for the
16 * specific language governing permissions and limitations
17 * under the License.
18 */
19 package org.apache.cxf.endpoint.dynamic;
20
21 import java.io.File;
22 import java.io.FileInputStream;
23 import java.io.IOException;
24 import java.net.MalformedURLException;
25 import java.net.URI;
26 import java.net.URISyntaxException;
27 import java.net.URL;
28 import java.net.URLClassLoader;
29 import java.util.Collection;
30 import java.util.Collections;
31 import java.util.Iterator;
32 import java.util.List;
33 import java.util.Map;
34 import java.util.StringTokenizer;
35 import java.util.jar.Attributes;
36 import java.util.jar.JarFile;
37 import java.util.logging.Level;
38 import java.util.logging.Logger;
39
40 import javax.xml.bind.JAXBContext;
41 import javax.xml.bind.JAXBException;
42 import javax.xml.namespace.QName;
43
44 import org.w3c.dom.Element;
45
46 import org.xml.sax.EntityResolver;
47 import org.xml.sax.InputSource;
48 import org.xml.sax.SAXException;
49 import org.xml.sax.SAXParseException;
50
51 import com.sun.codemodel.JCodeModel;
52 import com.sun.codemodel.JDefinedClass;
53 import com.sun.codemodel.JPackage;
54 import com.sun.codemodel.writer.FileCodeWriter;
55 import com.sun.tools.xjc.Options;
56 import com.sun.tools.xjc.api.ErrorListener;
57 import com.sun.tools.xjc.api.S2JJAXBModel;
58 import com.sun.tools.xjc.api.SchemaCompiler;
59 import com.sun.tools.xjc.api.XJC;
60
61 import org.apache.cxf.Bus;
62 import org.apache.cxf.bus.CXFBusFactory;
63 import org.apache.cxf.common.i18n.Message;
64 import org.apache.cxf.common.logging.LogUtils;
65 import org.apache.cxf.common.util.StringUtils;
66 import org.apache.cxf.endpoint.Client;
67 import org.apache.cxf.endpoint.ClientImpl;
68 import org.apache.cxf.endpoint.EndpointImplFactory;
69 import org.apache.cxf.endpoint.SimpleEndpointImplFactory;
70 import org.apache.cxf.helpers.FileUtils;
71 import org.apache.cxf.jaxb.JAXBDataBinding;
72 import org.apache.cxf.resource.URIResolver;
73 import org.apache.cxf.service.Service;
74 import org.apache.cxf.service.factory.ServiceConstructionException;
75 import org.apache.cxf.service.model.SchemaInfo;
76 import org.apache.cxf.service.model.ServiceInfo;
77 /**
78 * This class reads a WSDL and creates a dynamic client from it.
79 *
80 * Use {@link #newInstance} to obtain an instance, and then
81 * {@link #createClient(String)} (or other overloads) to create a client.
82 *
83 * It uses the JAXB data binding. It does not set up complex interceptors for
84 * features such as attachments.
85 * See {@link org.apache.cxf.jaxws.endpoint.dynamic.JaxWsDynamicClientFactory}
86 * for an alternative that sets up JAX-WS endpoints.
87 *
88 * This class may be subclassed to allow for other endpoints or behaviors.
89 */
90 public class DynamicClientFactory {
91
92 private static final Logger LOG = LogUtils.getL7dLogger(DynamicClientFactory.class);
93
94 private Bus bus;
95
96 private String tmpdir = System.getProperty("java.io.tmpdir");
97
98 private boolean simpleBindingEnabled = true;
99
100 private Map<String, Object> jaxbContextProperties;
101
102 protected DynamicClientFactory(Bus bus) {
103 this.bus = bus;
104 }
105
106 protected EndpointImplFactory getEndpointImplFactory() {
107 return SimpleEndpointImplFactory.getSingleton();
108 }
109
110 public void setTemporaryDirectory(String dir) {
111 tmpdir = dir;
112 }
113
114 /**
115 * Create a new instance using a specific <tt>Bus</tt>.
116 *
117 * @param b the <tt>Bus</tt> to use in subsequent operations with the
118 * instance
119 * @return the new instance
120 */
121 public static DynamicClientFactory newInstance(Bus b) {
122 return new DynamicClientFactory(b);
123 }
124
125 /**
126 * Create a new instance using a default <tt>Bus</tt>.
127 *
128 * @return the new instance
129 * @see CXFBusFactory#getDefaultBus()
130 */
131 public static DynamicClientFactory newInstance() {
132 Bus bus = CXFBusFactory.getThreadDefaultBus();
133 return new DynamicClientFactory(bus);
134 }
135
136 /**
137 * Create a new <code>Client</code> instance using the WSDL to be loaded
138 * from the specified URL and using the current classloading context.
139 *
140 * @param wsdlURL the URL to load
141 * @return
142 */
143 public Client createClient(String wsdlUrl) {
144 return createClient(wsdlUrl, (QName)null, (QName)null);
145 }
146
147 /**
148 * Create a new <code>Client</code> instance using the WSDL to be loaded
149 * from the specified URL and with the specified <code>ClassLoader</code>
150 * as parent.
151 *
152 * @param wsdlUrl
153 * @param classLoader
154 * @return
155 */
156 public Client createClient(String wsdlUrl, ClassLoader classLoader) {
157 return createClient(wsdlUrl, null, classLoader, null);
158 }
159
160 public Client createClient(String wsdlUrl, QName service) {
161 return createClient(wsdlUrl, service, null);
162 }
163
164 public Client createClient(String wsdlUrl, QName service, QName port) {
165 return createClient(wsdlUrl, service, null, port);
166 }
167
168 public Client createClient(String wsdlUrl, QName service, ClassLoader classLoader, QName port) {
169 if (classLoader == null) {
170 classLoader = Thread.currentThread().getContextClassLoader();
171 }
172 URL u = composeUrl(wsdlUrl);
173 LOG.log(Level.FINE, "Creating client from URL " + u.toString());
174 ClientImpl client = new ClientImpl(bus, u, service, port,
175 getEndpointImplFactory());
176
177 Service svc = client.getEndpoint().getService();
178 //all SI's should have the same schemas
179 Collection<SchemaInfo> schemas = svc.getServiceInfos().get(0).getSchemas();
180
181 SchemaCompiler compiler = XJC.createSchemaCompiler();
182 ErrorListener elForRun = new InnerErrorListener(wsdlUrl);
183 compiler.setErrorListener(elForRun);
184
185 addSchemas(wsdlUrl, schemas, compiler);
186
187 S2JJAXBModel intermediateModel = compiler.bind();
188 JCodeModel codeModel = intermediateModel.generateCode(null, elForRun);
189 StringBuilder sb = new StringBuilder();
190 boolean firstnt = false;
191
192 for (Iterator<JPackage> packages = codeModel.packages(); packages.hasNext();) {
193 JPackage jpackage = packages.next();
194 if (!isValidPackage(jpackage)) {
195 continue;
196 }
197 if (firstnt) {
198 sb.append(':');
199 } else {
200 firstnt = true;
201 }
202 sb.append(jpackage.name());
203 }
204 outputDebug(codeModel);
205
206 String packageList = sb.toString();
207
208 // our hashcode + timestamp ought to be enough.
209 String stem = toString() + "-" + System.currentTimeMillis();
210 File src = new File(tmpdir, stem + "-src");
211 if (!src.mkdir()) {
212 throw new IllegalStateException("Unable to create working directory " + src.getPath());
213 }
214 try {
215 FileCodeWriter writer = new FileCodeWriter(src);
216 codeModel.build(writer);
217 } catch (IOException e) {
218 throw new IllegalStateException("Unable to write generated Java files for schemas: "
219 + e.getMessage(), e);
220 }
221 File classes = new File(tmpdir, stem + "-classes");
222 if (!classes.mkdir()) {
223 throw new IllegalStateException("Unable to create working directory " + src.getPath());
224 }
225 StringBuilder classPath = new StringBuilder();
226 try {
227 setupClasspath(classPath, classLoader);
228 } catch (Exception ex) {
229 throw new RuntimeException(ex);
230 }
231
232 List<File> srcFiles = FileUtils.getFilesRecurse(src, ".+\\.java$");
233 if (!compileJavaSrc(classPath.toString(), srcFiles, classes.toString())) {
234 LOG.log(Level.SEVERE , new Message("COULD_NOT_COMPILE_SRC", LOG, wsdlUrl).toString());
235 }
236 FileUtils.removeDir(src);
237 URLClassLoader cl;
238 try {
239 cl = new URLClassLoader(new URL[] {classes.toURI().toURL()}, classLoader);
240 } catch (MalformedURLException mue) {
241 throw new IllegalStateException("Internal error; a directory returns a malformed URL: "
242 + mue.getMessage(), mue);
243 }
244
245 JAXBContext context;
246 Map<String, Object> contextProperties = jaxbContextProperties;
247
248 if (contextProperties == null) {
249 contextProperties = Collections.emptyMap();
250 }
251
252 try {
253 if (StringUtils.isEmpty(packageList)) {
254 context = JAXBContext.newInstance(new Class[0], contextProperties);
255 } else {
256 context = JAXBContext.newInstance(packageList, cl, contextProperties);
257 }
258 } catch (JAXBException jbe) {
259 throw new IllegalStateException("Unable to create JAXBContext for generated packages: "
260 + jbe.getMessage(), jbe);
261 }
262
263 JAXBDataBinding databinding = new JAXBDataBinding();
264 databinding.setContext(context);
265 svc.setDataBinding(databinding);
266
267 ServiceInfo svcfo = client.getEndpoint().getEndpointInfo().getService();
268
269 // Setup the new classloader!
270 Thread.currentThread().setContextClassLoader(cl);
271
272 TypeClassInitializer visitor = new TypeClassInitializer(svcfo, intermediateModel);
273 visitor.walk();
274 // delete the classes files
275 FileUtils.removeDir(classes);
276 return client;
277 }
278
279 private boolean isValidPackage(JPackage jpackage) {
280 if (jpackage == null) {
281 return false;
282 }
283 String name = jpackage.name();
284 if ("org.w3._2001.xmlschema".equals(name)
285 || "java.lang".equals(name)
286 || "java.io".equals(name)
287 || "generated".equals(name)) {
288 return false;
289 }
290 Iterator<JDefinedClass> i = jpackage.classes();
291 while (i.hasNext()) {
292 JDefinedClass current = i.next();
293 if ("ObjectFactory".equals(current.name())) {
294 return true;
295 }
296 }
297 return false;
298 }
299
300 private void outputDebug(JCodeModel codeModel) {
301 if (!LOG.isLoggable(Level.INFO)) {
302 return;
303 }
304
305 StringBuffer sb = new StringBuffer();
306 boolean first = true;
307 for (Iterator<JPackage> itr = codeModel.packages(); itr.hasNext();) {
308 JPackage package1 = itr.next();
309
310 for (Iterator<JDefinedClass> citr = package1.classes(); citr.hasNext();) {
311 if (!first) {
312 sb.append(", ");
313 } else {
314 first = false;
315 }
316 sb.append(citr.next().fullName());
317 }
318 }
319
320 LOG.log(Level.INFO, "Created classes: " + sb.toString());
321
322 }
323
324 private void addSchemas(String wsdlUrl, Collection<SchemaInfo> schemas, SchemaCompiler compiler) {
325 int num = 1;
326 for (SchemaInfo schema : schemas) {
327 Element el = schema.getElement();
328
329 compiler.parseSchema(wsdlUrl + "#types" + num, el);
330 num++;
331 }
332
333 if (simpleBindingEnabled && isJaxb21()) {
334 String id = "/org/apache/cxf/endpoint/dynamic/simple-binding.xjb";
335 LOG.info("Loading the JAXB 2.1 simple binding for client.");
336 InputSource source = new InputSource(getClass().getResourceAsStream(id));
337 source.setSystemId(id);
338 compiler.parseSchema(source);
339 }
340 }
341
342 private boolean isJaxb21() {
343 String id = Options.getBuildID();
344 StringTokenizer st = new StringTokenizer(id, ".");
345 String minor = null;
346
347 // major version
348 if (st.hasMoreTokens()) {
349 st.nextToken();
350 }
351
352 if (st.hasMoreTokens()) {
353 minor = st.nextToken();
354 }
355
356 try {
357 int i = Integer.valueOf(minor);
358 if (i >= 1) {
359 return true;
360 }
361 } catch (NumberFormatException e) {
362 // do nothing;
363 }
364
365 return false;
366 }
367
368 public boolean isSimpleBindingEnabled() {
369 return simpleBindingEnabled;
370 }
371
372 public void setSimpleBindingEnabled(boolean simpleBindingEnabled) {
373 this.simpleBindingEnabled = simpleBindingEnabled;
374 }
375
376 static boolean compileJavaSrc(String classPath, List<File> srcList, String dest) {
377 String[] javacCommand = new String[srcList.size() + 7];
378
379 javacCommand[0] = "javac";
380 javacCommand[1] = "-classpath";
381 javacCommand[2] = classPath;
382 javacCommand[3] = "-d";
383 javacCommand[4] = dest;
384 javacCommand[5] = "-target";
385 javacCommand[6] = "1.5";
386
387 int i = 7;
388 for (File f : srcList) {
389 javacCommand[i++] = f.getAbsolutePath();
390 }
391 org.apache.cxf.common.util.Compiler javaCompiler
392 = new org.apache.cxf.common.util.Compiler();
393
394 return javaCompiler.internalCompile(javacCommand, 7);
395 }
396
397 static void addClasspathFromManifest(StringBuilder classPath, File file)
398 throws URISyntaxException, IOException {
399
400 JarFile jar = new JarFile(file);
401 Attributes attr = null;
402 if (jar.getManifest() != null) {
403 attr = jar.getManifest().getMainAttributes();
404 }
405 if (attr != null) {
406 String cp = attr.getValue("Class-Path");
407 while (cp != null) {
408 String fileName = cp;
409 int idx = fileName.indexOf(' ');
410 if (idx != -1) {
411 fileName = fileName.substring(0, idx);
412 cp = cp.substring(idx + 1).trim();
413 } else {
414 cp = null;
415 }
416 URI uri = new URI(fileName);
417 File f2;
418 if (uri.isAbsolute()) {
419 f2 = new File(uri);
420 } else {
421 f2 = new File(file, fileName);
422 }
423 if (f2.exists()) {
424 classPath.append(f2.getAbsolutePath());
425 classPath.append(System.getProperty("path.separator"));
426 }
427 }
428 }
429 }
430
431 static void setupClasspath(StringBuilder classPath, ClassLoader classLoader)
432 throws URISyntaxException, IOException {
433
434 ClassLoader scl = ClassLoader.getSystemClassLoader();
435 ClassLoader tcl = classLoader;
436 do {
437 if (tcl instanceof URLClassLoader) {
438 URL[] urls = ((URLClassLoader)tcl).getURLs();
439 if (urls == null) {
440 urls = new URL[0];
441 }
442 for (URL url : urls) {
443 if (url.getProtocol().startsWith("file")) {
444 File file;
445 try {
446 file = new File(url.toURI().getPath());
447 } catch (URISyntaxException urise) {
448 file = new File(url.getPath());
449 }
450
451 if (file.exists()) {
452 classPath.append(file.getAbsolutePath())
453 .append(System
454 .getProperty("path.separator"));
455
456 if (file.getName().endsWith(".jar")) {
457 addClasspathFromManifest(classPath, file);
458 }
459 }
460 }
461 }
462 }
463 tcl = tcl.getParent();
464 if (null == tcl) {
465 break;
466 }
467 } while(!tcl.equals(scl.getParent()));
468 }
469
470 private URL composeUrl(String s) {
471 try {
472 URIResolver resolver = new URIResolver(null, s, getClass());
473
474 if (resolver.isResolved()) {
475 return resolver.getURI().toURL();
476 } else {
477 throw new ServiceConstructionException(new Message("COULD_NOT_RESOLVE_URL", LOG, s));
478 }
479 } catch (IOException e) {
480 throw new ServiceConstructionException(new Message("COULD_NOT_RESOLVE_URL", LOG, s), e);
481 }
482 }
483
484 private class InnerErrorListener implements ErrorListener {
485
486 private String url;
487
488 InnerErrorListener(String url) {
489 this.url = url;
490 }
491
492 public void error(SAXParseException arg0) {
493 throw new RuntimeException("Error compiling schema from WSDL at {" + url + "}: "
494 + arg0.getMessage(), arg0);
495 }
496
497 public void fatalError(SAXParseException arg0) {
498 throw new RuntimeException("Fatal error compiling schema from WSDL at {" + url + "}: "
499 + arg0.getMessage(), arg0);
500 }
501
502 public void info(SAXParseException arg0) {
503 // ignore
504 }
505
506 public void warning(SAXParseException arg0) {
507 // ignore
508 }
509 }
510
511 // sorry, but yuck. Try a file first?!?
512 static class RelativeEntityResolver implements EntityResolver {
513 private String baseURI;
514
515 public RelativeEntityResolver(String baseURI) {
516 super();
517 this.baseURI = baseURI;
518 }
519
520 public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException {
521 // the system id is null if the entity is in the wsdl.
522 if (systemId != null) {
523 File file = new File(baseURI, systemId);
524 if (file.exists()) {
525 return new InputSource(new FileInputStream(file));
526 } else {
527 return new InputSource(systemId);
528 }
529 }
530 return null;
531 }
532 }
533
534 /**
535 * Return the map of JAXB context properties used at the time that we create new contexts.
536 * @return the map
537 */
538 public Map<String, Object> getJaxbContextProperties() {
539 return jaxbContextProperties;
540 }
541
542 /**
543 * Set the map of JAXB context properties used at the time that we create new contexts.
544 * @param jaxbContextProperties
545 */
546 public void setJaxbContextProperties(Map<String, Object> jaxbContextProperties) {
547 this.jaxbContextProperties = jaxbContextProperties;
548 }
549 }