package com.mobilesorcery.sdk.html5;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileFilter;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubProgressMonitor;
import com.mobilesorcery.sdk.core.CoreMoSyncPlugin;
import com.mobilesorcery.sdk.core.IBuildResult;
import com.mobilesorcery.sdk.core.IBuildSession;
import com.mobilesorcery.sdk.core.IBuildVariant;
import com.mobilesorcery.sdk.core.IFileTreeDiff;
import com.mobilesorcery.sdk.core.IProcessConsole;
import com.mobilesorcery.sdk.core.IPropertyOwner;
import com.mobilesorcery.sdk.core.MoSyncBuilder;
import com.mobilesorcery.sdk.core.MoSyncExtension;
import com.mobilesorcery.sdk.core.MoSyncExtensionManager;
import com.mobilesorcery.sdk.core.MoSyncProject;
import com.mobilesorcery.sdk.core.MoSyncTool;
import com.mobilesorcery.sdk.core.PropertyUtil;
import com.mobilesorcery.sdk.core.SectionedPropertiesFile;
import com.mobilesorcery.sdk.core.SectionedPropertiesFile.Section;
import com.mobilesorcery.sdk.core.Util;
import com.mobilesorcery.sdk.core.build.AbstractBuildStep;
import com.mobilesorcery.sdk.core.build.AbstractBuildStepFactory;
import com.mobilesorcery.sdk.core.build.BundleBuildStep;
import com.mobilesorcery.sdk.core.build.IBuildStep;
import com.mobilesorcery.sdk.core.security.IApplicationPermissions;
import com.mobilesorcery.sdk.core.security.ICommonPermissions;
import com.mobilesorcery.sdk.html5.debug.JSODDSupport;
import com.mobilesorcery.sdk.html5.debug.hotreplace.FileRedefinable;
import com.mobilesorcery.sdk.internal.builder.IncrementalBuilderVisitor;
import com.mobilesorcery.sdk.internal.dependencies.DependencyManager;
public class HTML5DebugSupportBuildStep extends AbstractBuildStep {
private final class InstrumentationBuilderVisitor extends
IncrementalBuilderVisitor {
private final class Rewriter {
// TODO: Refactor this class into JSODDSupport; makes
// it much easier.
private final JSODDSupport op;
private Rewriter(JSODDSupport op) {
this.op = op;
}
public FileRedefinable rewrite(IResource resourceToInstrument, boolean fetchRemotely, boolean delete)
throws CoreException {
IPath resourcePath = resourceToInstrument.getFullPath();
resourcePath = resourcePath.removeFirstSegments(inputRoot
.getFullPath().segmentCount());
FileRedefinable result = null;
if (delete) {
result = op.delete(resourceToInstrument.getFullPath(),
op.getBaseline());
} else {
Writer output = null;
try {
output = createWriter(resourcePath);
MoSyncProject mosyncProject = MoSyncProject
.create(getProject());
if (fetchRemotely) {
op.generateRemoteFetch(mosyncProject, resourceToInstrument, output);
}
// This is a *build* op, so update the baseline.
result = op.rewrite(
resourceToInstrument.getFullPath(), fetchRemotely ? null : output,
op.getBaseline());
} catch (IOException e) {
throw new CoreException(
new Status(
IStatus.ERROR,
Html5Plugin.PLUGIN_ID,
"Cannot instrument JavaScript for debugging",
e));
} finally {
Util.safeClose(output);
}
}
return result;
}
public void writeFramework() throws CoreException {
Writer output = null;
try {
output = createWriter(new Path(JSODDSupport.getFrameworkPath()));
op.writeFramework(output);
} catch (IOException e) {
throw new CoreException(new Status(IStatus.ERROR, Html5Plugin.PLUGIN_ID, "Could not create debug framework", e));
} finally {
Util.safeClose(output);
}
}
private Writer createWriter(IPath localPath) throws IOException {
File outputFile = new File(outputRoot,
localPath.toOSString());
outputFile.getParentFile().mkdirs();
return new BufferedWriter(new OutputStreamWriter(
new FileOutputStream(outputFile), "UTF-8"));
}
}
private final HashSet<IResource> resourcesToInstrument = new HashSet<IResource>();
private final File outputRoot;
private final IFolder inputRoot;
private IFileTreeDiff diff;
public InstrumentationBuilderVisitor(IFolder inputRoot, File outputRoot) {
this.inputRoot = inputRoot;
this.outputRoot = outputRoot;
}
@Override
public boolean doesAffectBuild(IResource resource) {
IPath localPath = resource.getFullPath();
if (resource.getType() == IResource.FILE) {
localPath = Html5Plugin.getDefault().getLocalPath((IFile) resource);
}
return localPath != null && JSODDSupport.isValidJavaScriptFile(resource.getLocation());
}
@Override
public boolean visit(IResource resource) throws CoreException {
boolean shouldVisitChildren = super.visit(resource);
if (isBuildable(resource)) {
resourcesToInstrument.add(resource);
}
return shouldVisitChildren;
}
@Override
public void setDiff(IFileTreeDiff diff) throws CoreException {
super.setDiff(diff);
this.diff = diff;
}
public void instrument(IProgressMonitor monitor,
DependencyManager<IResource> dependencies,
IProcessConsole console) throws CoreException {
final JSODDSupport op = Html5Plugin.getDefault().getJSODDSupport(
project);
if (op == null) {
String platformTypeWarning = MoSyncProject.create(project).getProfileManagerType() == MoSyncTool.LEGACY_PROFILE_TYPE ?
"Make the project platform based and make sure the HTML5 capability is selected" : "Make sure that the HTML5 capability is selected.";
throw new CoreException(new Status(IStatus.ERROR, Html5Plugin.PLUGIN_ID,
MessageFormat.format("The project {0} has no support for HTML5. {1}", project.getName(), platformTypeWarning)));
}
IFileTreeDiff diff = this.diff;
// If we've changed the IP addr, then rebuild it all...
if (updateHTML5SpecificProps(op) || op.requiresFullBuild()) {
diff = null;
setDiff(diff);
}
op.applyDiff(diff);
Set<IResource> instrumentThese = computeResourcesToRebuild(dependencies);
Set<IResource> deleted = new HashSet<IResource>(
Arrays.asList(getDeletedResources()));
instrumentThese.addAll(deleted);
boolean fetchRemotely = Html5Plugin.getDefault().shouldFetchRemotely();
Rewriter rewriter = new Rewriter(op);
/*if (!instrumentThese.isEmpty() && fetchRemotely) {
IResource indexHtml = Html5Plugin.getDefault().getLocalFile(
project, new Path("index.html"));
if (indexHtml.getType() != IResource.FILE
|| !indexHtml.exists()) {
throw new CoreException(new Status(IStatus.ERROR,
Html5Plugin.PLUGIN_ID,
"Missing index.html, cannot build for debugging"));
}
rewriter.rewrite(indexHtml, true, false);
instrumentThese.remove(indexHtml);
}*/
for (IResource instrumentThis : instrumentThese) {
if (monitor.isCanceled()) {
return;
}
dependencies.addDependency(instrumentThis,
getResourceBundleLocation(project));
long start = System.currentTimeMillis();
boolean wasDeleted = deleted.contains(instrumentThis);
FileRedefinable instrumented = rewriter.rewrite(instrumentThis, fetchRemotely, wasDeleted);
int memoryConsumption = instrumented == null ? 0 : instrumented.getMemSize();
long elapsed = System.currentTimeMillis() - start;
String errorMsg = instrumented.validate();
errorMsg = errorMsg == null ? "" : (" (" + errorMsg + ")");
console.addMessage(MessageFormat.format(
"Instrumented {0} [{1}, {2}].{3}", instrumentThis
.getFullPath(),
Util.elapsedTime((int) elapsed), wasDeleted ? "deleted"
: Util.dataSize(memoryConsumption),
errorMsg));
}
if (diff == null) {
rewriter.writeFramework();
}
}
private boolean updateHTML5SpecificProps(JSODDSupport op) throws CoreException {
try {
IPath jsoddMetaData = getBuildState().getLocation().append(
".jsodd");
SectionedPropertiesFile jsoddPropsFile = SectionedPropertiesFile
.create();
if (jsoddMetaData.toFile().exists()) {
jsoddPropsFile = SectionedPropertiesFile
.parse(jsoddMetaData.toFile());
}
Map<String, String> jsoddProps = jsoddPropsFile
.getDefaultSection().getEntriesAsMap();
// TODO: Should NOT use default properties.
String host = op.getDefaultProperties().get(
JSODDSupport.SERVER_HOST_PROP);
String port = op.getDefaultProperties().get(
JSODDSupport.SERVER_PORT_PROP);
String reloadStrategy = Integer.toString(Html5Plugin.getDefault().getReloadStrategy());
String sourceChangeStartegy = Integer.toString(Html5Plugin.getDefault().getSourceChangeStrategy());
String enabled = Boolean.toString(Html5Plugin.getDefault().isJSODDEnabled(MoSyncProject.create(getProject())));
String oldReloadStrategy = jsoddProps.get(Html5Plugin.RELOAD_STRATEGY_PREF);
String oldSourceChangeStartegy = jsoddProps.get(Html5Plugin.SOURCE_CHANGE_STRATEGY_PREF);
String oldHost = jsoddProps.get(JSODDSupport.SERVER_HOST_PROP);
String oldPort = jsoddProps.get(JSODDSupport.SERVER_PORT_PROP);
String oldEnabled = jsoddProps.get(Html5Plugin.ODD_SUPPORT_PREF);
Section defaultSection = jsoddPropsFile.getDefaultSection();
defaultSection.getEntries().clear();
defaultSection.addEntry(JSODDSupport.SERVER_HOST_PROP, host);
defaultSection.addEntry(JSODDSupport.SERVER_PORT_PROP, port);
defaultSection.addEntry(Html5Plugin.RELOAD_STRATEGY_PREF, reloadStrategy);
defaultSection.addEntry(Html5Plugin.SOURCE_CHANGE_STRATEGY_PREF, sourceChangeStartegy);
defaultSection.addEntry(Html5Plugin.ODD_SUPPORT_PREF, enabled);
jsoddPropsFile.write(jsoddMetaData.toFile());
return !Util.equals(host, oldHost) ||
!Util.equals(port, oldPort) ||
!Util.equals(reloadStrategy, oldReloadStrategy) ||
!Util.equals(sourceChangeStartegy, oldSourceChangeStartegy) ||
!Util.equals(enabled, oldEnabled);
} catch (IOException e) {
CoreMoSyncPlugin.getDefault().log(e);
return false;
}
}
}
public static class Factory extends AbstractBuildStepFactory {
@Override
public IBuildStep create() {
return new HTML5DebugSupportBuildStep(this);
}
@Override
public String getId() {
return HTML5DebugSupportBuildStepExtension.ID;
}
@Override
public String getName() {
return "HTML5/JavaScript bundling";
}
public boolean isDefault() {
// Always false!
return false;
}
}
public HTML5DebugSupportBuildStep(Factory prototype) {
setName(prototype.getName());
}
public IResource getResourceBundleLocation(IProject project) {
return project.getFile(new Path("Resources/LocalFiles.bin"));
}
@Override
public int incrementalBuild(MoSyncProject project, IBuildSession session,
IBuildVariant variant, IFileTreeDiff diff, IBuildResult result,
IProgressMonitor monitor) throws Exception {
IProject wrappedProject = project.getWrappedProject();
IPath inputRootPath = Html5Plugin.getHTML5Folder(wrappedProject);
IFolder inputRootFolder = wrappedProject.getFolder(inputRootPath);
File inputRoot = inputRootFolder.getLocation().toFile();
if (inputRootFolder.exists()) {
IResource outputFile = getResourceBundleLocation(wrappedProject);
File outputResource = outputFile.getLocation().toFile();
IPropertyOwner properties = MoSyncBuilder.getPropertyOwner(project,
variant.getConfigurationId());
DependencyManager<IResource> deps = getBuildState()
.getDependencyManager();
// MOSYNC-2326 & MOSYNC-2327
deps.addDependency(outputFile, inputRootFolder);
File instrOutputRoot = MoSyncBuilder
.getOutputPath(wrappedProject, variant)
.append(inputRootPath).toFile();
List<MoSyncExtension> extensions = MoSyncExtensionManager.getDefault().getUsedExtensions(project, variant);
boolean useOutputFolderForBundling = !extensions.isEmpty();
for (MoSyncExtension extension : extensions) {
// Copy the JavaScript libs.
File jsLibs = extension.getLibPath().append("js").toFile();
if (jsLibs.exists()) {
Util.copy(new SubProgressMonitor(monitor, 1), jsLibs, new File(instrOutputRoot, "ext"), Util.getExtensionFilter("js"));
}
}
if (PropertyUtil.getBoolean(properties,
MoSyncBuilder.USE_DEBUG_RUNTIME_LIBS) && Html5Plugin.getDefault().isJSODDEnabled(project)) {
useOutputFolderForBundling = true;
monitor.beginTask("Instrumenting JavaScript source files", 10);
// Ok, do NOT copy files that we may want to instrument
// Do not uncomment this until we have a strategy --
// for various reasons (fixed file systems for example)
// we keep a conservative view at this point.
//if (!Html5Plugin.getDefault().shouldFetchRemotely()) {
copyUninstrumentedFiles(monitor, inputRoot, instrOutputRoot);
//}
InstrumentationBuilderVisitor visitor = new InstrumentationBuilderVisitor(
inputRootFolder, instrOutputRoot);
visitor.setProject(wrappedProject);
visitor.setDiff(diff);
visitor.instrument(new SubProgressMonitor(monitor, 7), deps,
getConsole());
// We need internet permissions.
IApplicationPermissions modifiedPermissions = project.getPermissions().createWorkingCopy();
modifiedPermissions.setRequestedPermission(ICommonPermissions.INTERNET, true);
session.getProperties().put(MODIFIED_PERMISSIONS, modifiedPermissions);
} else if (useOutputFolderForBundling) {
Util.copyDir(new SubProgressMonitor(monitor, 1), inputRoot, instrOutputRoot, null);
}
BundleBuildStep.bundle(useOutputFolderForBundling ? instrOutputRoot : inputRoot, outputResource);
}
monitor.done();
return CONTINUE;
}
private void copyUninstrumentedFiles(IProgressMonitor monitor,
File inputRoot, File outputRoot) throws IOException {
Util.copyDir(new SubProgressMonitor(monitor, 3), inputRoot, outputRoot,
new FileFilter() {
@Override
public boolean accept(File file) {
return !JSODDSupport.isValidJavaScriptFile(new Path(
file.getAbsolutePath()));
}
});
}
}