blob: c5755f9522b37c5ee670cdb4de978b517458afef [file] [log] [blame]
/*
* Copyright (C) 2015 The Android Open Source Project
*
* 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.android.tools.idea.structure.services;
import com.android.tools.idea.model.ManifestInfo;
import com.android.tools.idea.ui.properties.core.StringValueProperty;
import com.google.common.io.Files;
import com.google.common.io.Resources;
import com.intellij.ide.BrowserUtil;
import com.intellij.openapi.application.Application;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.command.WriteCommandAction;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.util.io.FileUtil;
import org.jetbrains.android.facet.AndroidFacet;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.xml.sax.SAXException;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParserFactory;
import java.io.*;
import java.net.URL;
import java.util.concurrent.Callable;
/**
* A class used by plugins to expose their service resources to Android Studio.
* <p/>
* Each subclass of this class should be associated with a single service, providing access to the
* content files (service.xml, recipe.xml, code files, etc.) that make up their service. This
* allows this base class to instantiate an associated {@link DeveloperService}
*/
public abstract class DeveloperServiceCreator {
/**
* Simple interface for a callback that runs on the dispatch thread.
*
* @see #runInBackground(Callable, Dispatchable)
*/
protected interface Dispatchable<T> {
void dispatch(@NotNull T input);
}
/**
* Reserved filename for the xml file which defines a service.
*/
private static final String SERVICE_XML = "service.xml";
/**
* Plugins return resources via InputStreams. We create a temporary directory and create Files
* for each of the InputStreams, which is important because the template parsing APIs we work
* with require them.
*/
@NotNull private final File myRootPath;
public DeveloperServiceCreator() {
try {
// TODO: Here, we copy all resources out from the initializer and create local File copies
// of them, because our template code (see Template.java, RecipeXmlParser.java) requires
// working with them. A longer term solution is to make our template code work with
// InputStreams. The biggest obstacle is our template code currently supports recursive
// directory operations (e.g. copy src/* to dest/), and InputStreams can't point to
// directories or allow walking a directory.
myRootPath = new File(FileUtil.generateRandomTemporaryPath(), getResourceRoot());
myRootPath.deleteOnExit();
for (String name : getResources()) {
assert !name.contains("..") : "Initializer resource can't specify relative path";
File file = new File(myRootPath, name);
Files.createParentDirs(file);
assert file.createNewFile();
String fullName = String.format("%1$s/%2$s", getResourceRoot(), name);
URL resource = getClass().getResource(fullName);
if (resource == null) {
throw new FileNotFoundException(String.format("Could not find service file %1$s", fullName));
}
Resources.asByteSource(resource).copyTo(Files.asByteSink(file));
}
}
catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* Returns a new {@link DeveloperService}, which can be used to install the service into the
* associated {@link Module}. Returns {@code null} if the initialization fails for any reason.
*/
@Nullable
public final DeveloperService createService(@NotNull Module module) {
if (AndroidFacet.getInstance(module) == null) {
throw new IllegalArgumentException(
String.format("Developer service cannot be associated with non-Android module %s", module.getName()));
}
final ServiceContext context = createContext(module);
initializeContext(context);
final ServiceXmlParser serviceParser = new ServiceXmlParser(module, myRootPath, context);
try {
final InputStream serviceXml = new FileInputStream(new File(myRootPath, SERVICE_XML));
try {
WriteCommandAction.runWriteCommandAction(module.getProject(), new Runnable() {
@Override
public void run() {
try {
SAXParserFactory.newInstance().newSAXParser().parse(serviceXml, serviceParser);
}
catch (ParserConfigurationException e) {
throw new RuntimeException(e);
}
catch (SAXException e) {
throw new RuntimeException(e);
}
catch (IOException e) {
throw new RuntimeException(e);
}
}
});
}
finally {
serviceXml.close();
}
}
catch (Exception e) {
Logger.getInstance(getClass())
.warn(String.format("Skipping over invalid service in module %1$s: %2$s", module.getName(), e.getMessage()));
return null;
}
return new DeveloperService(serviceParser);
}
@NotNull
private ServiceContext createContext(@NotNull Module module) {
ServiceContext context = new ServiceContext();
String packageName = ManifestInfo.get(module, false).getPackage();
if (packageName != null) {
context.putValue("packageName", new StringValueProperty(packageName));
}
return context;
}
/**
* Useful method child classes can call to run code asynchronously. A followup callback,
* if provided, will be run on the dispatch thread on successful completion of the initial
* callback, as long as the initial callback doesn't throw an exception or return null.
* The output of the initial callback will be fed as an argument into the dispatch callback.
*/
protected final <T> void runInBackground(@NotNull final Callable<T> backgroundAction, @Nullable final Dispatchable<T> dispatchAfter) {
final Application application = ApplicationManager.getApplication();
application.executeOnPooledThread(new Runnable() {
@Override
public void run() {
try {
T result = backgroundAction.call();
if (result != null && dispatchAfter != null) {
dispatchAfter.dispatch(result);
}
}
catch (Exception e) {
// We currently don't care about failure, and treat it same as returning a null result
// TODO: If necessary, add a param for handling failure
}
}
});
}
/**
* Convenience method to open a target ULR in a browser window.
*/
protected final void browse(@NotNull String url) {
BrowserUtil.browse(url);
}
/**
* Returns the root path that all resource paths returned by {@link #getResources()} live under.
* <p/>
* Be sure any slashes included in this path are forward slashes, as the value will be passed
* into {@link Class#getResource(String)}.
*/
@NotNull
protected abstract String getResourceRoot();
/**
* Returns the path to all resources required by this service, so that the external code loading
* this plugin can find them. One of the resources MUST be named "service.xml".
*/
// TODO: Revisit this and eliminate if possible. It exists so we can fetch all resources
// under getResourceRoot() as input streams and convert them to files.
@NotNull
protected abstract String[] getResources();
/**
* Given a fresh context, initialize it with values used by this service.
*/
protected abstract void initializeContext(@NotNull ServiceContext serviceContext);
}