Java tutorial
/* * Copyright (C) 2016 Kane O'Riley * * 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 me.oriley.crate; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import com.google.common.base.Function; import com.google.common.base.Joiner; import com.google.common.collect.Iterators; import com.squareup.javapoet.*; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.imageio.ImageIO; import java.awt.*; import java.awt.image.BufferedImage; import java.io.*; import java.util.*; import java.util.List; import java.util.concurrent.TimeUnit; import static java.util.Locale.US; import static javax.lang.model.element.Modifier.*; import static me.oriley.crate.utils.JavaPoetUtils.*; import static me.oriley.crate.utils.JavaPoetUtils.Nullability.NONNULL; public final class CrateGenerator { // Deprecated options private static final String PACKAGE_NAME = CrateGenerator.class.getPackage().getName(); private static final String CLASS_NAME = "CrateDictionary"; private static final boolean STATIC_MODE = false; private static final String CRATE_HASH = CrateHasher.getActualHash(); private static final String ASSETS = "assets"; private static final String DEBUG = "debug"; private static final String CLASS = "Class"; private static final List<String> FONT_EXTENSIONS = Arrays.asList("otf", "ttf"); private static final List<String> IMAGE_EXTENSIONS = Arrays.asList("jpg", "jpeg", "gif", "png"); private static final Logger log = LoggerFactory.getLogger(CrateGenerator.class.getSimpleName()); @NonNull private final String mBaseOutputDir; @NonNull private final String mVariantAssetDir; private final boolean mDebugLogging; public CrateGenerator(@NonNull String baseOutputDir, @NonNull String variantAssetDir, boolean debugLogging) { mBaseOutputDir = baseOutputDir; mVariantAssetDir = variantAssetDir; mDebugLogging = debugLogging; log("CrateGenerator constructed\n" + " Output: " + mBaseOutputDir + "\n" + " Asset: " + mVariantAssetDir + "\n" + " Package: " + PACKAGE_NAME + "\n" + " Class: " + CLASS_NAME + "\n" + " Static: " + STATIC_MODE + "\n" + " Logging: " + mDebugLogging); } public void buildCrate() { long startNanos = System.nanoTime(); File variantDir = new File(mVariantAssetDir); if (!variantDir.exists() || !variantDir.isDirectory()) { log("Asset directory does not exist, aborting"); return; } try { brewJava(variantDir, mVariantAssetDir, PACKAGE_NAME).writeTo(new File(mBaseOutputDir)); } catch (IOException e) { logError("Failed to generate java", e, true); } long lengthMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos); log("Time to build was " + lengthMillis + "ms"); } public boolean isCrateHashValid() { String crateOutputFile = mBaseOutputDir + '/' + PACKAGE_NAME.replace('.', '/') + "/" + CLASS_NAME + ".java"; long startNanos = System.nanoTime(); File file = new File(crateOutputFile); boolean returnValue = false; if (!file.exists()) { log("File " + crateOutputFile + " doesn't exist, hash invalid"); } else if (!file.isFile()) { log("File " + crateOutputFile + " is not a file (?), hash invalid"); } else { returnValue = isFileValid(file, getComments()); } long lengthMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos); log("Hash check took " + lengthMillis + "ms, was valid: " + returnValue); return returnValue; } private boolean isFileValid(@NonNull File crateOutputFile, @NonNull String[] comments) { if (comments.length <= 0) { return false; } boolean isValid = true; try { FileReader reader = new FileReader(crateOutputFile); BufferedReader input = new BufferedReader(reader); for (String comment : comments) { String fileLine = input.readLine(); if (fileLine == null || comment == null || !StringUtils.contains(fileLine, comment)) { log("Aborting, comment: " + comment + ", fileLine: " + fileLine); isValid = false; break; } else { log("Line valid, comment: " + comment + ", fileLine: " + fileLine); } } input.close(); reader.close(); } catch (IOException e) { logError("Error parsing file", e, false); isValid = false; } log("File check result -- isValid ? " + isValid); return isValid; } private void logError(@NonNull String message, @NonNull Throwable error, boolean throwError) { log.error("Crate: " + message, error); if (throwError) { throw new IllegalStateException("Crate: Fatal Exception"); } } private void log(@NonNull String message) { if (mDebugLogging) { log.warn("Crate: " + message); } } @NonNull private JavaFile brewJava(@NonNull File variantDir, @NonNull String variantAssetDir, @NonNull String packageName) { TypeSpec.Builder builder = TypeSpec.classBuilder(CLASS_NAME).addModifiers(PUBLIC, FINAL) .addAnnotation(createSuppressWarningAnnotation("unused")); builder.addField(createBooleanField(DEBUG, mDebugLogging)); TreeMap<String, Asset> allAssets = new TreeMap<>(); listFiles(allAssets, builder, "", variantDir, variantAssetDir, true); JavaFile.Builder javaBuilder = JavaFile.builder(packageName, builder.build()).indent(" "); for (String comment : getComments()) { javaBuilder.addFileComment(comment + "\n"); } return javaBuilder.build(); } @NonNull private String[] getComments() { return new String[] { CRATE_HASH, "Package: " + PACKAGE_NAME, "Class: " + CLASS_NAME, "Static: " + STATIC_MODE, "Debug: " + mDebugLogging }; } private void listFiles(@NonNull TreeMap<String, Asset> allAssets, @NonNull TypeSpec.Builder parentBuilder, @NonNull String classPathString, @NonNull File directory, @NonNull String variantAssetDir, boolean root) { String rootName = root ? ASSETS : directory.getName(); TypeSpec.Builder builder = TypeSpec.classBuilder(capitalise(rootName + CLASS)).addModifiers(PUBLIC, STATIC, FINAL); List<File> files = getFileList(directory); TreeMap<String, Asset> assetMap = new TreeMap<>(); boolean isFontFolder = true; boolean isImageFolder = true; for (File file : files) { if (file.isDirectory()) { listFiles(allAssets, builder, classPathString + file.getName() + ".", file, variantAssetDir, false); } else { String fileName = file.getName(); String fieldName = sanitiseFieldName(fileName).toUpperCase(US); if (assetMap.containsKey(fieldName)) { String baseFieldName = fieldName + "_"; int counter = 0; while (assetMap.containsKey(fieldName)) { fieldName = baseFieldName + counter; } } String filePath = file.getPath().replace(variantAssetDir + "/", ""); String fileExtension = getFileExtension(fileName).toLowerCase(US); AssetHolder asset; if (FONT_EXTENSIONS.contains(fileExtension)) { isImageFolder = false; String fontName = getFontName(file.getPath()); asset = new FontAssetHolder(fieldName, filePath, fileName, fontName != null ? fontName : fileName); builder.addField(createFontAssetField((FontAssetHolder) asset)); } else if (IMAGE_EXTENSIONS.contains(fileExtension)) { isFontFolder = false; int width = 0; int height = 0; try { BufferedImage image = ImageIO.read(file); if (image != null) { width = image.getWidth(); height = image.getHeight(); } } catch (IOException e) { logError("Error parsing image: " + file.getPath(), e, false); } asset = new ImageAssetHolder(fieldName, filePath, fileName, width, height); builder.addField(createImageAssetField((ImageAssetHolder) asset)); } else { isFontFolder = false; isImageFolder = false; asset = new AssetHolder(fieldName, filePath, fileName); builder.addField(createAssetField(asset)); } assetMap.put(fieldName, asset); allAssets.put(classPathString + fieldName, asset); } } if (!assetMap.isEmpty()) { TypeName elementType = TypeVariableName .get(isFontFolder ? FontAsset.class : isImageFolder ? ImageAsset.class : Asset.class); TypeName listType = ParameterizedTypeName.get(ClassName.get(List.class), elementType); builder.addField(createListField(listType, "LIST", assetMap)); } if (root && !allAssets.isEmpty()) { TypeName listType = ParameterizedTypeName.get(ClassName.get(List.class), TypeVariableName.get(Asset.class)); builder.addField(createListField(listType, "FULL_LIST", allAssets)); } parentBuilder.addType(builder.build()); parentBuilder.addField(createNonStaticClassField(rootName)); } @NonNull private FieldSpec createListField(@NonNull TypeName typeName, @NonNull String fieldName, @NonNull Map<String, Asset> assets) { FieldSpec.Builder builder = FieldSpec.builder(typeName, fieldName).addModifiers(PUBLIC, FINAL); return builder.initializer( CodeBlock.builder().add("$T.unmodifiableList($T.asList(", Collections.class, Arrays.class) .add(Joiner.on(", ").join(Iterators.transform(assets.entrySet().iterator(), new Function<Map.Entry<String, Asset>, String>() { @Override public String apply(Map.Entry<String, Asset> entry) { return entry != null ? entry.getKey() : null; } })) + "))") .build()) .build(); } @NonNull private FieldSpec createNonStaticClassField(@NonNull String rootName) { TypeName typeName = TypeVariableName.get(capitalise(rootName + CLASS)); FieldSpec.Builder builder = FieldSpec.builder(typeName, rootName).addModifiers(PUBLIC, FINAL) .initializer("new $T()", typeName); addNullability(builder, NONNULL); return builder.build(); } @NonNull private FieldSpec createAssetField(@NonNull AssetHolder asset) { FieldSpec.Builder builder = FieldSpec.builder(Asset.class, asset.mFieldName).addModifiers(PUBLIC, FINAL); asset.addInitialiser(builder); return builder.build(); } @NonNull private FieldSpec createFontAssetField(@NonNull FontAssetHolder asset) { FieldSpec.Builder builder = FieldSpec.builder(FontAsset.class, asset.mFieldName).addModifiers(PUBLIC, FINAL); asset.addInitialiser(builder); return builder.build(); } @NonNull private FieldSpec createImageAssetField(@NonNull ImageAssetHolder asset) { FieldSpec.Builder builder = FieldSpec.builder(ImageAsset.class, asset.mFieldName).addModifiers(PUBLIC, FINAL); asset.addInitialiser(builder); return builder.build(); } @NonNull private static List<File> getFileList(@NonNull File directory) { if (!directory.exists() || !directory.isDirectory()) { throw new IllegalArgumentException("Crate: Invalid file passed: " + directory.getAbsolutePath()); } List<File> files = new LinkedList<>(); File[] fileArray = directory.listFiles(); if (fileArray != null) { Arrays.sort(fileArray, new Comparator<File>() { @Override public int compare(File o1, File o2) { boolean o1Directory = o1.isDirectory(); boolean o2Directory = o2.isDirectory(); if ((o1Directory && o2Directory) || (!o1Directory && !o2Directory)) { return o1.getName().compareToIgnoreCase(o2.getName()); } else { return o1Directory ? -1 : 1; } } }); Collections.addAll(files, fileArray); } return files; } @Nullable private static String getFontName(@NonNull String filePath) { try { FileInputStream inputStream = new FileInputStream(filePath); Font font = Font.createFont(Font.TRUETYPE_FONT, inputStream); inputStream.close(); return font.getName(); } catch (FontFormatException | IOException e) { e.printStackTrace(); return null; } } @NonNull private static String getFileExtension(@NonNull String fileName) { String extension = ""; int i = fileName.lastIndexOf('.'); if (i > 0) { extension = fileName.substring(i + 1); } return extension; } @NonNull private static String sanitiseFieldName(@NonNull String fileName) { // JavaPoet doesn't like the dollar signs so we remove them too char[] charArray = fileName.toCharArray(); for (int i = 0; i < charArray.length; i++) { if (!Character.isJavaIdentifierPart(charArray[i]) || charArray[i] == '$') { charArray[i] = '_'; } } if (!Character.isJavaIdentifierStart(charArray[0]) || charArray[0] == '$') { return "_" + new String(charArray); } else { return new String(charArray); } } @SuppressWarnings("unused") private static class AssetHolder extends Asset { @NonNull final String mFieldName; private AssetHolder(@NonNull String fieldName, @NonNull String path, @NonNull String name) { super(path, name); mFieldName = fieldName; } public void addInitialiser(@NonNull FieldSpec.Builder builder) { builder.initializer("new $T($S, $S)", Asset.class, mPath, mName); } } @SuppressWarnings("unused") private static final class FontAssetHolder extends AssetHolder { @NonNull final String mFontName; private FontAssetHolder(@NonNull String fieldName, @NonNull String path, @NonNull String name, @NonNull String fontName) { super(fieldName, path, name); mFontName = fontName; } public void addInitialiser(@NonNull FieldSpec.Builder builder) { builder.initializer("new $T($S, $S, $S)", FontAsset.class, mPath, mName, mFontName); } } @SuppressWarnings("unused") private static final class ImageAssetHolder extends AssetHolder { final int mWidth; final int mHeight; private ImageAssetHolder(@NonNull String fieldName, @NonNull String path, @NonNull String name, int width, int height) { super(fieldName, path, name); mWidth = width; mHeight = height; } public void addInitialiser(@NonNull FieldSpec.Builder builder) { builder.initializer("new $T($S, $S, $L, $L)", ImageAsset.class, mPath, mName, mWidth, mHeight); } } }