All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.carma.swagger.doclet.parser.JaxRsAnnotationParser Maven / Gradle / Ivy

package com.carma.swagger.doclet.parser;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import com.carma.swagger.doclet.DocletOptions;
import com.carma.swagger.doclet.Recorder;
import com.carma.swagger.doclet.ServiceDoclet;
import com.carma.swagger.doclet.model.Api;
import com.carma.swagger.doclet.model.ApiDeclaration;
import com.carma.swagger.doclet.model.HttpMethod;
import com.carma.swagger.doclet.model.Operation;
import com.carma.swagger.doclet.model.ResourceListing;
import com.carma.swagger.doclet.model.ResourceListingAPI;
import com.google.common.base.Strings;
import com.google.common.io.ByteStreams;
import com.sun.javadoc.ClassDoc;
import com.sun.javadoc.MethodDoc;
import com.sun.javadoc.RootDoc;
import com.sun.javadoc.Type;

@SuppressWarnings("javadoc")
public class JaxRsAnnotationParser {

	// swagger 1.1 spec see https://groups.google.com/forum/#!topic/swagger-swaggersocket/mHdR9u0utH4
	// diffs between 1.1 and 1.2 see https://github.com/wordnik/swagger-spec/wiki/1.2-transition
	private static final String SWAGGER_VERSION = "1.2";

	private static final String SWAGGER_UI_VERSION = "2.1.0";

	private final DocletOptions options;
	private final RootDoc rootDoc;

	private static final  void addIfNotNull(Collection collection, T item) {
		if (item != null) {
			collection.add(item);
		}
	}

	public JaxRsAnnotationParser(DocletOptions options, RootDoc rootDoc) {
		this.options = options;
		this.rootDoc = rootDoc;
	}

