Java tutorial
package util; import javassist.bytecode.AccessFlag; import javassist.bytecode.AnnotationsAttribute; import javassist.bytecode.ClassFile; import javassist.bytecode.MethodInfo; import javassist.bytecode.annotation.Annotation; import javassist.bytecode.annotation.AnnotationMemberValue; import javassist.bytecode.annotation.ArrayMemberValue; import javassist.bytecode.annotation.BooleanMemberValue; import javassist.bytecode.annotation.IntegerMemberValue; import javassist.bytecode.annotation.MemberValue; import javassist.bytecode.annotation.StringMemberValue; import models.ModuleVersion; import models.User; import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.io.FileUtils; import java.io.DataInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; public class ModuleChecker { public static List<Diagnostic> collectModulesAndDiagnostics(List<File> uploadedFiles, List<Module> modules, File uploadsDir, User user) { List<Diagnostic> diagnostics = new ArrayList<Diagnostic>(); Map<String, File> fileByPath = new HashMap<String, File>(); Set<String> alreadyTreatedArchives = new HashSet<String>(); if (uploadedFiles.isEmpty() && modules.isEmpty()) { diagnostics.add(new Diagnostic("empty", "Empty upload")); } else { for (File f : uploadedFiles) { String name = f.getName(); String path = getPathRelativeTo(uploadsDir, f); fileByPath.put(path, f); if (name.endsWith(".car") || name.endsWith(".jar") // don't even try to match js files if they are in module-doc folders || (name.endsWith(".js") && !path.contains("module-doc"))) { String pathBeforeDot = path.substring(0, path.lastIndexOf('.')); // don't add a module for both the car, jar and js file if (!alreadyTreatedArchives.add(pathBeforeDot)) { continue; } int sep = name.indexOf('-'); if (sep == -1) { if (name.equals("default.car") || name.equals("default.js") || name.equals("default.jar")) { diagnostics.add(new Diagnostic("error", "Default module not allowed.")); } else { diagnostics.add(new Diagnostic("error", "Module artifact has no version: " + name)); } continue; } int dot = name.lastIndexOf('.'); String module = name.substring(0, sep); if (module.isEmpty()) { diagnostics.add(new Diagnostic("error", "Empty module name not allowed: " + name)); continue; } if (module.equals("default")) { diagnostics.add(new Diagnostic("error", "Default module not allowed: " + name)); continue; } String version = name.substring(sep + 1, dot); if (version.isEmpty()) { diagnostics.add(new Diagnostic("error", "Empty version number not allowed: " + name)); continue; } modules.add(new Module(module, version, path.substring(0, path.length() - name.length()))); } } for (Module m : modules) { checkModule(uploadsDir, fileByPath, m, user, modules); } for (Module m : modules) { checkModuleDependencyVersions(m); } if (modules.isEmpty()) { diagnostics.add(new Diagnostic("error", "No module defined")); } } if (!fileByPath.isEmpty()) { for (String key : fileByPath.keySet()) { diagnostics.add(new Diagnostic("error", "Unknown file: " + key, key.substring(1))); } } return diagnostics; } private static void checkModuleDependencyVersions(Module m) { // the given module has to be a JVM car module for (Import dep : m.dependencies) { if (dep.existingDependency != null) { // only check cars for binary version if (dep.existingDependency.isCarPresent && (m.ceylonMajor != dep.existingDependency.ceylonMajor || m.ceylonMinor != dep.existingDependency.ceylonMinor)) { m.diagnostics.add(new Diagnostic("error", "Module depends on an incompatible Ceylon version: " + dep.name + "/" + dep.version)); } if (!m.hasCar && !m.hasJar) { m.diagnostics.add(new Diagnostic("error", "Module depends on a non-JVM module: " + dep.name + "/" + dep.version)); } } if (dep.newDependency != null) { // only check cars for binary version if (dep.newDependency.hasCar && (m.ceylonMajor != dep.newDependency.ceylonMajor || m.ceylonMinor != dep.newDependency.ceylonMinor)) { m.diagnostics.add(new Diagnostic("error", "Module depends on an incompatible Ceylon version: " + dep.name + "/" + dep.version)); } if (!m.hasCar && !m.hasJar) { m.diagnostics.add(new Diagnostic("error", "Module depends on a non-JVM module: " + dep.name + "/" + dep.version)); } } } } public static void checkModule(File uploadsDir, Map<String, File> fileByPath, Module m, User user, List<Module> modules) { // check the path first (we always start and end with a separator) String expectedPath = File.separatorChar + m.name.replace('.', File.separatorChar) + File.separatorChar + m.version + File.separatorChar; if (!expectedPath.equals(m.path)) { m.diagnostics.add(new Diagnostic("error", "Module is not in the right path: " + m.path + " (expecting " + expectedPath + ")")); } models.Project project = models.Project.findOwner(m.name); if (project == null) { // nobody owns it, but perhaps we already have a claim for it project = models.Project.findForOwner(m.name, user); m.diagnostics.add(new Diagnostic("error", "You do not own this module", project)); } else { // do we own it? if (project.owner == user) { m.diagnostics.add(new Diagnostic("success", "You own this module")); } else { // we don't own it but we may be admin models.Module publishedModule = models.Module.findByName(m.name); if (publishedModule == null || !publishedModule.canEdit(user)) { // we're not the owner, and not admin, but perhaps we already have a claim for it project = models.Project.findForOwner(m.name, user); m.diagnostics.add(new Diagnostic("error", "You do not own this module", project)); } else { m.diagnostics.add(new Diagnostic("success", "You are admin on this module")); } } } models.ModuleVersion publishedModule = models.ModuleVersion.findByVersion(m.name, m.version); if (publishedModule != null) { m.diagnostics.add(new Diagnostic("error", "Module already published")); } // jar check first String jarName = m.name + "-" + m.version + ".jar"; String jarPath = m.path + jarName; m.hasJar = fileByPath.containsKey(jarPath); if (m.hasJar) { fileByPath.remove(jarPath); // jar String checksumPath = m.path + jarName + ".sha1"; m.hasJarChecksum = fileByPath.containsKey(checksumPath); if (m.hasJarChecksum) { fileByPath.remove(checksumPath); // jar checksum File jarFile = new File(uploadsDir, jarPath); m.jarChecksumValid = checkChecksum(uploadsDir, checksumPath, jarFile); if (m.jarChecksumValid) { m.diagnostics.add(new Diagnostic("success", "Jar checksum valid")); } else { m.diagnostics.add(new Diagnostic("error", "Invalid Jar checksum")); } } else { m.diagnostics.add(new Diagnostic("error", "Missing Jar checksum")); } } // car check String carName = m.name + "-" + m.version + ".car"; String carPath = m.path + carName; m.hasCar = fileByPath.containsKey(carPath); if (m.hasCar) { fileByPath.remove(carPath); // car if (!m.hasJar) { m.diagnostics.add(new Diagnostic("success", "Has car: " + carName)); } else { m.diagnostics.add( new Diagnostic("error", "If a module contains a jar it cannot contain other archives")); } String checksumPath = m.path + carName + ".sha1"; m.hasChecksum = fileByPath.containsKey(checksumPath); if (m.hasChecksum) { fileByPath.remove(checksumPath); // car checksum File carFile = new File(uploadsDir, carPath); m.checksumValid = checkChecksum(uploadsDir, checksumPath, carFile); if (m.checksumValid) { if (!m.hasJar) { m.diagnostics.add(new Diagnostic("success", "Checksum valid")); } } else { m.diagnostics.add(new Diagnostic("error", "Invalid checksum")); } } else if (!m.hasJar) { m.diagnostics.add(new Diagnostic("error", "Missing checksum")); } loadModuleInfo(uploadsDir, m.path + carName, m, modules); checkIsRunnable(uploadsDir, m.path + carName, m, modules); } else if (!m.hasJar) { m.diagnostics.add(new Diagnostic("warning", "Missing car archive")); } // js check String jsName = m.name + "-" + m.version + ".js"; String jsPath = m.path + jsName; m.hasJs = fileByPath.containsKey(jsPath); if (m.hasJs) { fileByPath.remove(jsPath); // js if (!m.hasJar) { m.diagnostics.add(new Diagnostic("success", "Has js: " + jsName)); } else { m.diagnostics.add( new Diagnostic("error", "If a module contains a jar it cannot contain other archives")); } } else if (!m.hasJar) { m.diagnostics.add(new Diagnostic("warning", "Missing js archive")); } // must have at least js or car or jar if (!m.hasCar && !m.hasJs && !m.hasJar) { m.diagnostics.add(new Diagnostic("error", "Module must have at least a car, jar or js archive")); } // src check String srcName = m.name + "-" + m.version + ".src"; File srcFile = new File(uploadsDir, m.path + srcName); if (srcFile.exists()) { m.hasSource = true; if (!m.hasJar) { m.diagnostics.add(new Diagnostic("success", "Has source")); } else { m.diagnostics.add( new Diagnostic("error", "If a module contains a jar it cannot contain other archives")); } fileByPath.remove(m.path + srcName); // source archive String srcChecksumPath = m.path + srcName + ".sha1"; m.hasSourceChecksum = fileByPath.containsKey(srcChecksumPath); if (m.hasSourceChecksum) { fileByPath.remove(srcChecksumPath); // car checksum m.sourceChecksumValid = checkChecksum(uploadsDir, srcChecksumPath, srcFile); if (m.sourceChecksumValid) { if (!m.hasJar) { m.diagnostics.add(new Diagnostic("success", "Source checksum valid")); } } else { m.diagnostics.add(new Diagnostic("error", "Invalid source checksum")); } } else if (!m.hasJar) { m.diagnostics.add(new Diagnostic("error", "Missing source checksum")); } } else if (!m.hasJar) { m.diagnostics.add(new Diagnostic("warning", "Missing source archive")); } // doc check String docName = m.path + "module-doc" + File.separator + "index.html"; File docFile = new File(uploadsDir, docName); if (docFile.exists()) { m.hasDocs = true; if (!m.hasJar) { m.diagnostics.add(new Diagnostic("success", "Has docs")); } else { m.diagnostics.add( new Diagnostic("error", "If a module contains a jar it cannot contain other archives")); } String prefix = m.path + "module-doc" + File.separator; Iterator<String> iterator = fileByPath.keySet().iterator(); while (iterator.hasNext()) { String key = iterator.next(); // count all the doc files if (key.startsWith(prefix)) { iterator.remove(); } } } else if (!m.hasJar) { m.diagnostics.add(new Diagnostic("warning", "Missing docs")); } // second jar check // if the jar is alone it's good. Otherwise an error was already added if (m.hasJar && !m.hasJs && !m.hasCar && !m.hasChecksum && !m.hasDocs && !m.hasSource && !m.hasSourceChecksum) { m.diagnostics.add(new Diagnostic("success", "Has jar: " + jarName)); } } private static void loadModuleInfo(File uploadsDir, String carName, Module m, List<Module> modules) { try { ZipFile car = new ZipFile(new File(uploadsDir, carName)); try { // try first with M4 format ZipEntry moduleEntry = car.getEntry(m.name.replace('.', '/') + "/module_.class"); if (moduleEntry == null) { // try with pre-M4 format moduleEntry = car.getEntry(m.name.replace('.', '/') + "/module.class"); if (moduleEntry == null) { m.diagnostics.add(new Diagnostic("error", ".car file does not contain module information")); return; } } m.diagnostics.add(new Diagnostic("success", ".car file contains module descriptor")); DataInputStream inputStream = new DataInputStream(car.getInputStream(moduleEntry)); ClassFile classFile = new ClassFile(inputStream); inputStream.close(); AnnotationsAttribute visible = (AnnotationsAttribute) classFile .getAttribute(AnnotationsAttribute.visibleTag); // ceylon version info Annotation ceylonAnnotation = visible .getAnnotation("com.redhat.ceylon.compiler.java.metadata.Ceylon"); if (ceylonAnnotation == null) { m.diagnostics.add( new Diagnostic("error", ".car does not contain @Ceylon annotation on module.class")); return; } m.diagnostics.add(new Diagnostic("success", ".car file module descriptor has @Ceylon annotation")); Integer major = getOptionalInt(ceylonAnnotation, "major", 0, m); Integer minor = getOptionalInt(ceylonAnnotation, "minor", 0, m); if (major == null || minor == null) { return; } m.ceylonMajor = major; m.ceylonMinor = minor; // module info Annotation moduleAnnotation = visible .getAnnotation("com.redhat.ceylon.compiler.java.metadata.Module"); if (moduleAnnotation == null) { m.diagnostics.add( new Diagnostic("error", ".car does not contain @Module annotation on module.class")); return; } m.diagnostics.add(new Diagnostic("success", ".car file module descriptor has @Module annotation")); String name = getString(moduleAnnotation, "name", m, false); String version = getString(moduleAnnotation, "version", m, false); if (name == null || version == null) { return; } if (!name.equals(m.name)) { m.diagnostics.add(new Diagnostic("error", ".car file contains unexpected module: " + name)); return; } if (!version.equals(m.version)) { m.diagnostics.add( new Diagnostic("error", ".car file contains unexpected module version: " + version)); return; } m.diagnostics.add(new Diagnostic("success", ".car file module descriptor has valid name/version")); // metadata m.license = getString(moduleAnnotation, "license", m, true); if (m.license != null) { m.diagnostics.add(new Diagnostic("success", "License: " + m.license)); } m.doc = getString(moduleAnnotation, "doc", m, true); if (m.doc != null) { m.diagnostics.add(new Diagnostic("success", "Has doc string")); } m.authors = getStringArray(moduleAnnotation, "by", m, true); if (m.authors != null && m.authors.length != 0) { m.diagnostics.add(new Diagnostic("success", "Has authors")); } // dependencies MemberValue dependencies = moduleAnnotation.getMemberValue("dependencies"); if (dependencies == null) { m.diagnostics.add(new Diagnostic("success", "Module has no dependencies")); return; // we're good } if (!(dependencies instanceof ArrayMemberValue)) { m.diagnostics.add( new Diagnostic("error", "Invalid 'dependencies' annotation value (expecting array)")); return; } MemberValue[] dependencyValues = ((ArrayMemberValue) dependencies).getValue(); if (dependencyValues.length == 0) { m.diagnostics.add(new Diagnostic("success", "Module has no dependencies")); return; // we're good } for (MemberValue dependencyValue : dependencyValues) { checkDependency(dependencyValue, m, modules); } } finally { car.close(); } } catch (IOException e) { e.printStackTrace(); m.diagnostics.add(new Diagnostic("error", "Invalid car file: " + e.getMessage())); } } private static void checkDependency(MemberValue dependencyValue, Module m, List<Module> modules) { if (!(dependencyValue instanceof AnnotationMemberValue)) { m.diagnostics.add(new Diagnostic("error", "Invalid dependency value (expecting annotation)")); return; } Annotation dependency = ((AnnotationMemberValue) dependencyValue).getValue(); if (!dependency.getTypeName().equals("com.redhat.ceylon.compiler.java.metadata.Import")) { m.diagnostics.add(new Diagnostic("error", "Invalid 'dependency' value (expecting @Import)")); return; } String name = getString(dependency, "name", m, false); String version = getString(dependency, "version", m, false); if (name == null || version == null) { return; } if (name.isEmpty()) { m.diagnostics.add(new Diagnostic("error", "Invalid empty dependency name")); return; } if (version.isEmpty()) { m.diagnostics.add(new Diagnostic("error", "Invalid empty dependency version")); return; } MemberValue optionalValue = dependency.getMemberValue("optional"); if (optionalValue != null) { if (!(optionalValue instanceof BooleanMemberValue)) { m.diagnostics.add(new Diagnostic("error", "Invalid @Import 'optional' value (expecting boolean)")); return; } boolean optional = ((BooleanMemberValue) optionalValue).getValue(); if (optional) { m.diagnostics.add(new Diagnostic("success", "Dependency " + name + "/" + version + " is optional")); return; } } // must make sure it exists checkDependencyExists(name, version, m, modules); } private static void checkDependencyExists(String name, String version, Module m, List<Module> modules) { // try to find it in the list of uploaded modules for (Module module : modules) { if (module.name.equals(name) && module.version.equals(version)) { m.diagnostics.add( new Diagnostic("success", "Dependency " + name + "/" + version + " is to be uploaded")); m.addDependency(name, version, module); return; } } // try to find it in the repo models.ModuleVersion dep = models.ModuleVersion.findByVersion(name, version); if (dep == null) { m.diagnostics.add(new Diagnostic("error", "Dependency " + name + "/" + version + " cannot be found in upload or repo")); } else { m.addDependency(name, version, dep); m.diagnostics.add(new Diagnostic("success", "Dependency " + name + "/" + version + " present in repo")); } } private static void checkIsRunnable(File uploadsDir, String carName, Module m, List<Module> modules) { try { ZipFile car = new ZipFile(new File(uploadsDir, carName)); try { ZipEntry moduleEntry = car.getEntry(m.name.replace('.', '/') + "/run.class"); if (moduleEntry == null) { return; } DataInputStream inputStream = new DataInputStream(car.getInputStream(moduleEntry)); ClassFile classFile = new ClassFile(inputStream); inputStream.close(); AnnotationsAttribute visible = (AnnotationsAttribute) classFile .getAttribute(AnnotationsAttribute.visibleTag); m.isRunnable = false; Annotation methodAnnotation = visible .getAnnotation("com.redhat.ceylon.compiler.java.metadata.Method"); if (methodAnnotation != null) { MethodInfo runMethodInfo = (MethodInfo) classFile.getMethod("run"); MethodInfo mainMethodInfo = (MethodInfo) classFile.getMethod("main"); if (runMethodInfo != null && mainMethodInfo != null && mainMethodInfo.toString().endsWith("V")) { m.isRunnable = AccessFlag.isPublic(mainMethodInfo.getAccessFlags()); } } } finally { car.close(); } } catch (IOException e) { e.printStackTrace(); //To change body of catch statement use File | Settings | File Templates. } if (m.isRunnable) { m.diagnostics.add(new Diagnostic("success", "Module is runnable.")); } } private static String getString(Annotation annotation, String field, Module m, boolean missingOK) { MemberValue value = annotation.getMemberValue(field); if (value == null) { if (!missingOK) { m.diagnostics.add(new Diagnostic("error", "Missing '" + field + "' annotation value")); } return null; } if (!(value instanceof StringMemberValue)) { m.diagnostics .add(new Diagnostic("error", "Invalid '" + field + "' annotation value (expecting String)")); return null; } return ((StringMemberValue) value).getValue(); } private static String[] getStringArray(Annotation annotation, String field, Module m, boolean missingOK) { MemberValue value = annotation.getMemberValue(field); if (value == null) { if (!missingOK) { m.diagnostics.add(new Diagnostic("error", "Missing '" + field + "' annotation value")); } return null; } if (!(value instanceof ArrayMemberValue)) { m.diagnostics .add(new Diagnostic("error", "Invalid '" + field + "' annotation value (expecting String[])")); return null; } MemberValue[] arrayValue = ((ArrayMemberValue) value).getValue(); if (arrayValue == null) { if (!missingOK) { m.diagnostics.add(new Diagnostic("error", "Missing '" + field + "' annotation value")); } return null; } String[] ret = new String[arrayValue.length]; int i = 0; for (MemberValue val : arrayValue) { if (!(val instanceof StringMemberValue)) { m.diagnostics.add( new Diagnostic("error", "Invalid '" + field + "' annotation value (expecting String[])")); return null; } ret[i++] = ((StringMemberValue) val).getValue(); } return ret; } private static Integer getOptionalInt(Annotation annotation, String field, int defaultValue, Module m) { MemberValue value = annotation.getMemberValue(field); if (value == null) { return defaultValue; } if (!(value instanceof IntegerMemberValue)) { m.diagnostics.add(new Diagnostic("error", "Invalid '" + field + "' annotation value (expecting int)")); return null; } return ((IntegerMemberValue) value).getValue(); } private static boolean checkChecksum(File uploadsDir, String checksumPath, File checkedFile) { File checksumFile = new File(uploadsDir, checksumPath); try { String checksum = FileUtils.readFileToString(checksumFile); String realChecksum = sha1(checkedFile); return realChecksum.equals(checksum); } catch (Exception x) { return false; } } public static String sha1(File file) throws IOException { InputStream is = new FileInputStream(file); try { String realChecksum = DigestUtils.shaHex(is); return realChecksum; } finally { is.close(); } } private static String getPathRelativeTo(File uploadsDir, File f) { try { String prefix = uploadsDir.getCanonicalPath(); String path = f.getCanonicalPath(); if (path.startsWith(prefix)) { return path.substring(prefix.length()); } } catch (IOException x) { throw new RuntimeException(x); } throw new RuntimeException("Invalid path: " + f.getPath()); } public static class Diagnostic { public String type; public String message; public String unknownPath; public models.Project project; public boolean projectClaim; Diagnostic(String type, String message) { this.type = type; this.message = message; } public Diagnostic(String type, String message, String unknownPath) { this(type, message); this.unknownPath = unknownPath; } public Diagnostic(String type, String message, models.Project project) { this(type, message); this.project = project; this.projectClaim = true; } } public static class Import { public String name; public String version; public boolean export; public boolean optional; public ModuleVersion existingDependency; public Module newDependency; Import(String name, String version, ModuleVersion dep) { this.name = name; this.version = version; this.existingDependency = dep; } Import(String name, String version, Module dep) { this.name = name; this.version = version; this.newDependency = dep; } } public static class Module { public String[] authors; public String doc; public String license; public boolean jarChecksumValid; public boolean hasJarChecksum; public boolean hasJar; public List<Diagnostic> diagnostics = new ArrayList<Diagnostic>(); public String name; public String version; public String path; public boolean hasCar; public boolean hasJs; public boolean hasChecksum; public boolean checksumValid; public boolean hasSource; public boolean hasSourceChecksum; public boolean sourceChecksumValid; public boolean hasDocs; public int ceylonMajor; public int ceylonMinor; public List<Import> dependencies = new LinkedList<Import>(); public boolean isRunnable; Module(String name, String version, String path) { this.name = name; this.version = version; this.path = path; } public void addDependency(String name, String version, ModuleVersion dep) { dependencies.add(new Import(name, version, dep)); } public void addDependency(String name, String version, Module dep) { dependencies.add(new Import(name, version, dep)); } public String getType() { String worse = "success"; for (Diagnostic d : diagnostics) { if (d.type.equals("error")) { return d.type; } if (d.type.equals("warning")) { worse = d.type; } } return worse; } public String getDocPath() { return path.substring(1) + "module-doc" + "/" + "index.html"; } } public static class UploadInfo { public List<Diagnostic> diagnostics; public List<Module> modules; public models.Upload upload; public String status; public UploadInfo(models.Upload upload, List<Module> modules, List<Diagnostic> diagnostics) { this.upload = upload; this.modules = modules; this.diagnostics = diagnostics; setStatus(); } private void setStatus() { status = "success"; for (Diagnostic d : diagnostics) { if (d.type.equals("error")) { status = d.type; return; } if (d.type.equals("warning")) { status = d.type; return; } if (d.type.equals("empty")) { status = d.type; } } for (Module m : modules) { String type = m.getType(); if (type.equals("error")) { status = type; return; } if (type.equals("warning")) { status = type; } } } public boolean isPublishable() { return status.equals("success") || status.equals("warning"); } } }