1 package org.codehaus.mojo.jaxb2.javageneration;
2
3 /*
4 * Licensed to the Apache Software Foundation (ASF) under one
5 * or more contributor license agreements. See the NOTICE file
6 * distributed with this work for additional information
7 * regarding copyright ownership. The ASF licenses this file
8 * to you under the Apache License, Version 2.0 (the
9 * "License"); you may not use this file except in compliance
10 * with the License. You may obtain a copy of the License at
11 *
12 * http://www.apache.org/licenses/LICENSE-2.0
13 *
14 * Unless required by applicable law or agreed to in writing,
15 * software distributed under the License is distributed on an
16 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17 * KIND, either express or implied. See the License for the
18 * specific language governing permissions and limitations
19 * under the License.
20 */
21
22 import com.sun.tools.xjc.Driver;
23 import org.apache.maven.model.Resource;
24 import org.apache.maven.plugin.MojoExecutionException;
25 import org.apache.maven.plugin.MojoFailureException;
26 import org.apache.maven.plugins.annotations.Parameter;
27 import org.apache.maven.settings.Proxy;
28 import org.apache.maven.settings.Settings;
29 import org.codehaus.mojo.jaxb2.AbstractJaxbMojo;
30 import org.codehaus.mojo.jaxb2.NoSchemasException;
31 import org.codehaus.mojo.jaxb2.shared.FileSystemUtilities;
32 import org.codehaus.mojo.jaxb2.shared.arguments.ArgumentBuilder;
33 import org.codehaus.mojo.jaxb2.shared.environment.EnvironmentFacet;
34 import org.codehaus.mojo.jaxb2.shared.environment.ToolExecutionEnvironment;
35 import org.codehaus.mojo.jaxb2.shared.environment.classloading.ThreadContextClassLoaderBuilder;
36 import org.codehaus.mojo.jaxb2.shared.environment.locale.LocaleFacet;
37 import org.codehaus.mojo.jaxb2.shared.environment.logging.LoggingHandlerEnvironmentFacet;
38 import org.codehaus.plexus.util.FileUtils;
39 import org.codehaus.plexus.util.IOUtil;
40
41 import java.io.File;
42 import java.io.FileWriter;
43 import java.net.HttpURLConnection;
44 import java.net.URL;
45 import java.net.URLConnection;
46 import java.util.ArrayList;
47 import java.util.List;
48
49 /**
50 * <p>Abstract superclass for Mojos generating Java source or binaries from XML schema(s) by invoking the JAXB XJC
51 * binding compiler. Most of the Configuration options for the AbstractJavaGeneratorMojo are set or copied to the
52 * XJC directly; refer to their documentation in the <a href="https://jaxb.java.net/">JAXB Reference Implementation</a>
53 * site.</p>
54 *
55 * @author <a href="mailto:lj@jguru.se">Lennart Jörelid</a>
56 * @see <a href="https://jaxb.java.net/">The JAXB Reference Implementation</a>
57 */
58 public abstract class AbstractJavaGeneratorMojo extends AbstractJaxbMojo {
59
60 private static final int XJC_COMPLETED_OK = 0;
61
62 /**
63 * <p>Corresponding XJC parameter: {@code catalog}.</p>
64 * <p>Specify catalog files to resolve external entity references.
65 * Supports TR9401, XCatalog, and OASIS XML Catalog format.</p>
66 */
67 @Parameter
68 protected File catalog;
69
70 /**
71 * <p>Corresponding XJC parameter: {@code episode}.</p>
72 * <p>Generate an episode file from this compilation, so that other schemas that rely on this schema can be
73 * compiled later and rely on classes that are generated from this compilation. The generated episode file is
74 * really just a JAXB customization file (but with vendor extensions.)</p>
75 * <p>If this parameter is {@code true}, the episode file generated is called {@code META-INF/sun-jaxb.episode},
76 * and included in the artifact.</p>
77 *
78 * @see #STANDARD_EPISODE_FILENAME
79 * @since 2.0
80 */
81 @Parameter(defaultValue = "true")
82 protected boolean generateEpisode;
83
84 /**
85 * <p>Sets the HTTP/HTTPS proxy to be used by the XJC, on the format
86 * {@code [user[:password]@]proxyHost[:proxyPort]}.
87 * All information is retrieved from the active proxy within the standard maven settings file.</p>
88 */
89 @Parameter(defaultValue = "${settings}", readonly = true)
90 protected Settings settings;
91
92 /**
93 * <p>Defines the content type of sources for the XJC. To simplify usage of the JAXB2 maven plugin,
94 * all source files are assumed to have the same type of content.</p>
95 * <p>This parameter replaces the previous multiple-choice boolean configuration options for the
96 * jaxb2-maven-plugin (i.e. dtd, xmlschema, relaxng, relaxng-compact, wsdl), and
97 * corresponds to setting one of those flags as an XJC argument.</p>
98 *
99 * @since 2.0
100 */
101 @Parameter(defaultValue = "XmlSchema")
102 protected SourceContentType sourceType;
103
104 /**
105 * <p>Corresponding XJC parameter: {@code npa}.</p>
106 * <p>Suppress the generation of package level annotations into {@code package-info.java}.
107 * Using this switch causes the generated code to internalize those annotations into the other
108 * generated classes.</p>
109 *
110 * @since 2.0
111 */
112 @Parameter(defaultValue = "false")
113 protected boolean noPackageLevelAnnotations;
114
115 /**
116 * <p>Corresponding XJC parameter: {@code no-header}.</p>
117 * <p>Suppress the generation of a file header comment that includes some note and timestamp.
118 * Using this makes the generated code more diff-friendly.</p>
119 *
120 * @since 2.0
121 */
122 @Parameter(defaultValue = "false")
123 protected boolean noGeneratedHeaderComments;
124
125 /**
126 * <p>Corresponding XJC parameter: {@code mark-generated}.</p>
127 * <p>This feature causes all of the generated code to have {@code @Generated} annotation.</p>
128 *
129 * @since 2.0
130 */
131 @Parameter(defaultValue = "false")
132 protected boolean addGeneratedAnnotation;
133
134 /**
135 * <p>Corresponding XJC parameter: {@code nv}.</p>
136 * <p>By default, the XJC binding compiler performs strict validation of the source schema before processing it.
137 * Use this option to disable strict schema validation. This does not mean that the binding compiler will not
138 * perform any validation, it simply means that it will perform less-strict validation.</p>
139 *
140 * @since 2.0
141 */
142 @Parameter(defaultValue = "false")
143 protected boolean laxSchemaValidation;
144
145 /**
146 * <p>Corresponding XJC parameter: {@code quiet}.</p>
147 * <p>Suppress compiler output, such as progress information and warnings.</p>
148 */
149 @Parameter(defaultValue = "false")
150 protected boolean quiet;
151
152 /**
153 * <p>Corresponding XJC parameter: {@code verbose}.</p>
154 * <p>Tells XJC to be extra verbose, such as printing informational messages or displaying stack traces.</p>
155 */
156 @Parameter(property = "xjc.verbose", defaultValue = "false")
157 protected boolean verbose;
158
159 /**
160 * <p>Corresponding XJC parameter: {@code extension}.</p>
161 * <p>By default, the XJC binding compiler strictly enforces the rules outlined in the Compatibility chapter of
162 * the JAXB Specification. Appendix E.2 defines a set of W3C XML Schema features that are not completely
163 * supported by JAXB v1.0. In some cases, you may be allowed to use them in the "-extension" mode enabled by
164 * this switch. In the default (strict) mode, you are also limited to using only the binding customizations
165 * defined in the specification.</p>
166 */
167 @Parameter(defaultValue = "false")
168 protected boolean extension;
169
170 /**
171 * Fails the Mojo execution if no XSDs/schemas are found.
172 *
173 * @since 1.3
174 */
175 @Parameter(defaultValue = "true")
176 protected boolean failOnNoSchemas;
177
178 /**
179 * <p>Removes all files from the output directory before running XJC.</p>
180 */
181 @Parameter(defaultValue = "true")
182 protected boolean clearOutputDir;
183
184 /**
185 * <p>Corresponding XJC parameter: {@code readOnly}.</p>
186 * <p>By default, the XJC binding compiler does not write-protect the Java source files it generates.
187 * Use this option to force the XJC binding compiler to mark the generated Java sources read-only.</p>
188 *
189 * @since 2.0
190 */
191 @Parameter(defaultValue = "false")
192 protected boolean readOnly;
193
194 /**
195 * <p>List of ordered extra arguments to the XJC command. Each extra argument is interpreted as a word, intended
196 * to be copied verbatim to the XJC argument list with spaces in between:</p>
197 * <pre>
198 * <code>
199 * <configuration>
200 * ...
201 * <arguments>
202 * <argument>-Xfluent-api</argument>
203 * <argument>somefile</argument>
204 * </arguments>
205 * </configuration>
206 * </code>
207 * </pre>
208 * <p>The arguments configured above yields the following extra arguments to the XJC command:
209 * <code>-Xfluent-api -episode somefile</code></p>
210 *
211 * @since 2.0
212 * @deprecated This should be removed in the 2.0+ release, as all arguments should be handled by other parameters.
213 */
214 @Parameter(property = "xjc.arguments")
215 protected List<String> arguments;
216
217 /**
218 * <p>Corresponding XJC parameter: {@code enableIntrospection}.</p>
219 * <p>Enable correct generation of Boolean getters/setters to enable Bean Introspection APIs.</p>
220 *
221 * @since 1.4
222 */
223 @Parameter(defaultValue = "false")
224 private boolean enableIntrospection;
225
226 /**
227 * <p>Corresponding XJC parameter: {@code p}.</p>
228 * <p>The package under which the source files will be generated. Quoting the XJC documentation:
229 * "Specifying a target package via this command-line option overrides any binding customization for package
230 * name and the default package name algorithm defined in the specification".</p>
231 */
232 @Parameter
233 protected String packageName;
234
235 /**
236 * <p>Corresponding XJC parameter: {@code target}.</p>
237 * <p>Permitted values: {@code "2.0"} and {@code "2.1"}. Avoid generating code that relies on JAXB newer than the
238 * version given. This will allow the generated code to run with JAXB 2.0 runtime (such as JavaSE 6.)</p>
239 *
240 * @since 1.3
241 */
242 @Parameter
243 protected String target;
244
245 /**
246 * <p>If provided, this parameter indicates that the XSDs used by XJC to generate Java code should be
247 * copied into the resulting artifact of this project (the JAR, WAR or whichever artifact type is generated).
248 * The value of the {@code xsdPathWithinArtifact} parameter is the relative path within the artifact where
249 * all source XSDs are copied to (hence the name "XSD Path Within Artifact").</p>
250 * <p>The target directory is created within the artifact if it does not already exist.
251 * If the {@code xsdPathWithinArtifact} parameter is not given, the XSDs used to generate Java code are
252 * <em>not</em> included within the project's artifact.</p>
253 * <p><em>Example:</em>Adding the sample configuration below would copy all source XSDs to the given directory
254 * within the resulting JAR (and/or test-JAR). If the directory {@code META-INF/jaxb/xsd} does not exist, it
255 * will be created.</p>
256 * <pre>
257 * <code>
258 * <configuration>
259 * ...
260 * <xsdPathWithinArtifact>META-INF/jaxb/xsd</xsdPathWithinArtifact>
261 * </configuration>
262 * </code>
263 * </pre>
264 * <p><strong>Note</strong>: This parameter was previously called {@code includeSchemasOutputPath}
265 * in the 1.x versions of this plugin, but was renamed and re-documented for improved usability and clarity.</p>
266 *
267 * @since 2.0
268 */
269 @Parameter
270 protected String xsdPathWithinArtifact;
271
272 /**
273 * <p>Java generation is required if any of the file products is outdated/stale.</p>
274 * {@inheritDoc}
275 */
276 @Override
277 protected boolean isReGenerationRequired() {
278
279 //
280 // Use the stale flag method to identify if we should re-generate the java source code from the supplied
281 // Xml Schema. Basically, we should regenerate the JAXB code if:
282 //
283 // a) The staleFile does not exist
284 // b) The staleFile exists and is older than one of the sources (XSD or XJB files).
285 // "Older" is determined by comparing the modification timestamp of the staleFile and the source files.
286 //
287 final File staleFile = getStaleFile();
288 final String debugPrefix = "StaleFile [" + FileSystemUtilities.getCanonicalPath(staleFile) + "]";
289
290 boolean stale = !staleFile.exists();
291 if (stale) {
292 getLog().debug(debugPrefix + " not found. JAXB (re-)generation required.");
293 } else {
294
295 final List<URL> sourceXSDs = getSources();
296 final List<File> sourceXJBs = getSourceXJBs();
297
298 if (getLog().isDebugEnabled()) {
299 getLog().debug(debugPrefix + " found. Checking timestamps on source XSD and XJB "
300 + "files to determine if JAXB (re-)generation is required.");
301 }
302
303 final long staleFileLastModified = staleFile.lastModified();
304 for (URL current : sourceXSDs) {
305
306 final URLConnection sourceXsdConnection;
307 try {
308 sourceXsdConnection = current.openConnection();
309 sourceXsdConnection.connect();
310 } catch (Exception e) {
311
312 // Can't determine if the staleFile is younger than this sourceXSD.
313 // Re-generate to be on the safe side.
314 stale = true;
315 break;
316 }
317
318 try {
319 if (sourceXsdConnection.getLastModified() > staleFileLastModified) {
320
321 if (getLog().isDebugEnabled()) {
322 getLog().debug(current.toString() + " is newer than the stale flag file.");
323 }
324 stale = true;
325 }
326 } finally {
327 if (sourceXsdConnection instanceof HttpURLConnection) {
328 ((HttpURLConnection) sourceXsdConnection).disconnect();
329 }
330 }
331 }
332
333 for (File current : sourceXJBs) {
334 if (current.lastModified() > staleFileLastModified) {
335
336 if (getLog().isDebugEnabled()) {
337 getLog().debug(FileSystemUtilities.getCanonicalPath(current)
338 + " is newer than the stale flag file.");
339 }
340
341 stale = true;
342 break;
343 }
344 }
345 }
346
347 // All done.
348 return stale;
349 }
350
351 /**
352 * {@inheritDoc}
353 */
354 @Override
355 protected boolean performExecution() throws MojoExecutionException, MojoFailureException {
356
357 boolean updateStaleFileTimestamp = false;
358
359 try {
360
361 // Setup the Tool's execution environment
362 ToolExecutionEnvironment environment = null;
363 try {
364
365 // Create a LocaleFacet if the user has configured an explicit Locale for the tool.
366 final LocaleFacet localeFacet = locale == null ? null : LocaleFacet.createFor(locale, getLog());
367
368 // Create the ToolExecutionEnvironment
369 environment = new ToolExecutionEnvironment(getLog(),
370 ThreadContextClassLoaderBuilder.createFor(this.getClass(), getLog()).addPaths(getClasspath()),
371 LoggingHandlerEnvironmentFacet.create(getLog(), getClass(), getEncoding(false)),
372 localeFacet);
373
374 // Add any extra configured EnvironmentFacets, as configured in the POM.
375 if (extraFacets != null) {
376 for (EnvironmentFacet current : extraFacets) {
377 environment.add(current);
378 }
379 }
380
381 // Setup the environment.
382 environment.setup();
383
384 // Compile the XJC arguments
385 final String[] xjcArguments = getXjcArguments(
386 environment.getClassPathAsArgument(),
387 STANDARD_EPISODE_FILENAME);
388
389 // Ensure that the outputDirectory exists, but only clear it if does not already
390 FileSystemUtilities.createDirectory(getOutputDirectory(), clearOutputDir);
391
392 // Do we need to re-create the episode file's parent directory.
393 final boolean reCreateEpisodeFileParentDirectory = generateEpisode && clearOutputDir;
394 if (reCreateEpisodeFileParentDirectory) {
395 getEpisodeFile(STANDARD_EPISODE_FILENAME);
396 }
397
398 // Check the system properties.
399 logSystemPropertiesAndBasedir();
400
401 // Fire XJC
402 if (XJC_COMPLETED_OK != Driver.run(xjcArguments, new XjcLogAdapter(getLog()))) {
403
404 final StringBuilder errorMsgBuilder = new StringBuilder();
405 errorMsgBuilder.append("\n+=================== [XJC Error]\n");
406 errorMsgBuilder.append("|\n");
407
408 final List<URL> sourceXSDs = getSources();
409 for (int i = 0; i < sourceXSDs.size(); i++) {
410 errorMsgBuilder.append("| " + i + ": ").append(sourceXSDs.get(i).toString()).append("\n");
411 }
412
413 errorMsgBuilder.append("|\n");
414 errorMsgBuilder.append("+=================== [End XJC Error]\n");
415 throw new MojoExecutionException(errorMsgBuilder.toString());
416 }
417
418 // Indicate that the output directory was updated.
419 getBuildContext().refresh(getOutputDirectory());
420
421 // Update the modification timestamp of the staleFile.
422 updateStaleFileTimestamp = true;
423
424 } finally {
425
426 if (environment != null) {
427 environment.restore();
428 }
429 }
430
431 // Add the generated source root to the project, enabling tooling and other plugins to see them.
432 addGeneratedSourcesToProjectSourceRoot();
433
434 // Copy all source XSDs to the resulting artifact?
435 if (xsdPathWithinArtifact != null) {
436
437 final String buildOutputDirectory = getProject().getBuild().getOutputDirectory();
438 final File targetXsdDirectory = new File(buildOutputDirectory, xsdPathWithinArtifact);
439 FileUtils.forceMkdir(targetXsdDirectory);
440
441 for (URL current : getSources()) {
442
443 String fileName = null;
444 if ("file".equalsIgnoreCase(current.getProtocol())) {
445 fileName = new File(current.getPath()).getName();
446 } else if ("jar".equalsIgnoreCase(current.getProtocol())) {
447
448 // Typical JAR path
449 // jar:file:/path/to/aJar.jar!/some/path/xsd/aResource.xsd
450 final int bangIndex = current.toString().indexOf("!");
451 if (bangIndex == -1) {
452 throw new MojoExecutionException("Illegal JAR URL [" + current.toString()
453 + "]: lacks a '!'");
454 }
455
456 final String internalPath = current.toString().substring(bangIndex + 1);
457 fileName = new File(internalPath).getName();
458 } else {
459 throw new MojoExecutionException("Could not extract FileName from URL [" + current + "]");
460 }
461
462 final File targetFile = new File(targetXsdDirectory, fileName);
463 if (targetFile.exists()) {
464
465 // TODO: Should we throw an exception here instead?
466 getLog().warn("File [" + FileSystemUtilities.getCanonicalPath(targetFile)
467 + "] already exists. Not copying XSD file [" + current.getPath() + "] to it.");
468 }
469 IOUtil.copy(current.openStream(), new FileWriter(targetFile));
470 }
471
472 // Refresh the BuildContext
473 getBuildContext().refresh(targetXsdDirectory);
474 }
475 } catch (MojoExecutionException e) {
476 throw e;
477 } catch (NoSchemasException e) {
478 if (failOnNoSchemas) {
479 throw new MojoExecutionException("", e);
480 }
481 } catch (Exception e) {
482 throw new MojoExecutionException(e.getMessage(), e);
483 }
484
485 // All done.
486 return updateStaleFileTimestamp;
487 }
488
489 /**
490 * Override this method to acquire a List holding all URLs to the JAXB sources for which this
491 * AbstractJavaGeneratorMojo should generate Java files. Sources are assumed to be in the form given by
492 * the {@code sourceType} value.
493 *
494 * @return A non-null List holding URLs to sources for the XJC generation.
495 * @see #sourceType
496 */
497 @Override
498 protected abstract List<URL> getSources();
499
500 /**
501 * Override this method to retrieve a list of Files to all XJB files for which this
502 * AbstractJavaGeneratorMojo should generate Java files.
503 *
504 * @return A non-null List holding binding files.
505 */
506 protected abstract List<File> getSourceXJBs();
507
508 /**
509 * Adds any directories containing the generated XJC classes to the appropriate Project compilation sources;
510 * either {@code TestCompileSourceRoot} or {@code CompileSourceRoot} depending on the exact Mojo implementation
511 * of this AbstractJavaGeneratorMojo.
512 */
513 protected abstract void addGeneratedSourcesToProjectSourceRoot();
514
515 /**
516 * Adds the supplied Resource to the project using the appropriate scope (i.e. resource or testResource)
517 * depending on the exact implementation of this AbstractJavaGeneratorMojo.
518 *
519 * @param resource The resource to add.
520 */
521 protected abstract void addResource(final Resource resource);
522
523 //
524 // Private helpers
525 //
526
527 private String[] getXjcArguments(final String classPath, final String episodeFileNameOrNull)
528 throws MojoExecutionException, NoSchemasException {
529
530 final ArgumentBuilder builder = new ArgumentBuilder();
531
532 // Add all flags on the form '-flagName'
533 builder.withFlag(true, sourceType.getXjcArgument());
534 builder.withFlag(noPackageLevelAnnotations, "npa");
535 builder.withFlag(laxSchemaValidation, "nv");
536 builder.withFlag(verbose, "verbose");
537 builder.withFlag(quiet, "quiet");
538 builder.withFlag(enableIntrospection, "enableIntrospection");
539 builder.withFlag(extension, "extension");
540 builder.withFlag(readOnly, "readOnly");
541 builder.withFlag(noGeneratedHeaderComments, "no-header");
542 builder.withFlag(addGeneratedAnnotation, "mark-generated");
543
544 // Add all arguments on the form '-argumentName argumentValue'
545 // (i.e. in 2 separate elements of the returned String[])
546 builder.withNamedArgument("httpproxy", getProxyString(settings.getActiveProxy()));
547 builder.withNamedArgument("encoding", getEncoding(true));
548 builder.withNamedArgument("p", packageName);
549 builder.withNamedArgument("target", target);
550 builder.withNamedArgument("d", getOutputDirectory().getAbsolutePath());
551 builder.withNamedArgument("classpath", classPath);
552
553 if (generateEpisode) {
554
555 // We must use the -extension flag for the episode to work.
556 if (!extension) {
557
558 if (getLog().isInfoEnabled()) {
559 getLog().info("Adding 'extension' flag to XJC arguments, since the 'generateEpisode' argument is "
560 + "given. (XJCs 'episode' argument requires that the 'extension' argument is provided).");
561 }
562 builder.withFlag(true, "extension");
563 }
564
565 final File episodeFile = getEpisodeFile(episodeFileNameOrNull);
566 builder.withNamedArgument("episode", FileSystemUtilities.getCanonicalPath(episodeFile));
567 }
568 if (catalog != null) {
569 builder.withNamedArgument("catalog", FileSystemUtilities.getCanonicalPath(catalog));
570 }
571
572 if (arguments != null) {
573 builder.withPreCompiledArguments(arguments);
574 }
575
576 for (File current : getSourceXJBs()) {
577
578 // Shorten the argument?
579 // final String strippedXjbPath = FileSystemUtilities.relativize(
580 // current.getAbsolutePath(), getProject().getBasedir());
581
582 // Each XJB must be given as a separate argument.
583 builder.withNamedArgument("-b", current.getAbsolutePath());
584 }
585
586 final List<URL> sourceXSDs = getSources();
587 if (sourceXSDs.isEmpty()) {
588
589 // If we have no XSDs, we are not going to be able to run XJC.
590 getLog().warn("No XSD files found. Please check your plugin configuration.");
591 throw new NoSchemasException();
592
593 } else {
594
595 final List<String> unwrappedSourceXSDs = new ArrayList<String>();
596 for (URL current : sourceXSDs) {
597
598 // Shorten the argument if possible.
599 if ("file".equalsIgnoreCase(current.getProtocol())) {
600 unwrappedSourceXSDs.add(FileSystemUtilities.relativize(
601 current.getPath(),
602 new File(System.getProperty("user.dir"))));
603 } else {
604 unwrappedSourceXSDs.add(current.toString());
605 }
606 }
607
608 builder.withPreCompiledArguments(unwrappedSourceXSDs);
609 }
610
611 // All done.
612 return logAndReturnToolArguments(builder.build(), "XJC");
613 }
614
615 private String getProxyString(final Proxy activeProxy) {
616
617 // Check sanity
618 if (activeProxy == null) {
619 return null;
620 }
621
622 // The XJC proxy argument should be on the form
623 // [user[:password]@]proxyHost[:proxyPort]
624 //
625 // builder.withNamedArgument("httpproxy", httpproxy);
626 //
627 final StringBuilder proxyBuilder = new StringBuilder();
628 if (activeProxy.getUsername() != null) {
629
630 // Start with the username.
631 proxyBuilder.append(activeProxy.getUsername());
632
633 // Append the password if provided.
634 if (activeProxy.getPassword() != null) {
635 proxyBuilder.append(":").append(activeProxy.getPassword());
636 }
637
638 proxyBuilder.append("@");
639 }
640
641 // Append hostname and port.
642 proxyBuilder.append(activeProxy.getHost()).append(":").append(activeProxy.getPort());
643
644 // All done.
645 return proxyBuilder.toString();
646 }
647 }