	public boolean run() {
		try {

			// setup additional classes needed for processing, generally these are java ones such as java.lang.String
			// adding them here allows them to be used in @outputType
			Collection typeClasses = new ArrayList();
			addIfNotNull(typeClasses, this.rootDoc.classNamed(java.lang.String.class.getName()));
			addIfNotNull(typeClasses, this.rootDoc.classNamed(java.lang.Integer.class.getName()));
			addIfNotNull(typeClasses, this.rootDoc.classNamed(java.lang.Boolean.class.getName()));
			addIfNotNull(typeClasses, this.rootDoc.classNamed(java.lang.Float.class.getName()));
			addIfNotNull(typeClasses, this.rootDoc.classNamed(java.lang.Double.class.getName()));
			addIfNotNull(typeClasses, this.rootDoc.classNamed(java.lang.Character.class.getName()));
			addIfNotNull(typeClasses, this.rootDoc.classNamed(java.lang.Long.class.getName()));
			addIfNotNull(typeClasses, this.rootDoc.classNamed(java.lang.Byte.class.getName()));
			addIfNotNull(typeClasses, this.rootDoc.classNamed(java.util.Date.class.getName()));
			addIfNotNull(typeClasses, this.rootDoc.classNamed(java.util.Map.class.getName()));
			addIfNotNull(typeClasses, this.rootDoc.classNamed(java.util.Collection.class.getName()));
			addIfNotNull(typeClasses, this.rootDoc.classNamed(java.util.Set.class.getName()));
			addIfNotNull(typeClasses, this.rootDoc.classNamed(java.util.List.class.getName()));
			addIfNotNull(typeClasses, this.rootDoc.classNamed(java.math.BigInteger.class.getName()));
			addIfNotNull(typeClasses, this.rootDoc.classNamed(java.math.BigDecimal.class.getName()));
			addIfNotNull(typeClasses, this.rootDoc.classNamed(java.util.UUID.class.getName()));
			addIfNotNull(typeClasses, this.rootDoc.classNamed(java.time.DayOfWeek.class.getName()));
			addIfNotNull(typeClasses, this.rootDoc.classNamed(java.time.Duration.class.getName()));
			addIfNotNull(typeClasses, this.rootDoc.classNamed(java.time.Instant.class.getName()));
			addIfNotNull(typeClasses, this.rootDoc.classNamed(java.time.LocalDate.class.getName()));
			addIfNotNull(typeClasses, this.rootDoc.classNamed(java.time.LocalDateTime.class.getName()));
			addIfNotNull(typeClasses, this.rootDoc.classNamed(java.time.Month.class.getName()));
			addIfNotNull(typeClasses, this.rootDoc.classNamed(java.time.MonthDay.class.getName()));
			addIfNotNull(typeClasses, this.rootDoc.classNamed(java.time.OffsetDateTime.class.getName()));
			addIfNotNull(typeClasses, this.rootDoc.classNamed(java.time.OffsetTime.class.getName()));
			addIfNotNull(typeClasses, this.rootDoc.classNamed(java.time.Period.class.getName()));
			addIfNotNull(typeClasses, this.rootDoc.classNamed(java.time.Year.class.getName()));
			addIfNotNull(typeClasses, this.rootDoc.classNamed(java.time.YearMonth.class.getName()));
			addIfNotNull(typeClasses, this.rootDoc.classNamed(java.time.ZoneId.class.getName()));
			addIfNotNull(typeClasses, this.rootDoc.classNamed(java.time.ZoneOffset.class.getName()));
			addIfNotNull(typeClasses, this.rootDoc.classNamed(java.time.ZonedDateTime.class.getName()));

			// filter the classes to process
			Collection docletClasses = new ArrayList();
			for (ClassDoc classDoc : this.rootDoc.classes()) {

				// see if excluded via its FQN
				boolean excludeResource = false;
				if (this.options.getExcludeResourcePrefixes() != null && !this.options.getExcludeResourcePrefixes().isEmpty()) {
					for (String prefix : this.options.getExcludeResourcePrefixes()) {
						String className = classDoc.qualifiedName();
						if (className.startsWith(prefix)) {
							excludeResource = true;
							break;
						}
					}
				}

				// see if the inclusion filter is set and if so this resource must match this
				if (!excludeResource && this.options.getIncludeResourcePrefixes() != null && !this.options.getIncludeResourcePrefixes().isEmpty()) {
					boolean matched = false;
					for (String prefix : this.options.getIncludeResourcePrefixes()) {
						String className = classDoc.qualifiedName();
						if (className.startsWith(prefix)) {
							matched = true;
							break;
						}
					}
					excludeResource = !matched;
				}

				if (excludeResource) {
					continue;
				}

				// see if deprecated
				if (this.options.isExcludeDeprecatedResourceClasses() && ParserHelper.isDeprecated(classDoc, this.options)) {
					continue;
				}

				// see if excluded via a tag
				if (ParserHelper.hasTag(classDoc, this.options.getExcludeClassTags())) {
					continue;
				}

				docletClasses.add(classDoc);
			}

			ClassDocCache classCache = new ClassDocCache(docletClasses);

			List declarations = null;

			// build up set of subresources
			// do simple parsing to find sub resource classes
			// these are ones referenced in the return types of methods
			// which have a path but no http method
			Map subResourceClasses = new HashMap();
			for (ClassDoc classDoc : docletClasses) {
				ClassDoc currentClassDoc = classDoc;
				while (currentClassDoc != null) {

					for (MethodDoc method : currentClassDoc.methods()) {
						// if the method has @Path but no Http method then its an entry point to a sub resource
						if (!ParserHelper.resolveMethodPath(method, this.options).isEmpty() && HttpMethod.fromMethod(method) == null) {
							ClassDoc subResourceClassDoc = classCache.findByType(method.returnType());
							if (subResourceClassDoc != null) {
								if (this.options.isLogDebug()) {
									System.out.println("Adding return type as sub resource class : " + subResourceClassDoc.name() + " for method "
											+ method.name() + " of referencing class " + currentClassDoc.name());
								}
								subResourceClasses.put(method.returnType(), subResourceClassDoc);
							}
						}
					}

					currentClassDoc = currentClassDoc.superclass();

					// ignore parent object class
					if (!ParserHelper.hasAncestor(currentClassDoc)) {
						break;
					}
				}
			}

			// parse with the v2 parser that supports endpoints of the same resource being spread across resource files
			Map resourceToDeclaration = new HashMap();
			for (ClassDoc classDoc : docletClasses) {
				CrossClassApiParser classParser = new CrossClassApiParser(this.options, classDoc, docletClasses, subResourceClasses, typeClasses,
						SWAGGER_VERSION, this.options.getApiVersion(), this.options.getApiBasePath());
				classParser.parse(resourceToDeclaration);
			}
			Collection declarationColl = resourceToDeclaration.values();

			if (this.options.isLogDebug()) {
				System.out.println("After parse phase api declarations are: ");
				for (ApiDeclaration apiDec : declarationColl) {
					System.out.println("Api Dec: base path " + apiDec.getBasePath() + ", res path: " + apiDec.getResourcePath());
					for (Api api : apiDec.getApis()) {
						System.out.println("Api path:" + api.getPath());
						for (Operation op : api.getOperations()) {
							System.out.println("Api nick name:" + op.getNickname() + " method " + op.getMethod());
						}
					}
				}
			}

			// add any extra declarations
			if (this.options.getExtraApiDeclarations() != null && !this.options.getExtraApiDeclarations().isEmpty()) {
				declarationColl = new ArrayList(declarationColl);
				declarationColl.addAll(this.options.getExtraApiDeclarations());
			}

			// set root path on any empty resources
			for (ApiDeclaration api : declarationColl) {
				if (api.getResourcePath() == null || api.getResourcePath().isEmpty() || api.getResourcePath().equals("/")) {
					api.setResourcePath(this.options.getResourceRootPath());
				}
			}

			// merge the api declarations
			declarationColl = new ApiDeclarationMerger(SWAGGER_VERSION, this.options.getApiVersion(), this.options.getApiBasePath()).merge(declarationColl);

			// clear any empty models
			for (ApiDeclaration api : declarationColl) {
				if (api.getModels() != null && api.getModels().isEmpty()) {
					api.setModels(null);
				}
			}

			declarations = new ArrayList(declarationColl);

			// sort the api declarations if needed
			if (this.options.isSortResourcesByPriority()) {

				Collections.sort(declarations, new Comparator() {

					public int compare(ApiDeclaration dec1, ApiDeclaration dec2) {
						return Integer.compare(dec1.getPriority(), dec2.getPriority());
					}

				});

			} else if (this.options.isSortResourcesByPath()) {
				Collections.sort(declarations, new Comparator() {

					public int compare(ApiDeclaration dec1, ApiDeclaration dec2) {
						if (dec1 == null || dec1.getResourcePath() == null) {
							return 1;
						}
						if (dec2 == null || dec2.getResourcePath() == null) {
							return -1;
						}
						return dec1.getResourcePath().compareTo(dec2.getResourcePath());
					}

				});
			}

			// sort apis of each declaration
			if (this.options.isSortApisByPath()) {
				for (ApiDeclaration dec : declarations) {
					if (dec.getApis() != null) {
						Collections.sort(dec.getApis(), new Comparator() {

							public int compare(Api o1, Api o2) {
								if (o1 == null || o1.getPath() == null) {
									return -1;
								}
								return o1.getPath().compareTo(o2.getPath());
							}
						});
					}
				}
			}

			writeApis(declarations);
			// Copy swagger-ui into the output directory.
			if (this.options.isIncludeSwaggerUi()) {
				copyUi();
			}
			return true;
		} catch (IOException e) {
			System.err.println("Failed to write api docs, err msg: " + e.getMessage());
			e.printStackTrace();
			return false;
		}
	}

