Convert the nascent Jem code into Java

Fixes #1
This commit is contained in:
R. Tyler Croy 2015-08-13 17:31:40 -07:00
parent 55fc37d9cf
commit 65fd115fa3
No known key found for this signature in database
GPG Key ID: 1426C7DC3F51E16F
15 changed files with 437 additions and 390 deletions

View File

@ -1,10 +1,12 @@
apply plugin: 'java'
apply plugin: 'groovy'
apply plugin: 'maven'
apply plugin: 'codenarc'
version = '0.1'
version = '0.1.0'
group = 'com.github.jrubygradle'
description = 'A Groovy-based library for managing Ruby gems'
description = 'A library for managing Ruby gems'
defaultTasks 'check', 'assemble'
repositories {
jcenter()

View File

@ -1,17 +0,0 @@
package com.github.jrubygradle.jem
import com.fasterxml.jackson.annotation.JsonProperty
import groovy.transform.CompileStatic
import groovy.transform.TypeChecked
/**
*/
@TypeChecked
@CompileStatic
class Dependency {
@JsonProperty
String name
@JsonProperty
List<Requirement> requirements
}

View File

@ -0,0 +1,14 @@
package com.github.jrubygradle.jem;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
*/
public class Dependency {
@JsonProperty
public String name;
@JsonProperty
public List<Requirement> requirements;
}

View File

@ -1,135 +0,0 @@
package com.github.jrubygradle.jem
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
import groovy.json.JsonOutput
import groovy.transform.CompileStatic
import groovy.transform.TypeChecked
/**
* Plain Old Groovy Object for an enumeration of metadata provided by a gem
*/
@TypeChecked
@CompileStatic
class Gem {
@JsonProperty
String name
@JsonProperty
Version version
@JsonProperty
String description
@JsonProperty
String platform
@JsonProperty
Object email
@JsonProperty
String homepage
@JsonProperty
List<String> authors = []
@JsonProperty
List<String> files
@JsonProperty(value='test_files')
List<String> testFiles
@JsonProperty
List<String> executables
@JsonProperty
String bindir
@JsonProperty(value='require_paths')
List<String> requirePaths
@JsonProperty
List<String> licenses
@JsonProperty(value='specification_version')
Integer specificationVersion
@JsonProperty(value='rubygems_version')
String rubygemsVersion
/**
* Take the given argument and produce a {@code Gem} instance
*
* @param metadata a {@code java.lang.String}, a {@code java.io.File} or a {@code java.util.zip.GZIPInputStream}
* @return
*/
static Gem fromFile(Object metadata) {
if (metadata instanceof String) {
return createGemFromFile(new File(metadata))
}
if (metadata instanceof File) {
return createGemFromFile(metadata as File)
}
if (metadata instanceof InputStream) {
return createGemFromInputStream(metadata as InputStream)
}
return null
}
/**
* Output the gemspec stub for this file
*
* See <https://github.com/rubygems/rubygems/blob/165030689defe16680b7f336232db62024f49de4/lib/rubygems/specification.rb#L2422-L2512>
*
* @return
*/
String toRuby() {
return """\
# -*- encoding: utf-8 -*-
# stub: ${name} ${version.version} ${platform} ${requirePaths.join("\0")}
#
# NOTE: This specification was generated by groovy-gem
# <https://github.com/jruby-gradle/groovy-gem>
Gem::Specification.new do |s|
s.name = ${sanitize(name)}
s.version = ${sanitize(version.version)}
s.description = ${sanitize(description)}
s.homepage = ${sanitize(homepage)}
s.authors = ${sanitize(authors)}
s.email = ${sanitize(email)}
s.licenses = ${sanitize(licenses)}
s.platform = ${sanitize(platform)}
s.require_paths = ${sanitize(requirePaths)}
s.executables = ${sanitize(executables)}
s.rubygems_version = ${sanitize(rubygemsVersion)}
end
"""
}
/** Convert whatever object we're given into a safe (see: JSON) reprepsentation */
protected String sanitize(Object value) {
return JsonOutput.toJson(value)
}
private static Gem createGemFromFile(File gemMetadataFile) {
if (!gemMetadataFile.exists()) {
return null
}
return getYamlMapper().readValue(gemMetadataFile, Gem)
}
private static Gem createGemFromInputStream(InputStream gemMetadataStream) {
return getYamlMapper().readValue(gemMetadataStream, Gem)
}
private static ObjectMapper getYamlMapper() {
ObjectMapper mapper = new ObjectMapper(new YAMLFactory())
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
return mapper
}
}

View File

@ -0,0 +1,155 @@
package com.github.jrubygradle.jem;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import java.io.File;
import java.io.IOError;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
/**
* Plain Old Java Object for an enumeration of metadata provided by a gem
*/
public class Gem {
@JsonProperty
public String name;
@JsonProperty
public Version version;
@JsonProperty
public String description;
@JsonProperty
public String platform;
@JsonProperty
public Object email;
@JsonProperty
public String homepage;
@JsonProperty
public List<String> authors;
@JsonProperty
public List<String> files;
@JsonProperty(value="test_files")
public List<String> testFiles;
@JsonProperty
public List<String> executables;
@JsonProperty
public String bindir;
@JsonProperty(value="require_paths")
public List<String> requirePaths;
@JsonProperty
public List<String> licenses;
@JsonProperty(value="specification_version")
public Integer specificationVersion;
@JsonProperty(value="rubygems_version")
public String rubygemsVersion;
/**
* Take the given argument and produce a {@code Gem} instance
*
* @param metadata a {@code java.lang.String}, a {@code java.io.File} or a {@code java.util.zip.GZIPInputStream}
* @return
*/
public static Gem fromFile(Object metadata) throws JsonProcessingException, IOException {
if (metadata instanceof String) {
return createGemFromFile(new File((String)metadata));
}
if (metadata instanceof File) {
return createGemFromFile((File)(metadata));
}
if (metadata instanceof InputStream) {
return createGemFromInputStream((InputStream)(metadata));
}
return null;
}
/**
* Output the gemspec stub for this file
*
* See <https://github.com/rubygems/rubygems/blob/165030689defe16680b7f336232db62024f49de4/lib/rubygems/specification.rb#L2422-L2512>
*
* @return
*/
public String toRuby() throws JsonProcessingException {
String[] specification = {
"# -*- encoding: utf-8 -*-",
"#",
String.format("# stub: %s %s %s %s",
name, version.version, platform, join(requirePaths.toArray(new String[0]), "\0")),
"#",
"# NOTE: This specification was generated by `jem`",
"# <https://github.com/jruby-gradle/jem>",
"",
"Gem::Specification.new do |s|",
" s.name = " + sanitize(name),
" s.version = " + sanitize(version.version),
" s.description = " + sanitize(description),
" s.homepage = " + sanitize(homepage),
" s.authors = " + sanitize(authors),
" s.email = " + sanitize(email),
" s.licenses = " + sanitize(licenses),
"",
" s.platform = " + sanitize(platform),
" s.require_paths = " + sanitize(requirePaths),
" s.executables = " + sanitize(executables),
" s.rubygems_version = " + sanitize(rubygemsVersion),
"end",
};
return join(specification);
}
private String join(String[] segments) {
return this.join(segments, System.getProperty("line.separator"));
}
private String join(String[] segments, String split) {
StringBuilder builder = new StringBuilder();
for (String segment : segments) {
builder.append(String.format("%s%s", segment, split));
}
return builder.toString();
}
/** Convert whatever object we're given into a safe (see: JSON) reprepsentation */
protected String sanitize(Object value) throws JsonProcessingException {
ObjectMapper mapper = new ObjectMapper();
return mapper.writeValueAsString(value);
}
private static Gem createGemFromFile(File gemMetadataFile) throws
JsonProcessingException, IOException {
if (!gemMetadataFile.exists()) {
return null;
}
return getYamlMapper().readValue(gemMetadataFile, Gem.class);
}
private static Gem createGemFromInputStream(InputStream gemMetadataStream) throws
JsonProcessingException, IOException {
return getYamlMapper().readValue(gemMetadataStream, Gem.class);
}
private static ObjectMapper getYamlMapper() {
ObjectMapper mapper = new ObjectMapper(new YAMLFactory());
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
return mapper;
}
}

View File

@ -1,36 +0,0 @@
package com.github.jrubygradle.jem
import groovy.transform.CompileStatic
import com.github.jrubygradle.jem.internal.GemInstaller as GemInstallerImpl
/**
* GemInstaller manages the installation of a .gem file into a given directory
*/
@CompileStatic
class GemInstaller {
static enum DuplicateBehavior {
OVERWRITE,
SKIP,
FAIL
}
protected GemInstallerImpl impl
GemInstaller(String installDir, String gemPath) {
this(installDir, new File(gemPath))
}
GemInstaller(String installDir, File gemFile) {
this(installDir, [gemFile])
}
GemInstaller(String installDir, List<File> gemPaths) {
impl = new GemInstallerImpl(installDir, gemPaths)
}
void install() {
impl.install()
}
}

View File

@ -0,0 +1,35 @@
package com.github.jrubygradle.jem;
import java.io.File;
import java.util.Arrays;
import java.util.List;
/**
* GemInstaller manages the installation of a .gem file into a given directory
*/
public class GemInstaller {
public static enum DuplicateBehavior {
OVERWRITE,
SKIP,
FAIL
};
protected com.github.jrubygradle.jem.internal.GemInstaller impl;
public GemInstaller(String installDir, String gemPath) {
this(installDir, new File(gemPath));
}
public GemInstaller(String installDir, File gemFile) {
this(installDir, Arrays.asList(gemFile));
}
public GemInstaller(String installDir, List<File> gemPaths) {
impl = new com.github.jrubygradle.jem.internal.GemInstaller(installDir, gemPaths);
}
public void install() {
impl.install();
}
}

View File

@ -1,7 +0,0 @@
package com.github.jrubygradle.jem
/**
* Reader for taking a .gem flie and turning it into objects
*/
class GemReader {
}

View File

@ -1,11 +0,0 @@
package com.github.jrubygradle.jem
import groovy.transform.CompileStatic
import groovy.transform.TypeChecked
/**
*/
@TypeChecked
@CompileStatic
class Requirement {
}

View File

@ -0,0 +1,6 @@
package com.github.jrubygradle.jem;
/**
*/
public class Requirement {
}

View File

@ -1,12 +0,0 @@
package com.github.jrubygradle.jem
import com.fasterxml.jackson.annotation.JsonProperty
import groovy.transform.CompileStatic
/**
*/
@CompileStatic
class Version {
@JsonProperty
String version
}

View File

@ -0,0 +1,10 @@
package com.github.jrubygradle.jem;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
*/
public class Version {
@JsonProperty
public String version;
}

View File

@ -1,169 +0,0 @@
package com.github.jrubygradle.jem.internal
import org.jboss.shrinkwrap.api.ArchiveFormat
import org.jboss.shrinkwrap.api.ArchivePath
import org.jboss.shrinkwrap.api.GenericArchive
import org.jboss.shrinkwrap.api.ShrinkWrap
import org.jboss.shrinkwrap.api.Node
import org.jboss.shrinkwrap.api.exporter.ExplodedExporter
import org.jboss.shrinkwrap.api.importer.TarImporter
import org.jboss.shrinkwrap.impl.base.io.IOUtil
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import groovy.transform.CompileStatic
import com.github.jrubygradle.jem.Gem
import com.github.jrubygradle.jem.GemInstaller.DuplicateBehavior
import java.nio.file.Files
import java.util.zip.GZIPInputStream
@CompileStatic
class GemInstaller {
static final List<String> GEM_HOME_DIRS = ['bin', 'build_info', 'cache', 'doc',
'extensions', 'gems', 'specifications']
protected Logger logger = LoggerFactory.getLogger(this.class)
protected File installDirectory
protected List<File> gems
GemInstaller(String installDir, List<File> gems) {
this.installDirectory = new File(installDir)
this.gems = gems
}
/** Install and overwrite anything that stands in the way */
void install() {
install(DuplicateBehavior.OVERWRITE)
}
void install(DuplicateBehavior onDuplicateBehavior) {
if (!mkdirs()) {
/* raise some exception? */
}
gems.each { File gem ->
installGem(installDirectory, gem, onDuplicateBehavior)
}
}
boolean installGem(File installDir, File gem, DuplicateBehavior onDuplicate) {
/* TODO: isValidGem? */
cacheGemInInstallDir(installDir, gem)
GenericArchive gemArchive = ShrinkWrap.create(TarImporter).importFrom(gem).as(GenericArchive)
Node metadata = gemArchive.get('metadata.gz')
GenericArchive dataArchive = gemArchive.getAsType(GenericArchive.class,
"data.tar.gz",
ArchiveFormat.TAR_GZ);
Gem gemMetadata = Gem.fromFile(new GZIPInputStream(metadata.asset.openStream()))
logger.info("We've processed metadata for ${gemMetadata.name} at version ${gemMetadata.version}")
extractSpecification(installDir, gemMetadata)
extractData(installDir, dataArchive, gemMetadata)
extractExecutables(installDir, dataArchive, gemMetadata)
return true
}
/**
* Create the requisite directories to map a GEM_HOME structure, namely:
* bin/
* build_info/
* cache/
* doc/
* extensions/
* gems/
* specifications/
*
* @return True if all directories were created successfully
*/
boolean mkdirs() {
boolean success = true
GEM_HOME_DIRS.each { String dirName ->
File newDir = new File(installDirectory, dirName)
logger.info("Attempting to create: ${newDir.absolutePath}")
if (!newDir.mkdirs()) {
logger.error("Failed to make ${newDir.absolutePath}, bailing on mkdirs()")
return false
}
}
return success
}
/**
* Primarily meant to be an internal method which will determine whether the
* given {@code java.io.File} is a valid gem archive or not. This includes looking
* inside it to see that it is a legitimate tar file
*
* @param gemFile Fully formed {@code java.io.File} object referencing a gem
* @return true if the file does in fact walk and talk like a gem
*/
boolean isValidGem(File gemFile) {
logger.info("Validating gem ${gemFile}")
/* If it doesn't end with gem, let's not even consider it a gem file */
if (!gemFile.absolutePath.endsWith('.gem')) {
return false
}
return false
}
String gemFullName(Gem gem) {
String fullName = "${gem.name}-${gem.version.version}"
if ((gem.platform) && (gem.platform != 'ruby')) {
fullName = "${fullName}-${gem.platform}"
}
return fullName
}
/** Cache the gem in GEM_HOME/cache */
protected void cacheGemInInstallDir(File installDir, File gem) {
File cacheDir = new File(installDir, 'cache')
Files.copy(gem.toPath(), (new File(cacheDir, gem.name)).toPath())
}
/** Extract the gemspec file from the {@code Gem} provided into the ${installDir}/specifications */
protected void extractSpecification(File installDir, Gem gem) {
String outputFileName = "${gemFullName(gem)}.gemspec"
File outputFile = new File(installDir, ['specifications', outputFileName].join(File.separator))
PrintWriter writer = new PrintWriter(outputFile.newOutputStream())
writer.write(gem.toRuby())
writer.flush()
}
/** Extract the data.tar.gz contents into gems/full-name/* */
protected void extractData(File installDir, GenericArchive dataTarGz, Gem gem) {
File outputDir = new File(installDir, 'gems')
outputDir.mkdirs()
dataTarGz.as(ExplodedExporter.class).exportExploded(outputDir, gemFullName(gem))
}
/** Extract the executables from the specified bindir */
protected void extractExecutables(File installDir, GenericArchive dataTarGz, Gem gem) {
/*
* default to 'bin' if the bindir isn't otherwise set, it's not clear whether
* it is always guaranteed to be set or not though
*/
String binDir = gem.bindir ?: 'bin'
File bin = new File(installDir, binDir)
List<String> execs = gem.executables.collect { String ex -> [binDir, ex].join(File.separator) }
bin.mkdirs()
dataTarGz.content.each { ArchivePath path, Node node ->
execs.each { String exec ->
if (path.get().matches(/.*${exec}/)) {
IOUtil.copy(node.asset.openStream(), (new File(installDir, exec)).newOutputStream())
}
}
}
}
}

View File

@ -0,0 +1,212 @@
package com.github.jrubygradle.jem.internal;
import org.jboss.shrinkwrap.api.ArchiveFormat;
import org.jboss.shrinkwrap.api.ArchivePath;
import org.jboss.shrinkwrap.api.GenericArchive;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.Node;
import org.jboss.shrinkwrap.api.exporter.ExplodedExporter;
import org.jboss.shrinkwrap.api.importer.TarImporter;
import org.jboss.shrinkwrap.impl.base.io.IOUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.github.jrubygradle.jem.Gem;
import com.github.jrubygradle.jem.GemInstaller.DuplicateBehavior;
import java.io.*;
import java.nio.file.Files;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.GZIPInputStream;
public class GemInstaller {
public static final String[] GEM_HOME_DIRS = {"bin", "build_info", "cache", "doc",
"extensions", "gems", "specifications"};
protected Logger logger = LoggerFactory.getLogger(GemInstaller.class);
protected File installDirectory;
protected List<File> gems;
public GemInstaller(String installDir, List<File> gems) {
this.installDirectory = new File(installDir);
this.gems = gems;
}
/** Install and overwrite anything that stands in the way */
public void install() {
install(DuplicateBehavior.OVERWRITE);
}
public void install(DuplicateBehavior onDuplicateBehavior) {
if (!mkdirs()) {
/* raise some exception? */
}
for (File gem : this.gems) {
installGem(installDirectory, gem, onDuplicateBehavior);
}
}
public boolean installGem(File installDir, File gem, DuplicateBehavior onDuplicate) {
/* TODO: isValidGem? */
try {
cacheGemInInstallDir(installDir, gem);
}
catch (IOException ex) {
logger.error("Failed to cache our gem in %s", installDir, ex);
return false;
}
Gem gemMetadata;
GenericArchive gemArchive = ShrinkWrap.create(TarImporter.class)
.importFrom(gem).as(GenericArchive.class);
Node metadata = gemArchive.get("metadata.gz");
GenericArchive dataArchive = gemArchive.getAsType(GenericArchive.class,
"data.tar.gz",
ArchiveFormat.TAR_GZ);
try {
gemMetadata = Gem.fromFile(new GZIPInputStream(metadata.getAsset().openStream()));
}
catch (IOException ex) {
logger.error("Failed to process the metadata", ex);
return false;
}
logger.info(String.format("We've processed metadata for %s at version %s",
gemMetadata.name, gemMetadata.version.version));
try {
extractSpecification(installDir, gemMetadata);
}
catch (Exception ex) {
logger.error(String.format("Could not extract the gem specification for %s into %s",
gemMetadata.name, installDir), ex);
}
extractData(installDir, dataArchive, gemMetadata);
try {
extractExecutables(installDir, dataArchive, gemMetadata);
}
catch (Exception ex) {
logger.error(String.format("Could not extract the gem executables for %s into %s",
gemMetadata.name, installDir), ex);
}
return true;
}
/**
* Create the requisite directories to map a GEM_HOME structure, namely:
* bin/
* build_info/
* cache/
* doc/
* extensions/
* gems/
* specifications/
*
* @return True if all directories were created successfully
*/
public boolean mkdirs() {
boolean success = true;
for (String dirName : GEM_HOME_DIRS) {
File newDir = new File(installDirectory, dirName);
logger.info(String.format("Attempting to create: %s", newDir.getAbsolutePath()));
if (!newDir.mkdirs()) {
logger.error(String.format("Failed to make %s, bailing on mkdirs()", newDir.getAbsolutePath()));
return false;
}
}
return success;
}
/**
* Primarily meant to be an internal method which will determine whether the
* given {@code java.io.File} is a valid gem archive or not. This includes looking
* inside it to see that it is a legitimate tar file
*
* @param gemFile Fully formed {@code java.io.File} object referencing a gem
* @return true if the file does in fact walk and talk like a gem
*/
public boolean isValidGem(File gemFile) {
logger.info(String.format("Validating gem %s", gemFile));
/* If it doesn"t end with gem, let"s not even consider it a gem file */
if (!gemFile.getAbsolutePath().endsWith(".gem")) {
return false;
}
return false;
}
public String gemFullName(Gem gem) {
String fullName = String.format("%s-%s", gem.name, gem.version.version);
if ((gem.platform instanceof String) && !(gem.platform.equals("ruby"))) {
fullName = String.format("%s-%s", fullName, gem.platform);
}
return fullName;
}
/** Cache the gem in GEM_HOME/cache */
protected void cacheGemInInstallDir(File installDir, File gem) throws IOException {
File cacheDir = new File(installDir, "cache");
Files.copy(gem.toPath(), (new File(cacheDir, gem.getName())).toPath());
}
/** Extract the gemspec file from the {@code Gem} provided into the ${installDir}/specifications */
protected void extractSpecification(File installDir, Gem gem) throws Exception {
String outputFileName = String.format("%s.gemspec", gemFullName(gem));
File specDir = new File(installDir, "specifications");
FileOutputStream output = new FileOutputStream(new File(specDir, outputFileName));
PrintWriter writer = new PrintWriter(output);
writer.write(gem.toRuby());
writer.flush();
}
/** Extract the data.tar.gz contents into gems/full-name/* */
protected void extractData(File installDir, GenericArchive dataTarGz, Gem gem) {
File outputDir = new File(installDir, "gems");
outputDir.mkdirs();
dataTarGz.as(ExplodedExporter.class).exportExploded(outputDir, gemFullName(gem));
}
/** Extract the executables from the specified bindir */
protected void extractExecutables(File installDir, GenericArchive dataTarGz, Gem gem) throws Exception {
/*
* default to "bin" if the bindir isn"t otherwise set, it"s not clear whether
* it is always guaranteed to be set or not though
*/
String binDir = gem.bindir;
if (!(binDir instanceof String)) {
binDir = "bin";
}
File bin = new File(installDir, binDir);
bin.mkdirs();
Pattern p = Pattern.compile("/" + binDir + "/(.*)?");
for (Map.Entry<ArchivePath, Node> entry : dataTarGz.getContent().entrySet()) {
ArchivePath path = entry.getKey();
Node node = entry.getValue();
logger.info(node.toString());
Matcher m = p.matcher(path.get());
if (m.matches()) {
File fullOutputPath = new File(bin, m.toMatchResult().group(1));
fullOutputPath.createNewFile();
IOUtil.copy(node.getAsset().openStream(),
new FileOutputStream(fullOutputPath));
}
}
}
}

View File

@ -70,7 +70,7 @@ class GemSpec extends Specification {
gem.version.version == '0.19.1'
and: "its properties should equal what's in the YAML"
gem.getProperty(property) == value
gem."${property}" == value
where:
property | value