Author: mhermanto
Date: Thu Feb 24 04:28:51 2011
New Revision: 1074040
URL: http://svn.apache.org/viewvc?rev=1074040&view=rev
Log:
Improve JS export mechanism to enable property renaming, in JS runtime
compilation.
http://codereview.appspot.com/4175057/
Added:
shindig/trunk/features/src/main/javascript/features/exportjs/
shindig/trunk/features/src/main/javascript/features/exportjs/exportjs.js
shindig/trunk/features/src/main/javascript/features/exportjs/feature.xml
shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/js/
shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/js/AbstractJsCompilerTest.java
Modified:
shindig/trunk/features/src/main/javascript/features/features.txt
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/js/AbstractJsCompiler.java
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/js/JsCompiler.java
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/JsHandler.java
Added: shindig/trunk/features/src/main/javascript/features/exportjs/exportjs.js
URL:
http://svn.apache.org/viewvc/shindig/trunk/features/src/main/javascript/features/exportjs/exportjs.js?rev=1074040&view=auto
==============================================================================
--- shindig/trunk/features/src/main/javascript/features/exportjs/exportjs.js
(added)
+++ shindig/trunk/features/src/main/javascript/features/exportjs/exportjs.js
Thu Feb 24 04:28:51 2011
@@ -0,0 +1,83 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview This provides a mechanism to export symbols, across all
+ * cases below. This implementation works best when used with
ClosureJsCompiler.
+ *
+ * "global" object, ie: feature=globals.
+ * feature directive: <exports type="js">gadgets</exports>
+ * code: var gadgets = {};
+ *
+ * "singleton" object, ie: feature=rpc.
+ * feature directive: <exports type="js">gadgets.foo.bar</exports>
+ * gadgets.foo = function() {
+ * return { bar : function() { ... } };
+ * }();
+ *
+ * "closured" object, ie: feature=shindig.uri.
+ * This wraps to a function that exports any resulting properties it returns
+ * in an Object. feature directive: <exports
type="js">gadgets.foo.bar</exports>
+ * gadgets.foo = (function() {
+ * return { bar : function() { ... } };
+ * })();
+ *
+ * "prototype" object, ie: feature=container.
+ * feature directive: <exports type="js">gadgets.foo.prototype.bar</exports>
+ * gadgets.foo = function() {};
+ * gadgets.foo.prototype.bar = function() { ... };
+ *
+ * "static" object.
+ * feature directive: <exports type="js">gadgets.foo.bar</exports>
+ * gadgets.foo = {};
+ * gadgets.foo.bar = function() { ... };
+ */
+function exportJs(namespace, components, opt_props) {
+ var base = window;
+ var prevBase = null;
+ var nsParts = namespace.split('.');
+
+ for (var i = 0, part; part = nsParts.shift(); i++) {
+ base[part] = components[i] || {};
+ prevBase = base;
+ base = base[part];
+ }
+
+ var exportProps = function(root) {
+ var props = opt_props || {};
+ for (var prop in props) {
+ if (props.hasOwnProperty(prop) && root.hasOwnProperty(prop)) {
+ root[props[prop]] = root[prop];
+ }
+ }
+ };
+
+ if (typeof base === 'object') {
+ exportProps(base);
+
+ } else if (typeof base === 'function') {
+ var exportedFn = function() {
+ var result = base.apply(null, arguments);
+ if (typeof result === 'object') {
+ exportProps(result);
+ }
+ return result;
+ };
+ prevBase[part] = exportedFn;
+ }
+}
Added: shindig/trunk/features/src/main/javascript/features/exportjs/feature.xml
URL:
http://svn.apache.org/viewvc/shindig/trunk/features/src/main/javascript/features/exportjs/feature.xml?rev=1074040&view=auto
==============================================================================
--- shindig/trunk/features/src/main/javascript/features/exportjs/feature.xml
(added)
+++ shindig/trunk/features/src/main/javascript/features/exportjs/feature.xml
Thu Feb 24 04:28:51 2011
@@ -0,0 +1,27 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+-->
+<feature>
+ <name>exportjs</name>
+ <gadget>
+ <script src="exportjs.js"/>
+ </gadget>
+ <container>
+ <script src="exportjs.js"/>
+ </container>
+</feature>
Modified: shindig/trunk/features/src/main/javascript/features/features.txt
URL:
http://svn.apache.org/viewvc/shindig/trunk/features/src/main/javascript/features/features.txt?rev=1074040&r1=1074039&r2=1074040&view=diff
==============================================================================
--- shindig/trunk/features/src/main/javascript/features/features.txt (original)
+++ shindig/trunk/features/src/main/javascript/features/features.txt Thu Feb 24
04:28:51 2011
@@ -39,6 +39,7 @@ features/core/feature.xml
features/dynamic-height.height/feature.xml
features/dynamic-height.util/feature.xml
features/dynamic-height/feature.xml
+features/exportjs/feature.xml
features/flash/feature.xml
features/i18n/feature.xml
features/jsondom/feature.xml
Modified:
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/js/AbstractJsCompiler.java
URL:
http://svn.apache.org/viewvc/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/js/AbstractJsCompiler.java?rev=1074040&r1=1074039&r2=1074040&view=diff
==============================================================================
---
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/js/AbstractJsCompiler.java
(original)
+++
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/js/AbstractJsCompiler.java
Thu Feb 24 04:28:51 2011
@@ -17,43 +17,155 @@
*/
package org.apache.shindig.gadgets.rewrite.js;
-import com.google.caja.util.Sets;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.inject.Inject;
-import org.apache.commons.lang.StringUtils;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.features.FeatureRegistry;
+import org.apache.shindig.gadgets.features.FeatureRegistry.LookupResult;
+import org.apache.shindig.gadgets.features.FeatureResource;
-import java.util.Collections;
+import java.util.Collection;
import java.util.List;
-import java.util.Set;
+import java.util.Map;
/**
* Base for a JsCompiler implementation.
*/
public abstract class AbstractJsCompiler implements JsCompiler {
- public String generateExportStatements(List<String> symbols) {
- StringBuilder exportStatements = new StringBuilder();
- Set<String> allExports = Sets.newHashSet();
- List<String> exports = Lists.newArrayListWithCapacity(symbols.size());
- exports.addAll(symbols);
- Collections.sort(exports);
- String prevExport = null;
- for (String export : exports) {
- if (!export.equals(prevExport)) {
- String[] pieces = StringUtils.split(export, "\\.");
- String base = "window";
- for (int i = 0; i < pieces.length; ++i) {
- String symExported = (i == 0) ? pieces[0] : base + "." + pieces[i];
- if (!allExports.contains(symExported)) {
- String curExport = base + "['" + pieces[i] + "']=" + symExported +
";\n";
- exportStatements.append(curExport);
- allExports.add(symExported);
- }
- base = symExported;
- }
+ @VisibleForTesting
+ static final String FEATURE_NAME = "exportjs";
+
+ private static final String FUNCTION_NAME = "exportJs";
+
+ private boolean hasInjectedJs = false;
+ private final FeatureRegistry featureRegistry;
+
+ @Inject
+ public AbstractJsCompiler(FeatureRegistry featureRegistry) {
+ this.featureRegistry = featureRegistry;
+ }
+
+ private void appendExportJsFeature(StringBuilder out, GadgetContext context)
{
+ LookupResult lookup = featureRegistry.getFeatureResources(context,
+ ImmutableList.of(FEATURE_NAME), null);
+ for (FeatureRegistry.FeatureBundle bundle : lookup.getBundles()) {
+ for (FeatureResource resource : bundle.getResources()) {
+ out.append(resource.getDebugContent());
+ }
+ }
+ }
+
+ public String generateExportStatements(GadgetContext context, List<String>
symbols) {
+ Collection<Input> inputs = generateInputs(symbols);
+ StringBuilder result = new StringBuilder();
+ if (!inputs.isEmpty()) {
+ if (!hasInjectedJs) {
+ appendExportJsFeature(result, context);
+ hasInjectedJs = true;
}
- prevExport = export;
+ for (Input input : inputs) {
+ result.append(generateExportStatement(input));
+ }
+ }
+ return result.toString();
+ }
+
+ private static class Input {
+ String namespace;
+ List<String> components;
+ List<String> properties;
+ private Input(String namespace, List<String> components) {
+ this.namespace = namespace;
+ this.components = components;
+ this.properties = Lists.newArrayList();
+ }
+ static Input newGlobal() {
+ return new Input(null, ImmutableList.<String>of());
}
- return exportStatements.toString();
+ static Input newLocal(String namespace, List<String> components) {
+ return new Input(namespace, components);
+ }
+ }
+
+ private List<String> expandNamespace(String namespace) {
+ List<String> result = Lists.newArrayList();
+ for (int from = 0; ;) {
+ int idx = namespace.indexOf('.', from);
+ if (idx >= 0) {
+ result.add(namespace.substring(0, idx));
+ from = idx + 1;
+ } else {
+ result.add(namespace);
+ break;
+ }
+ }
+ return result;
+ }
+
+ private Collection<Input> generateInputs(List<String> symbols) {
+ Map<String, Input> result = Maps.newHashMap();
+ for (String symbol : symbols) {
+ String ns = getNamespace(symbol);
+ Input input = result.get(ns);
+ if (input == null) {
+ input = (ns != null) ? Input.newLocal(ns, expandNamespace(ns)) :
Input.newGlobal();
+ result.put(ns, input);
+ }
+ String property = (ns != null) ? getProperty(symbol) : symbol;
+ input.properties.add(property);
+ }
+ return result.values();
+ }
+
+ private String generateExportStatement(Input input) {
+ StringBuilder result = new StringBuilder();
+
+ // Local namespace.
+ if (input.namespace != null) {
+
result.append(FUNCTION_NAME).append("('").append(input.namespace).append("',[");
+ result.append(Joiner.on(',').join(input.components));
+ result.append("],{");
+ for (int i = 0; i < input.properties.size(); i++) {
+ String prop = input.properties.get(i);
+ if (i > 0) result.append(",");
+ result.append(prop).append(":'").append(prop).append("'");
+ }
+ result.append("});");
+
+ // Global/window namespace.
+ } else {
+ for (int i = 0; i < input.properties.size(); i++) {
+ String prop = input.properties.get(i);
+ result.append(FUNCTION_NAME).append("(");
+ result.append("'").append(prop).append("',[").append(prop);
+ result.append("]);");
+ }
+ }
+
+ return result.toString();
+ }
+
+ /**
+ * Return the namespace for symbol (before last dot). If symbol is global,
+ * return null, to indicate "window" namespace.
+ */
+ private String getNamespace(String symbol) {
+ int idx = symbol.lastIndexOf('.');
+ return (idx >= 0) ? symbol.substring(0, idx) : null;
+ }
+
+ /**
+ * Return the property of symbol (after last dot). If symbol is global,
+ * return the original string.
+ */
+ private String getProperty(String symbol) {
+ int idx = symbol.lastIndexOf('.');
+ return (idx >= 0) ? symbol.substring(idx + 1) : symbol;
}
}
Modified:
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/js/JsCompiler.java
URL:
http://svn.apache.org/viewvc/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/js/JsCompiler.java?rev=1074040&r1=1074039&r2=1074040&view=diff
==============================================================================
---
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/js/JsCompiler.java
(original)
+++
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/js/JsCompiler.java
Thu Feb 24 04:28:51 2011
@@ -17,45 +17,48 @@
*/
package org.apache.shindig.gadgets.rewrite.js;
+import org.apache.shindig.gadgets.GadgetContext;
+
import java.util.List;
public interface JsCompiler {
/**
* Compiles the provided code with the provided list of external symbols.
- *
+ *
* @param jsData The code to compile.
* @param externs The list of external symbols.
* @return A compilation result object.
*/
public Result compile(String jsData, List<String> externs);
-
+
/**
* Generates a sequence of statements to mark the specified symbols as
* exported.
*
+ * @param context The gadget context.
* @param symbols The symbols to export.
* @return A sequence of JavaScript statements to export those symbols.
*/
- public String generateExportStatements(List<String> symbols);
-
+ public String generateExportStatements(GadgetContext context, List<String>
symbols);
+
public static class Result {
private final String compiled;
private final List<String> errors;
-
+
public Result(String compiled) {
this.compiled = compiled;
this.errors = null;
}
-
+
public Result(List<String> errors) {
this.compiled = null;
this.errors = errors;
}
-
+
public String getCode() {
return compiled;
}
-
+
public List<String> getErrors() {
return errors;
}
Modified:
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/JsHandler.java
URL:
http://svn.apache.org/viewvc/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/JsHandler.java?rev=1074040&r1=1074039&r2=1074040&view=diff
==============================================================================
---
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/JsHandler.java
(original)
+++
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/JsHandler.java
Thu Feb 24 04:28:51 2011
@@ -136,7 +136,7 @@ public class JsHandler {
// Add externs for this bundle.
appendExternsForBundle(bundle, allExterns);
- jsData.append(compiler.generateExportStatements(rawExports));
+ jsData.append(compiler.generateExportStatements(ctx, rawExports));
}
}
Added:
shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/js/AbstractJsCompilerTest.java
URL:
http://svn.apache.org/viewvc/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/js/AbstractJsCompilerTest.java?rev=1074040&view=auto
==============================================================================
---
shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/js/AbstractJsCompilerTest.java
(added)
+++
shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/js/AbstractJsCompilerTest.java
Thu Feb 24 04:28:51 2011
@@ -0,0 +1,110 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.apache.shindig.gadgets.rewrite.js;
+
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.replay;
+import static org.junit.Assert.assertEquals;
+
+import com.google.common.collect.ImmutableList;
+
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.features.FeatureRegistry;
+import org.apache.shindig.gadgets.features.FeatureRegistry.LookupResult;
+import org.apache.shindig.gadgets.features.FeatureResource;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.List;
+
+public class AbstractJsCompilerTest {
+ private final String EXPORT_JS_FUNCTION = "function exportJs() { };";
+
+ private AbstractJsCompiler compiler;
+ private GadgetContext context;
+
+ @Before
+ public void setUp() throws Exception {
+ context = new GadgetContext();
+ FeatureResource featureResourceMock = mockFeatureResource();
+ FeatureRegistry.FeatureBundle featureBundle = new
FeatureRegistry.FeatureBundle(
+ null, ImmutableList.of(featureResourceMock));
+ LookupResult lookupMock = mockLookupResult(featureBundle);
+ FeatureRegistry featureRegistryMock = mockFeatureRegistry(lookupMock);
+
+ compiler = new AbstractJsCompiler(featureRegistryMock) {
+ public Result compile(String jsData, List<String> externs) {
+ return null;
+ }
+ };
+ }
+
+ @SuppressWarnings("unchecked")
+ private FeatureRegistry mockFeatureRegistry(LookupResult lookupMock) {
+ FeatureRegistry result = createMock(FeatureRegistry.class);
+ expect(result.getFeatureResources(
+ context, ImmutableList.of(AbstractJsCompiler.FEATURE_NAME), null)).
+ andReturn(lookupMock).anyTimes();
+ replay(result);
+ return result;
+ }
+
+ private LookupResult mockLookupResult(FeatureRegistry.FeatureBundle
featureBundle) {
+ LookupResult result = createMock(LookupResult.class);
+
expect(result.getBundles()).andReturn(ImmutableList.of(featureBundle)).anyTimes();
+ replay(result);
+ return result;
+ }
+
+ private FeatureResource mockFeatureResource() {
+ FeatureResource result = createMock(FeatureResource.class);
+ expect(result.getDebugContent()).andReturn(EXPORT_JS_FUNCTION).anyTimes();
+ replay(result);
+ return result;
+ }
+
+ @Test
+ public void testGenerateExportStatementsEmpty() throws Exception {
+ String actual = compiler.generateExportStatements(context,
ImmutableList.<String>of());
+ assertEquals("", actual);
+ }
+
+ @Test
+ public void testGenerateExportStatementsNotEmpty() throws Exception {
+ ImmutableList<String> list = ImmutableList.of(
+ "gadgets",
+ "gadgets.rpc.call",
+ "cc",
+ "cc.prototype.site");
+ String actual = compiler.generateExportStatements(context, list);
+ assertEquals(
+ EXPORT_JS_FUNCTION +
+ "exportJs('gadgets',[gadgets]);" +
+ "exportJs('cc',[cc]);" +
+ "exportJs('gadgets.rpc',[gadgets,gadgets.rpc],{call:'call'});" +
+ "exportJs('cc.prototype',[cc,cc.prototype],{site:'site'});",
+ actual);
+ list = ImmutableList.of("gadgets.util.makeClosure");
+ actual = compiler.generateExportStatements(context, list);
+ assertEquals(
+ // JS functions only get inlined once.
+
"exportJs('gadgets.util',[gadgets,gadgets.util],{makeClosure:'makeClosure'});",
+ actual);
+ }
+}