	private void writeApis(Collection apis) throws IOException {

		List resources = new LinkedList();
		File outputDirectory = this.options.getOutputDirectory();
		Recorder recorder = this.options.getRecorder();
		for (ApiDeclaration api : apis) {
			String resourcePath = api.getResourcePath();
			if (!Strings.isNullOrEmpty(resourcePath)) {
				// make sure the filename for the resource is valid
				String resourceFile = ParserHelper.generateResourceFilename(resourcePath);
				resources.add(new ResourceListingAPI("/" + resourceFile + ".{format}", api.getDescription()));
				File apiFile = new File(outputDirectory, resourceFile + ".json");
				recorder.record(apiFile, api);
			}
		}

		// write out json for the resource listing
		ResourceListing listing = new ResourceListing(SWAGGER_VERSION, this.options.getApiVersion(), this.options.getDocBasePath(), resources,
				this.options.getApiAuthorizations(), this.options.getApiInfo());
		File docFile = new File(outputDirectory, "service.json");
		recorder.record(docFile, listing);

	}

	private void copyUi() throws IOException {
		File outputDirectory = this.options.getOutputDirectory();
		if (outputDirectory == null) {
			outputDirectory = new File(".");
		}
		Recorder recorder = this.options.getRecorder();
		String uiPath = this.options.getSwaggerUiPath();

		if (uiPath == null) {
			// default inbuilt zip
			copyZip(recorder, null, outputDirectory);
		} else {
			// zip or dir
			File uiPathFile = new File(uiPath);
			if (uiPathFile.isDirectory()) {
				System.out.println("Using swagger dir from: " + uiPathFile.getAbsolutePath());
				copyDirectory(recorder, uiPathFile, uiPathFile, outputDirectory);
			} else if (!uiPathFile.exists()) {
				File f = new File(".");
				System.out.println("SwaggerDoclet working directory: " + f.getAbsolutePath());
				System.out.println("-swaggerUiPath not set correctly as it did not exist: " + uiPathFile.getAbsolutePath());
				throw new RuntimeException("-swaggerUiPath not set correctly as it did not exist: " + uiPathFile.getAbsolutePath());
			} else {
				copyZip(recorder, uiPathFile, outputDirectory);
			}
		}
	}

