diff --git a/java/java-tests/testSrc/com/intellij/util/io/java/ClassFileBuilderTest.kt b/java/java-tests/testSrc/com/intellij/util/io/java/ClassFileBuilderTest.kt new file mode 100644 index 000000000000..09e81ce6f4a8 --- /dev/null +++ b/java/java-tests/testSrc/com/intellij/util/io/java/ClassFileBuilderTest.kt @@ -0,0 +1,94 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.util.io.java + +import com.intellij.testFramework.UsefulTestCase.assertEmpty +import com.intellij.testFramework.UsefulTestCase.assertOneElement +import com.intellij.util.io.directoryContent +import junit.framework.TestCase.assertSame +import org.junit.Test +import java.io.File +import java.io.Serializable +import java.lang.reflect.Modifier +import java.util.* +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +/** + * @author nik + */ +class ClassFileBuilderTest { + @Test + fun `empty class`() { + val dir = directoryContent { + classFile("A") {} + }.generateInTempDir() + val aClass = loadClass("A", File(dir, "A.class")) + assertSame(Object::class.java, aClass.superclass) + assertEmpty(aClass.interfaces) + assertEmpty(aClass.declaredFields) + } + + @Test + fun `class with extends and implements`() { + val dir = directoryContent { + classFile("A") { + superclass = ArrayList::class.java.name + interfaces = listOf(Serializable::class.java.name) + } + }.generateInTempDir() + val aClass = loadClass("A", File(dir, "A.class")) + assertSame(ArrayList::class.java, aClass.superclass) + assertSame(Serializable::class.java, assertOneElement(aClass.interfaces)) + assertEmpty(aClass.declaredFields) + } + + @Test + fun `class with fields`() { + val dir = directoryContent { + classFile("A") { + field("foo", Int::class, AccessModifier.PUBLIC) + field("bar", Object::class.java.name) + } + }.generateInTempDir() + val aClass = loadClass("A", File(dir, "A.class")) + val foo = aClass.getDeclaredField("foo") + assertSame(Int::class.java, foo.type) + assertTrue(Modifier.isPublic(foo.modifiers)) + val bar = aClass.getDeclaredField("bar") + assertSame(Object::class.java, bar.type) + assertTrue(Modifier.isPrivate(bar.modifiers)) + } + + @Test + fun `class in non-default package`() { + val dir = directoryContent { + classFile("p.A") {} + }.generateInTempDir() + val aClass = loadClass("p.A", File(dir, "p/A.class")) + assertEquals("p.A", aClass.name) + } +} + +private fun loadClass(name: String, file: File): Class<*> { + return MyClassLoader(ClassFileBuilderTest::class.java.classLoader).doDefineClass(name, file.readBytes()) +} + +private class MyClassLoader(parent: ClassLoader) : ClassLoader(parent) { + fun doDefineClass(name: String, data: ByteArray): Class<*> { + return defineClass(name, data, 0, data.size) + } +} diff --git a/java/testFramework/src/com/intellij/util/io/java/ClassFileBuilder.kt b/java/testFramework/src/com/intellij/util/io/java/ClassFileBuilder.kt new file mode 100644 index 000000000000..0c0ee17e8215 --- /dev/null +++ b/java/testFramework/src/com/intellij/util/io/java/ClassFileBuilder.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.util.io.java + +import com.intellij.util.io.DirectoryContentBuilder +import com.intellij.util.io.java.impl.ClassFileBuilderImpl +import org.jetbrains.jps.model.java.LanguageLevel +import kotlin.reflect.KClass + +/** + * Produces class-file with qualified name [name] in a place specified by [content]. If the class is not from the default package, + * the produced file will be placed in a sub-directory according to its package. + * + * @author nik + */ +inline fun DirectoryContentBuilder.classFile(name: String, content: ClassFileBuilder.() -> Unit) { + val builder = ClassFileBuilderImpl(name) + builder.content() + builder.generate(this) +} + +abstract class ClassFileBuilder { + var javaVersion: LanguageLevel = LanguageLevel.JDK_1_8 + var superclass: String = "java.lang.Object" + var interfaces: List = emptyList() + var access: AccessModifier = AccessModifier.PUBLIC + + abstract fun field(name: String, type: KClass<*>, access: AccessModifier = AccessModifier.PRIVATE) + + /** + * Adds a field which type is a class with qualified name [type] + */ + abstract fun field(name: String, type: String, access: AccessModifier = AccessModifier.PRIVATE) +} + +enum class AccessModifier { PRIVATE, PUBLIC, PROTECTED, PACKAGE_LOCAL } \ No newline at end of file diff --git a/java/testFramework/src/com/intellij/util/io/java/impl/ClassFileBuilderImpl.kt b/java/testFramework/src/com/intellij/util/io/java/impl/ClassFileBuilderImpl.kt new file mode 100644 index 000000000000..3e7cb4057b03 --- /dev/null +++ b/java/testFramework/src/com/intellij/util/io/java/impl/ClassFileBuilderImpl.kt @@ -0,0 +1,90 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.util.io.java.impl + +import com.intellij.openapi.util.text.StringUtil +import com.intellij.util.PathUtil +import com.intellij.util.io.DirectoryContentBuilder +import com.intellij.util.io.java.AccessModifier +import com.intellij.util.io.java.ClassFileBuilder +import org.jetbrains.jps.model.java.LanguageLevel +import org.jetbrains.org.objectweb.asm.ClassWriter +import org.jetbrains.org.objectweb.asm.Opcodes +import org.jetbrains.org.objectweb.asm.Type +import kotlin.reflect.KClass + +/** + * @author nik + */ +class ClassFileBuilderImpl(private val name: String) : ClassFileBuilder() { + private val writer = ClassWriter(ClassWriter.COMPUTE_FRAMES) + + override fun field(name: String, type: String, access: AccessModifier) { + addField(name, "L" + toJvmName(type) + ";", access) + } + + override fun field(name: String, type: KClass<*>, access: AccessModifier) { + addField(name, Type.getDescriptor(type.java), access) + } + + private fun addField(name: String, typeDescriptor: String, access: AccessModifier) { + writer.visitField(access.toAsmCode(), name, typeDescriptor, null, null).visitEnd() + } + + fun generate(targetRoot: DirectoryContentBuilder) { + writer.visit(javaVersion.toAsmCode(), access.toAsmCode(), toJvmName(name), null, + superclass.replace('.', '/'), + interfaces.map(::toJvmName).toTypedArray()) + writer.visitEnd() + + targetRoot.directories(StringUtil.getPackageName(name).replace('.', '/')) { + file("${StringUtil.getShortName(name)}.class", writer.toByteArray()) + } + } + + private fun DirectoryContentBuilder.directories(relativePath: String, content: DirectoryContentBuilder.() -> Unit) { + if (relativePath.isEmpty()) { + content() + } + else { + directories(PathUtil.getParentPath(relativePath)) { + dir(PathUtil.getFileName(relativePath)) { + content() + } + } + } + } +} + +private fun toJvmName(className: String) = className.replace('.', '/') + +private fun LanguageLevel.toAsmCode() = when (this) { + LanguageLevel.JDK_1_3 -> Opcodes.V1_3 + LanguageLevel.JDK_1_4 -> Opcodes.V1_4 + LanguageLevel.JDK_1_5 -> Opcodes.V1_5 + LanguageLevel.JDK_1_6 -> Opcodes.V1_6 + LanguageLevel.JDK_1_7 -> Opcodes.V1_7 + LanguageLevel.JDK_1_8 -> Opcodes.V1_8 + LanguageLevel.JDK_1_9 -> Opcodes.V1_9 + LanguageLevel.JDK_X -> throw UnsupportedOperationException("Java 10 isn't supported yet") +} + +private fun AccessModifier.toAsmCode() = when (this) { + AccessModifier.PROTECTED -> Opcodes.ACC_PROTECTED + AccessModifier.PRIVATE -> Opcodes.ACC_PRIVATE + AccessModifier.PUBLIC -> Opcodes.ACC_PUBLIC + AccessModifier.PACKAGE_LOCAL -> 0 +} \ No newline at end of file diff --git a/jps/jps-builders/jps-builders.iml b/jps/jps-builders/jps-builders.iml index 1ff957a1691d..e19939e9aa1c 100644 --- a/jps/jps-builders/jps-builders.iml +++ b/jps/jps-builders/jps-builders.iml @@ -24,7 +24,7 @@ - + diff --git a/jps/jps-builders/testSrc/org/jetbrains/jps/builders/java/MockPackageFacadeBuilder.kt b/jps/jps-builders/testSrc/org/jetbrains/jps/builders/java/MockPackageFacadeBuilder.kt index c9bfe3de75b8..60c03b83cae0 100644 --- a/jps/jps-builders/testSrc/org/jetbrains/jps/builders/java/MockPackageFacadeBuilder.kt +++ b/jps/jps-builders/testSrc/org/jetbrains/jps/builders/java/MockPackageFacadeBuilder.kt @@ -20,6 +20,10 @@ import com.intellij.openapi.util.io.FileUtilRt import com.intellij.openapi.util.text.StringUtil import com.intellij.util.containers.MultiMap import com.intellij.util.io.EnumeratorStringDescriptor +import com.intellij.util.io.directoryContent +import com.intellij.util.io.java.AccessModifier +import com.intellij.util.io.java.ClassFileBuilder +import com.intellij.util.io.java.classFile import gnu.trove.THashSet import org.jetbrains.jps.ModuleChunk import org.jetbrains.jps.builders.DirtyFilesHolder @@ -30,9 +34,8 @@ import org.jetbrains.jps.incremental.ModuleBuildTarget import org.jetbrains.jps.incremental.ModuleLevelBuilder import org.jetbrains.jps.incremental.storage.AbstractStateStorage import org.jetbrains.jps.incremental.storage.PathStringDescriptor +import org.jetbrains.jps.model.java.LanguageLevel import org.jetbrains.org.objectweb.asm.ClassReader -import org.jetbrains.org.objectweb.asm.ClassWriter -import org.jetbrains.org.objectweb.asm.Opcodes import java.io.File import java.util.* import java.util.regex.Pattern @@ -75,19 +78,19 @@ class MockPackageFacadeGenerator : ModuleLevelBuilder(BuilderCategory.SOURCE_PRO val callback = JavaBuilderUtil.getDependenciesRegistrar(context) fun generateClass(packageName: String, className: String, target: ModuleBuildTarget, sources: Collection, - allSources: Collection, generate: (ClassWriter.() -> Unit)? = null) { - val writer = ClassWriter(ClassWriter.COMPUTE_FRAMES) - val fullClassName = StringUtil.getQualifiedName(packageName, className).replace('.', '/') - writer.visit(Opcodes.V1_6, Opcodes.ACC_PUBLIC, fullClassName, null, "java/lang/Object", null) - if (generate != null) { - writer.generate() - } - writer.visitEnd() - val outputFile = File(target.outputDir, "$fullClassName.class") - val classBytes = writer.toByteArray() - FileUtil.writeToFile(outputFile, classBytes) + allSources: Collection, content: (ClassFileBuilder.() -> Unit)? = null) { + val fullClassName = StringUtil.getQualifiedName(packageName, className) + directoryContent { + classFile(fullClassName) { + javaVersion = LanguageLevel.JDK_1_6 + if (content != null) { + content() + } + } + }.generate(target.outputDir!!) + val outputFile = File(target.outputDir, "${fullClassName.replace('.', '/')}.class") outputConsumer.registerOutputFile(target, outputFile, sources) - callback.associate(fullClassName.replace('/', '.'), allSources, ClassReader(classBytes)) + callback.associate(fullClassName, allSources, ClassReader(outputFile.readBytes())) } for (target in chunk.targets) { @@ -130,8 +133,8 @@ class MockPackageFacadeGenerator : ModuleLevelBuilder(BuilderCategory.SOURCE_PRO generateClass(packageName, "PackageFacade", target, dirtySource, allSources) { for (fileName in classNames) { - val fieldClass = StringUtil.getQualifiedName(packageName, fileName).replace('.', '/') - visitField(Opcodes.ACC_PUBLIC, StringUtil.decapitalize(fileName), "L$fieldClass;", null, null).visitEnd() + val fieldClass = StringUtil.getQualifiedName(packageName, fileName) + field(StringUtil.decapitalize(fileName), fieldClass, AccessModifier.PUBLIC) } } for (source in dirtySource) { diff --git a/platform/testFramework/src/com/intellij/util/io/DirectoryContentSpec.kt b/platform/testFramework/src/com/intellij/util/io/DirectoryContentSpec.kt index 0e6852373b08..418d65e1015a 100644 --- a/platform/testFramework/src/com/intellij/util/io/DirectoryContentSpec.kt +++ b/platform/testFramework/src/com/intellij/util/io/DirectoryContentSpec.kt @@ -42,6 +42,8 @@ abstract class DirectoryContentBuilder { abstract fun file(name: String, text: String) + abstract fun file(name: String, content: ByteArray) + inline fun dir(name: String, content: DirectoryContentBuilder.() -> Unit) { val dirDefinition = DirectorySpec() DirectoryContentBuilderImpl(dirDefinition).content() diff --git a/platform/testFramework/src/com/intellij/util/io/impl/DirectoryContentSpecImpl.kt b/platform/testFramework/src/com/intellij/util/io/impl/DirectoryContentSpecImpl.kt index 778d2c507cd3..e0c5b30c8ac3 100644 --- a/platform/testFramework/src/com/intellij/util/io/impl/DirectoryContentSpecImpl.kt +++ b/platform/testFramework/src/com/intellij/util/io/impl/DirectoryContentSpecImpl.kt @@ -104,7 +104,11 @@ class DirectoryContentBuilderImpl(val result: DirectorySpecBase) : DirectoryCont } override fun file(name: String, text: String) { - addChild(name, FileSpec(text.toByteArray())) + file(name, text.toByteArray()) + } + + override fun file(name: String, content: ByteArray) { + addChild(name, FileSpec(content)) } }