org.robovm.compiler.plugin.objc.InterfaceBuilderClassesPlugin Maven / Gradle / Ivy
The newest version!
/*
* Copyright (C) 2015 RoboVM AB
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
package org.robovm.compiler.plugin.objc;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.filefilter.IOFileFilter;
import org.apache.commons.io.filefilter.SuffixFileFilter;
import org.apache.commons.lang3.StringUtils;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.Opcodes;
import org.robovm.compiler.Linker;
import org.robovm.compiler.clazz.Clazz;
import org.robovm.compiler.config.Config;
import org.robovm.compiler.config.Config.Builder;
import org.robovm.compiler.config.Resource;
import org.robovm.compiler.config.Resource.Walker;
import org.robovm.compiler.log.Logger;
import org.robovm.compiler.plugin.AbstractCompilerPlugin;
import org.robovm.compiler.plugin.CompilerPlugin;
/**
* {@link CompilerPlugin} which forces view controllers and views referenced by
* Storyboards and XIB files in resource folders to be linked into the
* executable. Also embeds a list of such classes into the executable which can
* later be pre-registered in {@code UIApplication.main(...)} when the app is
* launched.
*/
public class InterfaceBuilderClassesPlugin extends AbstractCompilerPlugin {
private static final String[] JAR_ZIP_EXTENSIONS = new String[] { "jar", "zip" };
private static final String CLASS_EXTENSION = "class";
private static final String CUSTOM_CLASS = "Lorg/robovm/objc/annotation/CustomClass;";
private static final String NATIVE_CLASS = "Lorg/robovm/objc/annotation/NativeClass;";
private static final Pattern IB_CLASS_PATTERN = Pattern.compile(".*(ViewController|View)");
/**
* Ignore package names like this when searching classpath folders for
* classes in {@link #buildClassToUrlMap(List)}.
*/
private static final Pattern EXCLUDED_PACKAGES = Pattern.compile("org\\.robovm\\.apple\\..*");
private static final String RUNTIME_DATA_ID = "org.robovm.apple.uikit.UIApplication.preloadClasses";
private Logger logger;
private List preloadClasses;
@Override
public void beforeConfig(Builder builder, final Config config) throws IOException {
logger = config.getLogger();
preloadClasses = new ArrayList<>();
List customClasses = findCustomClassesInIBFiles(config);
if (customClasses.isEmpty()) {
// Nothing needs to be done by this plugin.
return;
}
// We now have a list of ObjC class names. We need to map those to Java
// class names. ObjC class names are generated in two ways:
// 1. Auto-generated by taking the fully-qualified class name of the
// Java class, replacing '.' by '_' and prepending 'j_'.
// 2. Using a @CustomClass annotation on the Java class.
//
// Reversing 1 is easy. We just iterate the classes in the configured
// classpath, apply the same rule to each class name and look for a
// match in customClasses.
//
// To reverse 2 we need to parse class files which is more time
// consuming. We don't want to parse every class in the classpath so
// first we look for simple class names which match the ObjC class names
// and look for a @CustomClass annotation on those. Then we look for
// classes with names that suggest they are view controllers/views.
// Finally we have to parse every class in the classpath.
// Build a map of class names to URLs.
List classpath = new ArrayList<>();
classpath.addAll(config.getBootclasspath());
classpath.addAll(config.getClasspath());
Map classToUrlMap = buildClassToUrlMap(classpath);
// Build a map of auto-generated ObjC class names to Java class names.
Map autoNameToJavaName = new HashMap<>();
for (String javaName : classToUrlMap.keySet()) {
autoNameToJavaName.put(getAutoName(javaName), javaName);
}
Map result = new HashMap<>();
LinkedList unresolved = new LinkedList<>(customClasses);
Map customClassValuesCache = new HashMap<>();
// Resolve auto-generated.
for (Iterator it = unresolved.iterator(); it.hasNext();) {
String objCName = it.next();
String javaName = autoNameToJavaName.get(objCName);
if (javaName != null) {
result.put(objCName, javaName);
it.remove();
}
}
// Resolve classes which match by simple name.
outer: for (Iterator it = unresolved.iterator(); it.hasNext();) {
String objCName = it.next();
for (String javaName : classToUrlMap.keySet()) {
if (matchSimpleName(objCName, javaName)) {
URL url = classToUrlMap.get(javaName);
if (objCName.equals(getCustomClass(url, customClassValuesCache))) {
result.put(objCName, javaName);
it.remove();
continue outer;
}
if (isNativeClass(url)) {
// its native class no need to add it to pre-load classes just remove it from unresolved
it.remove();
continue outer;
}
}
}
}
// Resolve classes by looking for Java classes which have names looking
// like view controllers/views.
if (!unresolved.isEmpty()) {
Map candidates = new HashMap<>();
for (String javaName : classToUrlMap.keySet()) {
if (looksLikeObjCClass(javaName)) {
String s = getCustomClass(classToUrlMap.get(javaName), customClassValuesCache);
if (s != null) {
candidates.put(s, javaName);
}
}
}
for (Iterator it = unresolved.iterator(); it.hasNext();) {
String objCName = it.next();
String javaName = candidates.get(objCName);
if (javaName != null) {
result.put(objCName, javaName);
it.remove();
}
}
}
// Finally parse every class on the classpath and look for @CustomClass
// annotations.
if (!unresolved.isEmpty()) {
outer: for (Iterator it = unresolved.iterator(); it.hasNext();) {
String objCName = it.next();
for (String javaName : classToUrlMap.keySet()) {
String s = getCustomClass(classToUrlMap.get(javaName), customClassValuesCache);
if (objCName.equals(s)) {
result.put(objCName, javaName);
it.remove();
continue outer;
}
}
}
}
if (!unresolved.isEmpty()) {
logger.warn("Failed to find Java classes for the following Objective-C classes in Storyboard/XIB files: %s",
unresolved);
}
for (Entry entry : result.entrySet()) {
builder.addForceLinkClass(entry.getValue());
preloadClasses.add(entry.getValue());
}
}
@Override
public void beforeLinker(Config config, Linker linker, Set classes) throws IOException {
if (!preloadClasses.isEmpty()) {
linker.addRuntimeData(RUNTIME_DATA_ID, StringUtils.join(preloadClasses, ",").getBytes("UTF8"));
}
}
private boolean looksLikeObjCClass(String javaName) {
return IB_CLASS_PATTERN.matcher(javaName).matches();
}
private boolean matchSimpleName(String objCName, String javaName) {
if (objCName.equals(javaName)) {
return true;
}
if (javaName.length() > objCName.length() && javaName.endsWith(objCName)) {
char c = javaName.charAt(javaName.length() - objCName.length() - 1);
return c == '.' || c == '$';
}
return false;
}
private String getCustomClass(URL url, Map customClassValuesCache) throws IOException {
if (customClassValuesCache.containsKey(url)) {
return customClassValuesCache.get(url);
}
class Visitor extends ClassVisitor {
String customClass;
Visitor() {
super(Opcodes.ASM4);
}
@Override
public AnnotationVisitor visitAnnotation(final String desc, boolean visible) {
if (CUSTOM_CLASS.equals(desc)) {
return new AnnotationVisitor(Opcodes.ASM4) {
public void visit(String name, Object value) {
customClass = (String) value;
}
};
}
return super.visitAnnotation(desc, visible);
}
}
Visitor visitor = new Visitor();
new ClassReader(IOUtils.toByteArray(url.openStream())).accept(visitor, 0);
customClassValuesCache.put(url, visitor.customClass);
return visitor.customClass;
}
private boolean isNativeClass(URL url) throws IOException {
class Visitor extends ClassVisitor {
private boolean nativeClass;
private Visitor() {
super(Opcodes.ASM4);
}
@Override
public AnnotationVisitor visitAnnotation(final String desc, boolean visible) {
if (NATIVE_CLASS.equals(desc)) {
nativeClass = true;
}
return super.visitAnnotation(desc, visible);
}
}
Visitor visitor = new Visitor();
new ClassReader(IOUtils.toByteArray(url.openStream())).accept(visitor, 0);
return visitor.nativeClass;
}
private String getAutoName(String javaName) {
return "j_" + javaName.replace('.', '_');
}
private boolean isJarFile(File f) {
return f.isFile() && FilenameUtils.isExtension(f.getName(), JAR_ZIP_EXTENSIONS);
}
private Map buildClassToUrlMap(List paths) {
// Reverse the list since classes in the first paths should take
// precedence over classes in latter paths.
Collections.reverse(paths);
Map classToUrlMap = new HashMap<>();
for (File path : paths) {
if (isJarFile(path)) {
try (ZipFile zipFile = new ZipFile(path)) {
for (ZipEntry entry : Collections.list(zipFile.entries())) {
if (!entry.isDirectory()) {
if (FilenameUtils.isExtension(entry.getName(), CLASS_EXTENSION)) {
String className = FilenameUtils.removeExtension(entry.getName()).replace('/', '.');
URL url = new URL("jar", null, -1, path.toURI().toString() + "!/" + entry.getName());
classToUrlMap.put(className, url);
}
}
}
} catch (IOException e) {
logger.warn("Failed to read JAR/ZIP file %s: %s", path.getAbsolutePath(), e.getMessage());
}
} else if (path.isDirectory()) {
path = path.getAbsoluteFile();
for (File f : FileUtils.listFiles(path, new SuffixFileFilter("." + CLASS_EXTENSION),
new PackageNameFilter(path.getAbsolutePath()))) {
String className = FilenameUtils.removeExtension(f.getAbsolutePath());
className = className.substring(path.getAbsolutePath().length() + 1);
className = className.replace(File.separatorChar, '.');
try {
classToUrlMap.put(className, f.toURI().toURL());
} catch (MalformedURLException e) {
throw new Error(e);
}
}
}
}
return classToUrlMap;
}
private static class PackageNameFilter implements IOFileFilter {
private final String baseDir;
public PackageNameFilter(String baseDir) {
this.baseDir = baseDir;
}
@Override
public boolean accept(File file) {
String packag = file.getAbsolutePath().substring(baseDir.length() + 1).replace(File.separatorChar, '.');
return !EXCLUDED_PACKAGES.matcher(packag).matches();
}
@Override
public boolean accept(File dir, String name) {
// Never called so just return true
return true;
}
}
private List findCustomClassesInIBFiles(final Config config) throws IOException {
final List customClasses = new ArrayList<>();
for (Resource res : config.getResources()) {
res.walk(new Walker() {
@Override
public boolean processDir(Resource resource, File dir, File destDir) throws IOException {
return true;
}
@Override
public void processFile(Resource resource, File file, File destDir)
throws IOException {
String filename = file.getName().toLowerCase();
if (filename.endsWith(".storyboard") || filename.endsWith(".xib")) {
try {
customClasses.addAll(findCustomClassesInIBFile(file));
} catch (XMLStreamException | IOException e) {
// Storyboard or Xib may be corrupt.
config.getLogger().warn("Failed to read Interface Builder file %s: %s",
file.getAbsolutePath(), e.getMessage());
}
}
}
});
}
return customClasses;
}
private List findCustomClassesInIBFile(File file) throws XMLStreamException, IOException {
List customClasses = new ArrayList<>();
try (FileInputStream fis = FileUtils.openInputStream(file)) {
XMLInputFactory factory = XMLInputFactory.newInstance();
XMLStreamReader reader = factory.createXMLStreamReader(fis);
while (reader.hasNext()) {
int event = reader.next();
switch (event) {
case XMLStreamConstants.START_ELEMENT:
String customClass = reader.getAttributeValue(null, "customClass");
if (customClass != null && !customClass.trim().isEmpty()) {
customClasses.add(customClass);
}
}
}
reader.close();
}
return customClasses;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy