class ProjectGenerationController<R extends ProjectRequest> {

	private static final Log logger = LogFactory.getLog(ProjectGenerationController.class);

	private final InitializrMetadataProvider metadataProvider;

	private final ProjectGenerationInvoker<R> projectGenerationInvoker;

	public ProjectGenerationController(InitializrMetadataProvider metadataProvider,
			ProjectGenerationInvoker<R> projectGenerationInvoker) {
		this.metadataProvider = metadataProvider;
		this.projectGenerationInvoker = projectGenerationInvoker;
	}

	@ModelAttribute
	R projectRequest(@RequestHeader Map<String, String> headers,
			@RequestParam(name = "style", required = false) String style) {
		if (style != null) {
			throw new InvalidProjectRequestException("Dependencies must be specified using 'dependencies'");
		}
		return projectRequest(headers);
	}

	/**
	 * Create an initialized {@link ProjectRequest} instance to use to bind the parameters
	 * of a project generation request.
	 * @param headers the headers of the request
	 * @return a new {@link ProjectRequest} instance
	 */
	public abstract R projectRequest(@RequestHeader Map<String, String> headers);

	protected InitializrMetadata getMetadata() {
		return this.metadataProvider.get();
	}

	@ExceptionHandler
	public void invalidProjectRequest(HttpServletResponse response, InvalidProjectRequestException ex)
			throws IOException {
		response.sendError(HttpStatus.BAD_REQUEST.value(), ex.getMessage());
	}

	@RequestMapping(path = { "/pom", "/pom.xml" }, method = { RequestMethod.GET, RequestMethod.POST })
	public ResponseEntity<byte[]> pom(R request) {
		request.setType("maven-build");
		byte[] mavenPom = this.projectGenerationInvoker.invokeBuildGeneration(request);
		return createResponseEntity(mavenPom, "application/octet-stream", "pom.xml");
	}

	@RequestMapping(path = { "/build", "/build.gradle" }, method = { RequestMethod.GET, RequestMethod.POST })
	public ResponseEntity<byte[]> gradle(R request) {
		request.setType("gradle-build");
		byte[] gradleBuild = this.projectGenerationInvoker.invokeBuildGeneration(request);
		return createResponseEntity(gradleBuild, "application/octet-stream", "build.gradle");
	}

	@RequestMapping(path = "/starter.zip", method = { RequestMethod.GET, RequestMethod.POST })
	public ResponseEntity<byte[]> springZip(R request) throws IOException {
		ProjectGenerationResult result = this.projectGenerationInvoker.invokeProjectStructureGeneration(request);
		Path archive = createArchive(result, "zip", ZipArchiveOutputStream::new, ZipArchiveEntry::new,
				ZipArchiveEntry::setUnixMode);
		return upload(archive, result.getRootDirectory(),
				generateFileName(result.getProjectDescription().getArtifactId(), "zip"), "application/zip");
	}

	@RequestMapping(path = "/starter.tgz", method = { RequestMethod.GET, RequestMethod.POST },
			produces = "application/x-compress")
	public ResponseEntity<byte[]> springTgz(R request) throws IOException {
		ProjectGenerationResult result = this.projectGenerationInvoker.invokeProjectStructureGeneration(request);
		Path archive = createArchive(result, "tar.gz", this::createTarArchiveOutputStream, TarArchiveEntry::new,
				TarArchiveEntry::setMode);
		return upload(archive, result.getRootDirectory(),
				generateFileName(result.getProjectDescription().getArtifactId(), "tar.gz"), "application/x-compress");
	}

	private TarArchiveOutputStream createTarArchiveOutputStream(OutputStream output) {
		try {
			TarArchiveOutputStream out = new TarArchiveOutputStream(new GzipCompressorOutputStream(output));
			out.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX);
			return out;
		}
		catch (IOException ex) {
			throw new IllegalStateException(ex);
		}
	}

	private <T extends ArchiveEntry> Path createArchive(ProjectGenerationResult result, String fileExtension,
			Function<OutputStream, ? extends ArchiveOutputStream> archiveOutputStream,
			BiFunction<File, String, T> archiveEntry, BiConsumer<T, Integer> setMode) throws IOException {
		Path archive = this.projectGenerationInvoker.createDistributionFile(result.getRootDirectory(),
				"." + fileExtension);
		String wrapperScript = getWrapperScript(result.getProjectDescription());
		try (ArchiveOutputStream output = archiveOutputStream.apply(Files.newOutputStream(archive))) {
			Files.walk(result.getRootDirectory())
				.filter((path) -> !result.getRootDirectory().equals(path))
				.forEach((path) -> {
					try {
						String entryName = getEntryName(result.getRootDirectory(), path);
						T entry = archiveEntry.apply(path.toFile(), entryName);
						setMode.accept(entry, getUnixMode(wrapperScript, entryName, path));
						output.putArchiveEntry(entry);
						if (!Files.isDirectory(path)) {
							Files.copy(path, output);
						}
						output.closeArchiveEntry();
					}
					catch (IOException ex) {
						throw new IllegalStateException(ex);
					}
				});
		}
		return archive;
	}

	private String getEntryName(Path root, Path path) {
		String entryName = root.relativize(path).toString().replace('\\', '/');
		if (Files.isDirectory(path)) {
			entryName += "/";
		}
		return entryName;
	}

	private int getUnixMode(String wrapperScript, String entryName, Path path) {
		if (Files.isDirectory(path)) {
			return UnixStat.DIR_FLAG | UnixStat.DEFAULT_DIR_PERM;
		}
		return UnixStat.FILE_FLAG | (entryName.equals(wrapperScript) ? 0755 : UnixStat.DEFAULT_FILE_PERM);
	}

	private String generateFileName(String artifactId, String extension) {
		String candidate = (StringUtils.hasText(artifactId) ? artifactId
				: this.metadataProvider.get().getArtifactId().getContent());
		try {
			return URLEncoder.encode(candidate, "UTF-8") + "." + extension;
		}
		catch (UnsupportedEncodingException ex) {
			throw new IllegalStateException("Cannot encode URL", ex);
		}
	}

	private static String getWrapperScript(ProjectDescription description) {
		BuildSystem buildSystem = description.getBuildSystem();
		String script = buildSystem.id().equals(MavenBuildSystem.ID) ? "mvnw" : "gradlew";
		return (description.getBaseDirectory() != null) ? description.getBaseDirectory() + "/" + script : script;
	}

	private ResponseEntity<byte[]> upload(Path archive, Path dir, String fileName, String contentType)
			throws IOException {<FILL_FUNCTION_BODY>}

	private ResponseEntity<byte[]> createResponseEntity(byte[] content, String contentType, String fileName) {
		String contentDispositionValue = "attachment; filename=\"" + fileName + "\"";
		return ResponseEntity.ok()
			.header("Content-Type", contentType)
			.header("Content-Disposition", contentDispositionValue)
			.body(content);
	}

}

		byte[] bytes = Files.readAllBytes(archive);
		logger.info(String.format("Uploading: %s (%s bytes)", archive, bytes.length));
		ResponseEntity<byte[]> result = createResponseEntity(bytes, contentType, fileName);
		this.projectGenerationInvoker.cleanTempFiles(dir);
		return result;
	
<no_super_class>