package org.netbeans.gradle.model; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.net.URL; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.IdentityHashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicReference; import org.gradle.tooling.BuildAction; import org.gradle.tooling.BuildActionExecuter; import org.gradle.tooling.BuildController; import org.gradle.tooling.ProjectConnection; import org.gradle.tooling.model.DomainObjectSet; import org.gradle.tooling.model.gradle.BasicGradleProject; import org.gradle.tooling.model.gradle.GradleBuild; import org.netbeans.gradle.model.api.GradleProjectInfoQuery2; import org.netbeans.gradle.model.internal.CustomSerializedMap; import org.netbeans.gradle.model.internal.ModelQueryInput; import org.netbeans.gradle.model.internal.ModelQueryOutput; import org.netbeans.gradle.model.internal.ModelQueryOutputRef; import org.netbeans.gradle.model.util.BasicFileUtils; import org.netbeans.gradle.model.util.BuilderUtils; import org.netbeans.gradle.model.util.ClassLoaderUtils; import org.netbeans.gradle.model.util.CollectionUtils; import org.netbeans.gradle.model.util.SerializationCache; import org.netbeans.gradle.model.util.SerializationCaches; import org.netbeans.gradle.model.util.SerializationUtils; import org.netbeans.gradle.model.util.TemporaryFileManager; import org.netbeans.gradle.model.util.TemporaryFileRef; public final class GenericModelFetcher { private static final Charset INIT_SCRIPT_ENCODING = Charset.forName("UTF-8"); private static final String INIT_SCRIPT_LOCATION = "/org/netbeans/gradle/scripts/dynamic-model-init-script.gradle"; private static final AtomicReference<String> INIT_SCRIPT_REF = new AtomicReference<String>(null); private static final String DEFAULT_MODEL_INPUT_PREFIX = "model-input"; private static final String DEFAULT_INIT_SCRIPT_PREFIX = "dyn-model-gradle-init"; private static volatile String modelInputPrefix = DEFAULT_MODEL_INPUT_PREFIX; private static volatile String initScriptPrefix = DEFAULT_INIT_SCRIPT_PREFIX; // key -> list of BuildInfoBuilder private final GradleInfoQueryMap buildInfoBuilders; // key -> list of ProjectInfoBuilder private final GradleInfoQueryMap projectInfoBuilders; // TODO: These classes must be key based as well. private final Set<Class<?>> modelClasses; public GenericModelFetcher( Map<Object, List<GradleBuildInfoQuery<?>>> buildInfoRequests, Map<Object, List<GradleProjectInfoQuery2<?>>> projectInfoRequests, Collection<Class<?>> modelClasses) { this.buildInfoBuilders = GradleInfoQueryMap.fromBuildInfos(buildInfoRequests); this.projectInfoBuilders = GradleInfoQueryMap.fromProjectInfos(projectInfoRequests); this.modelClasses = Collections.unmodifiableSet(new HashSet<Class<?>>(modelClasses)); CollectionUtils.checkNoNullElements(this.modelClasses, "modelClasses"); } public static void setDefaultPrefixes() { modelInputPrefix = DEFAULT_MODEL_INPUT_PREFIX; initScriptPrefix = DEFAULT_INIT_SCRIPT_PREFIX; } public static void setModelInputPrefix(String modelInputPrefix) { if (modelInputPrefix == null) throw new NullPointerException("modelInputPrefix"); GenericModelFetcher.modelInputPrefix = modelInputPrefix; } public static void setInitScriptPrefix(String initScriptPrefix) { if (initScriptPrefix == null) throw new NullPointerException("initScriptPrefix"); GenericModelFetcher.initScriptPrefix = initScriptPrefix; } private FetchedModelsOrError transformActionModels(ActionFetchedModelsOrError actionModels) { return new FetchedModelsOrError( transformActionModels(actionModels.getModels()), actionModels.getBuildScriptEvaluationError(), actionModels.getUnexpectedError()); } private FetchedProjectModels transformActionModels(ActionFetchedProjectModels actionModels) { GradleMultiProjectDef projectDef = actionModels.getProjectDef(); Map<Class<?>, Object> toolingModels = actionModels.getToolingModels(); Map<Object, List<?>> projectInfoResults = projectInfoBuilders.deserializeResults( actionModels.getProjectInfoResults(), GradleInfoQueryMap.builderIssueTransformer()); Throwable issue = actionModels.getIssue(); return new FetchedProjectModels(projectDef, projectInfoResults, toolingModels, issue); } private Collection<FetchedProjectModels> transformActionModels(Collection<ActionFetchedProjectModels> actionModels) { List<FetchedProjectModels> result = new ArrayList<FetchedProjectModels>(actionModels.size()); for (ActionFetchedProjectModels entry: actionModels) { result.add(transformActionModels(entry)); } return result; } private FetchedModels transformActionModels(ActionFetchedModels actionModels) { if (actionModels == null) { return null; } Map<Object, List<?>> buildModels = buildInfoBuilders.deserializeResults( actionModels.getBuildModels(), GradleInfoQueryMap.builderIssueTransformer()); FetchedProjectModels defaultProjectModels = transformActionModels(actionModels.getDefaultProjectModels()); Collection<FetchedProjectModels> otherProjectModels = transformActionModels(actionModels.getOtherProjectModels()); return new FetchedModels(new FetchedBuildModels(buildModels), defaultProjectModels, otherProjectModels); } public FetchedModelsOrError getModels(ProjectConnection connection, OperationInitializer init) throws IOException { BuildActionExecuter<ActionFetchedModelsOrError> executer = connection.action( new ModelFetcherBuildAction(buildInfoBuilders, modelClasses)); BuildOperationArgs buildOPArgs = new BuildOperationArgs(); init.initOperation(buildOPArgs); buildOPArgs.setupLongRunningOP(executer); String[] userArgs = buildOPArgs.getArguments(); if (userArgs == null) { userArgs = new String[0]; } String initScript = getInitScript(); initScript = initScript.replace( "$NB_BOOT_CLASSPATH", toPastableString(ClassLoaderUtils.getLocationOfClassPath().getPath())); TemporaryFileManager fileManager = TemporaryFileManager.getDefault(); ModelQueryInput modelInput = new ModelQueryInput(projectInfoBuilders.getSerializableBuilderMap()); TemporaryFileRef modelInputFile = fileManager.createFileFromSerialized(modelInputPrefix, modelInput); try { initScript = initScript.replace("$INPUT_FILE", toPastableString(modelInputFile.getFile())); TemporaryFileRef initScriptRef = fileManager .createFile(initScriptPrefix, initScript, INIT_SCRIPT_ENCODING); try { String[] executerArgs = new String[userArgs.length + 2]; System.arraycopy(userArgs, 0, executerArgs, 0, userArgs.length); executerArgs[executerArgs.length - 2] = "--init-script"; executerArgs[executerArgs.length - 1] = initScriptRef.getFile().getPath(); executer.withArguments(executerArgs); return transformActionModels(executer.run()); } finally { initScriptRef.close(); } } finally { modelInputFile.close(); } } private static String toPastableString(File file) { return toPastableString(file.getAbsolutePath()); } private static String toPastableString(String value) { String result = value; result = result.replace("\\", "\\\\"); result = result.replace("'", "\\'"); result = BasicFileUtils.toSafelyPastableToJavaCode(result); return "'" + result + "'"; } private static String readAllFromReader(Reader reader) throws IOException { int expectedFileSize = 4 * 1024; StringBuilder result = new StringBuilder(expectedFileSize); char[] buf = new char[expectedFileSize]; do { int readCount = reader.read(buf); if (readCount <= 0) { break; } result.append(buf, 0, readCount); } while (true); return result.toString(); } private static String readResourceText(String resourcePath, Charset charset) throws IOException { URL resourceURL = GenericModelFetcher.class.getResource(resourcePath); if (resourceURL == null) { throw new IOException("Missing resource: " + resourcePath); } InputStream resourceIS = resourceURL.openStream(); try { Reader resourceReader = new InputStreamReader(resourceIS, charset); try { return readAllFromReader(resourceReader); } finally { resourceReader.close(); } } finally { resourceIS.close(); } } private static String getInitScript() { String result = INIT_SCRIPT_REF.get(); if (result == null) { try { result = readResourceText(INIT_SCRIPT_LOCATION, INIT_SCRIPT_ENCODING); } catch (IOException ex) { throw new IllegalStateException("Missing init-script file from resource.", ex); } INIT_SCRIPT_REF.set(result); result = INIT_SCRIPT_REF.get(); } return result; } private static <T> T getModel(ModelGetter getter, Class<T> modelClass) { T result = getter.findModel(modelClass); if (result == null) { throw new RuntimeException("Required model could not be loaded: " + modelClass); } return result; } private static ModelGetter defaultModelGetter(final BuildController controller) { return new ModelGetter() { public <T> T findModel(Class<T> modelClass) { return controller.getModel(modelClass); } }; } private static ModelGetter projectModelGetter( final BuildController controller, final BasicGradleProject referenceProject) { return new ModelGetter() { public <T> T findModel(Class<T> modelClass) { return controller.findModel(referenceProject, modelClass); } }; } private static ModelQueryOutput getModelOutput(SerializationCache cache, ModelGetter getter) { byte[] serializedResult = getModel(getter, ModelQueryOutputRef.class) .getSerializedModelQueryOutput(); try { return (ModelQueryOutput)SerializationUtils.deserializeObject(serializedResult, cache); } catch (ClassNotFoundException ex) { throw new RuntimeException(ex); } } private static final class ModelFetcherBuildAction implements BuildAction<ActionFetchedModelsOrError> { private static final long serialVersionUID = 1L; // key -> list of BuildInfoBuilder private final CustomSerializedMap.Deserializer serializedBuildInfoRequests; private final Set<Class<?>> modelClasses; public ModelFetcherBuildAction( GradleInfoQueryMap buildInfoRequests, Set<Class<?>> modelClasses) { this.serializedBuildInfoRequests = buildInfoRequests.getSerializableBuilderMap(); this.modelClasses = modelClasses; } private CustomSerializedMap getBuildInfoResults(BuildController controller) { ClassLoader parentClassLoader = getClass().getClassLoader(); Map<Object, List<?>> buildInfoRequests = serializedBuildInfoRequests.deserialize(SerializationCaches.getDefault(), parentClassLoader, GradleInfoQueryMap.buildInfoBuilderIssueTransformer()); if (buildInfoRequests.isEmpty()) { return CustomSerializedMap.EMPTY; } CustomSerializedMap.Builder result = new CustomSerializedMap.Builder(buildInfoRequests.size()); for (Map.Entry<Object, List<?>> entry: buildInfoRequests.entrySet()) { Object key = entry.getKey(); for (Object buildBuilder: entry.getValue()) { Object info = null; Throwable issue = null; BuildInfoBuilder<?> builder = null; try { builder = (BuildInfoBuilder<?>)buildBuilder; info = builder.getInfo(controller); } catch (Throwable ex) { issue = ex; } if (info != null || issue != null) { BuilderResult builderResult = new BuilderResult( info, BuilderUtils.createIssue(builder, issue)); result.addValue(key, builderResult); } } } return result.create(); } public ActionFetchedModels executeUnsafe(EvaluatedBuild evaluatedBuild, BuildController controller) { AllProjectInfoBuilder builder = new AllProjectInfoBuilder(modelClasses, evaluatedBuild); Map<String, ActionFetchedProjectModels> fetchedModels = builder.buildProjectModels(controller); ActionFetchedProjectModels defaultModels = fetchedModels.remove(builder.getDefaultProjectPath()); CustomSerializedMap buildModels = getBuildInfoResults(controller); return new ActionFetchedModels(buildModels, defaultModels, fetchedModels.values()); } public ActionFetchedModelsOrError execute(final BuildController controller) { EvaluatedBuild evaluatedBuild; try { // Note: Currently Gradle throws an exception before actually // executing the build action. However, if this behaviour is // ever changed, I expect them to be thrown here. evaluatedBuild = new EvaluatedBuild(controller); } catch (Throwable buildScriptEvaluationError) { return new ActionFetchedModelsOrError(null, buildScriptEvaluationError, null); } Throwable unexpected = null; ActionFetchedModels result = null; try { result = executeUnsafe(evaluatedBuild, controller); } catch (Throwable ex) { unexpected = ex; } return new ActionFetchedModelsOrError(result, null, unexpected); } } private static final class EvaluatedBuild { public final BuildController controller; public final GradleBuild buildModel; public final Collection<? extends BasicGradleProject> allProjects; public EvaluatedBuild(BuildController controller) { this.controller = controller; this.buildModel = controller.getBuildModel(); this.allProjects = buildModel.getProjects(); } } private static final class AllProjectInfoBuilder { private final Set<Class<?>> modelClasses; private final Map<String, BasicGradleProject> basicInfos; private final Map<String, ModelQueryOutput> customInfos; private final BasicGradleProject basicRootProject; private final String defaultProjectPath; private final SerializationCache serializationCache; public AllProjectInfoBuilder(Set<Class<?>> modelClasses, EvaluatedBuild evaluatedBuild) { int projectCount = evaluatedBuild.allProjects.size(); this.modelClasses = modelClasses; this.basicInfos = CollectionUtils.newHashMap(projectCount); this.customInfos = CollectionUtils.newHashMap(projectCount); this.basicRootProject = evaluatedBuild.buildModel.getRootProject(); this.serializationCache = SerializationCaches.getDefault(); this.defaultProjectPath = addCustomInfo(defaultModelGetter(evaluatedBuild.controller)); // TODO: If lazy project evaluation is available, review this // not to force evaluation of unnecessary projects. for (BasicGradleProject project: evaluatedBuild.allProjects) { addBasicInfo(project); } } private String addCustomInfo(ModelGetter modelGetter) { assert serializationCache != null : "serializationCache is null in addCustomInfo"; ModelQueryOutput customInfo = getModelOutput(serializationCache, modelGetter); String projectPath = customInfo.getBasicInfo().getProjectFullName(); customInfos.put(projectPath, customInfo); return projectPath; } private void addBasicInfo(BasicGradleProject projectRef) { basicInfos.put(projectRef.getPath(), projectRef); } public String getDefaultProjectPath() { return defaultProjectPath; } // Note: We expect the result of this method to be mutable. public Map<String, ActionFetchedProjectModels> buildProjectModels(BuildController controller) { for (Map.Entry<String, BasicGradleProject> entry: basicInfos.entrySet()) { String projectPath = entry.getKey(); if (!customInfos.containsKey(projectPath)) { String addedProjectPath = addCustomInfo(projectModelGetter(controller, entry.getValue())); if (!projectPath.equals(addedProjectPath)) { throw new IllegalStateException("The path fetched from" + " the build script is different than provided" + " by BasicGradleProject. BasicGradleProject.path = " + projectPath + ". ModelQueryOutput.projectFullName = " + addedProjectPath); } } } Map<String, GradleProjectTree> projectTrees = CollectionUtils.newHashMap(basicInfos.size()); GradleProjectTree rootTree = parseTrees(controller, basicRootProject, projectTrees); // This should be a NO-OP because all projects should be reachable // from the root project. Do it anyway. for (BasicGradleProject project: basicInfos.values()) { parseTrees(controller, project, projectTrees); } Map<String, ActionFetchedProjectModels> result = CollectionUtils.newHashMap(basicInfos.size()); for (Map.Entry<String, BasicGradleProject> entry: basicInfos.entrySet()) { ActionFetchedProjectModels fetchedModels = getFetchedProjectModels(controller, entry, rootTree, projectTrees); result.put(entry.getKey(), fetchedModels); } return result; } private ActionFetchedProjectModels getFetchedProjectModels( BuildController controller, Map.Entry<String, BasicGradleProject> entry, GradleProjectTree rootTree, Map<String, GradleProjectTree> projects) { String projectPath = entry.getKey(); ModelQueryOutput modelOutput = customInfos.get(projectPath); if (modelOutput == null) { throw new IllegalStateException("Missing ModelQueryOutput for project " + projectPath); } GradleProjectTree projectTree = projects.get(projectPath); if (projectTree == null) { throw new IllegalStateException("Missing GradleProjectTree for project " + projectPath); } Map<Class<?>, Object> toolingModels; if (modelClasses.isEmpty()) { toolingModels = Collections.emptyMap(); } else { ModelGetter modelGetter = projectModelGetter(controller, entry.getValue()); toolingModels = new IdentityHashMap<Class<?>, Object>(2 * modelClasses.size()); for (Class<?> modelClass: modelClasses) { Object modelValue = modelGetter.findModel(modelClass); if (modelValue != null) { toolingModels.put(modelClass, modelValue); } } } return new ActionFetchedProjectModels( new GradleMultiProjectDef(rootTree, projectTree), modelOutput.getProjectInfoResults(), toolingModels, modelOutput.getIssue()); } private GradleProjectTree parseTrees( BuildController controller, BasicGradleProject project, Map<String, GradleProjectTree> trees) { String projectPath = project.getPath(); GradleProjectTree cached = trees.get(projectPath); if (cached != null) { return cached; } DomainObjectSet<? extends BasicGradleProject> basicChildren = project.getChildren(); List<GradleProjectTree> children = new ArrayList<GradleProjectTree>(basicChildren.size()); for (BasicGradleProject child: basicChildren) { children.add(parseTrees(controller, child, trees)); } ModelQueryOutput customInfo = customInfos.get(projectPath); if (customInfo == null) { throw new IllegalStateException("Missing ModelQueryOutput for project " + projectPath); } ModelQueryOutput.BasicInfo basicInfo = customInfo.getBasicInfo(); GenericProjectProperties genericProperties = new GenericProjectProperties( basicInfo.getProjectId(), projectPath, project.getProjectDirectory(), basicInfo.getBuildScript(), basicInfo.getBuildDir()); GradleProjectTree result = new GradleProjectTree( genericProperties, basicInfo.getTasks(), children); trees.put(projectPath, result); return result; } } private interface ModelGetter { public <T> T findModel(Class<T> modelClass); } }