	private void copyZip(Recorder recorder, File uiPathFile, File outputDirectory) throws IOException {
		ZipInputStream swaggerZip = null;
		try {
			if (uiPathFile == null) {
				swaggerZip = new ZipInputStream(ServiceDoclet.class.getResourceAsStream("/swagger-ui-" + SWAGGER_UI_VERSION + ".zip"));
				System.out.println("Using default swagger-ui.zip file from SwaggerDoclet jar file");
			} else {
				swaggerZip = new ZipInputStream(new FileInputStream(uiPathFile));
				System.out.println("Using swagger-ui.zip file from: " + uiPathFile.getAbsolutePath());
			}

			ZipEntry entry = swaggerZip.getNextEntry();
			while (entry != null) {
				final File swaggerFile = new File(outputDirectory, entry.getName());
				if (entry.isDirectory()) {
					if (!swaggerFile.isDirectory() && !swaggerFile.mkdirs()) {
						throw new RuntimeException("Unable to create directory: " + swaggerFile);
					}
				} else {

					FileOutputStream outputStream = null;
					try {
						outputStream = new FileOutputStream(swaggerFile);
						ByteStreams.copy(swaggerZip, outputStream);
						outputStream.flush();
					} finally {
						if (outputStream != null) {
							outputStream.close();
						}
					}

				}

				entry = swaggerZip.getNextEntry();
			}

		} finally {
			if (swaggerZip != null) {
				swaggerZip.close();
			}
		}
	}

	private void copyDirectory(Recorder recorder, File uiPathFile, File sourceLocation, File targetLocation) throws IOException {
		if (sourceLocation.isDirectory()) {
			if (!targetLocation.exists()) {
				if (!targetLocation.mkdirs()) {
					throw new IOException("Failed to create the dir: " + targetLocation.getAbsolutePath());
				}
			}

			String[] children = sourceLocation.list();
			if (children != null) {
				for (String element : children) {
					copyDirectory(recorder, uiPathFile, new File(sourceLocation, element), new File(targetLocation, element));
				}
			}
		} else {

			InputStream in = null;
			OutputStream out = null;
			try {
				in = new FileInputStream(sourceLocation);
				out = new FileOutputStream(targetLocation);
				ByteStreams.copy(in, out);
				out.flush();

			} finally {
				if (in != null) {
					try {
						in.close();
					} catch (IOException ex) {
						// ignore
					}
				}
				if (out != null) {
					try {
						out.close();
					} catch (IOException ex) {
						// ignore
					}
				}
			}
		}
	}

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy