Switch to the xsiframe GWT linker

This will be the default in a future version of GWT.  It is not
compatible with Gerrit's hacky server side selector.  Get rid of
the hacky server selection and run only the stock code.

The new linker selection is compatible with Super Dev Mode.

Change-Id: I26c2ed184b6f336d4f964732023aa81d647a5e7c
This commit is contained in:
Shawn Pearce
2014-03-07 01:44:47 -08:00
parent e6cdfa5769
commit 4aba6912cc
13 changed files with 5 additions and 582 deletions

View File

@@ -2600,14 +2600,6 @@ By default, unset, so no Expiry-Date header is generated.
[[site]]
=== Section site
[[site.checkUserAgent]]site.checkUserAgent::
+
If true the server checks the User-Agent HTTP header and sends the
correct JavaScript to the client as part of the initial page load.
This usually reduces a round-trip for the client, allowing the UI to
start more quickly. If false, a tiny JavaScript loader is sent to the
client instead to determine the correct code to use. Default is true.
[[site.refreshHeaderFooter]]site.refreshHeaderFooter::
+
If true the server checks the site header, footer and CSS files for

View File

@@ -43,14 +43,6 @@ gwt_module(
visibility = ['PUBLIC'],
)
java_library(
name = 'Linker',
srcs = glob([SRC + 'linker/rebind/*.java']),
resources = [SRC + 'linker/ServerPlannedIFrameLinker.gwt.xml'],
deps = ['//lib/gwt:dev'],
visibility = ['PUBLIC'],
)
java_library2(
name = 'linker_server',
srcs = glob([SRC + 'linker/server/*.java']),

View File

@@ -1,19 +0,0 @@
<!--
Copyright (C) 2009 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.
-->
<module>
<define-linker name='serverplanned' class='com.google.gwtexpui.linker.rebind.ServerPlannedIFrameLinker'/>
<add-linker name='serverplanned'/>
</module>

View File

@@ -1,68 +0,0 @@
// Copyright (C) 2009 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.google.gwtexpui.linker.rebind;
import com.google.gwt.core.ext.LinkerContext;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.linker.AbstractLinker;
import com.google.gwt.core.ext.linker.ArtifactSet;
import com.google.gwt.core.ext.linker.CompilationResult;
import com.google.gwt.core.ext.linker.LinkerOrder;
import com.google.gwt.core.ext.linker.SelectionProperty;
import com.google.gwt.core.ext.linker.StylesheetReference;
import java.util.Map;
import java.util.SortedMap;
/** Saves data normally used by the {@code nocache.js} file. */
@LinkerOrder(LinkerOrder.Order.POST)
public class ServerPlannedIFrameLinker extends AbstractLinker {
@Override
public String getDescription() {
return "ServerPlannedIFrameLinker";
}
@Override
public ArtifactSet link(final TreeLogger logger, final LinkerContext context,
final ArtifactSet artifacts) throws UnableToCompleteException {
ArtifactSet toReturn = new ArtifactSet(artifacts);
StringBuilder table = new StringBuilder();
for (StylesheetReference r : artifacts.find(StylesheetReference.class)) {
table.append("css ");
table.append(r.getSrc());
table.append("\n");
}
for (CompilationResult r : artifacts.find(CompilationResult.class)) {
table.append(r.getStrongName());
table.append("\n");
for (SortedMap<SelectionProperty, String> p : r.getPropertyMap()) {
for (Map.Entry<SelectionProperty, String> e : p.entrySet()) {
table.append(" ");
table.append(e.getKey().getName());
table.append("=");
table.append(e.getValue());
table.append('\n');
}
}
table.append("\n");
}
toReturn.add(emitString(logger, table.toString(), "permutations"));
return toReturn;
}
}

View File

@@ -1,36 +0,0 @@
// Copyright (C) 2009 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.google.gwtexpui.linker.server;
import javax.servlet.http.HttpServletRequest;
/** A rule that must execute on the client, as we don't know how to compute it. */
final class ClientSideRule implements Rule {
private final String name;
ClientSideRule(String name) {
this.name = name;
}
@Override
public String getName() {
return name;
}
@Override
public String select(HttpServletRequest req) {
return null;
}
}

View File

@@ -1,158 +0,0 @@
// Copyright (C) 2009 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.google.gwtexpui.linker.server;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Arrays;
/** A single permutation of the compiled GWT application. */
public class Permutation {
private final PermutationSelector selector;
private final String cacheHTML;
private final String[] values;
Permutation(PermutationSelector sel, String cacheHTML, String[] values) {
this.selector = sel;
this.cacheHTML = cacheHTML;
this.values = values;
}
boolean matches(String[] r) {
return Arrays.equals(values, r);
}
/**
* Append GWT bootstrap for this permutation onto the end of the body.
* <p>
* The GWT bootstrap for this particular permutation is appended onto the end
* of the {@code body} element of the passed host page.
* <p>
* To keep the bootstrap code small and simple, not all GWT features are
* actually supported. The {@code gwt:property}, {@code gwt:onPropertyErrorFn}
* and {@code gwt:onLoadErrorFn} meta tags are ignored and not handled.
* <p>
* Load order may differ from the standard GWT {@code nocache.js}. The browser
* is asked to load the iframe immediately, rather than after the body has
* finished loading.
*
* @param dom host page HTML document.
*/
public void inject(Document dom) {
String moduleName = selector.getModuleName();
StringBuilder s = new StringBuilder();
s.append("\n");
s.append("function ").append(moduleName).append("(){");
s.append("var s,l,t");
s.append(",w=window");
s.append(",d=document");
s.append(",n='").append(moduleName).append("'");
s.append(",f=d.createElement('iframe')");
s.append(";");
// Callback to execute the module once both s and l are true.
//
s.append("function m(){");
s.append("if(s&&l){");
// Base path needs to be absolute. There isn't an easy way to do this
// other than forcing an image to load and then pulling the URL back.
//
s.append("var b,i=d.createElement('img');");
s.append("i.src=n+'/clear.cache.gif';");
s.append("b=i.src;");
s.append("b=b.substring(0,b.lastIndexOf('/')+1);");
s.append(moduleName).append("=null;"); // allow us to GC
s.append("f.contentWindow.gwtOnLoad(undefined,n,b);");
s.append("}");
s.append("}");
// Set s true when the module script has finished loading. The
// exact name here is known to the IFrameLinker and is called by
// the code in the iframe.
//
s.append(moduleName).append(".onScriptLoad=function(){");
s.append("s=1;m();");
s.append("};");
// Set l true when the browser has finished processing the iframe
// tag, and everything else on the page.
//
s.append(moduleName).append(".r=function(){");
s.append("l=1;m();");
s.append("};");
// Prevents mixed mode security in IE6/7.
s.append("f.src=\"javascript:''\";");
s.append("f.id=n;");
s.append("f.style.cssText");
s.append("='position:absolute;width:0;height:0;border:none';");
s.append("f.tabIndex=-1;");
s.append("d.body.appendChild(f);");
// The src has to be set after the iframe is attached to the DOM to avoid
// refresh quirks in Safari. We have to use the location.replace trick to
// avoid FF2 refresh quirks.
//
s.append("f.contentWindow.location.replace(n+'/").append(cacheHTML).append("');");
// defer attribute here is to workaround IE running immediately.
//
s.append("d.write('<script defer=\"defer\">").append(moduleName).append(".r()</'+'script>');");
s.append("}");
s.append(moduleName).append("();");
s.append("\n//");
final Element html = dom.getDocumentElement();
final Element head = (Element) html.getElementsByTagName("head").item(0);
final Element body = (Element) html.getElementsByTagName("body").item(0);
for (String css : selector.getCSS()) {
if (isRelativeURL(css)) {
css = moduleName + '/' + css;
}
final Element link = dom.createElement("link");
link.setAttribute("rel", "stylesheet");
link.setAttribute("href", css);
head.appendChild(link);
}
final Element script = dom.createElement("script");
script.setAttribute("type", "text/javascript");
script.setAttribute("language", "javascript");
script.appendChild(dom.createComment(s.toString()));
body.appendChild(script);
}
private static boolean isRelativeURL(String src) {
if (src.startsWith("/")) {
return false;
}
try {
// If it parses as a URL, assume it is not relative.
//
new URL(src);
return false;
} catch (MalformedURLException e) {
}
return true;
}
}

View File

@@ -1,205 +0,0 @@
// Copyright (C) 2009 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.google.gwtexpui.linker.server;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
/**
* Selects a permutation based on the HTTP request.
* <p>
* To use this class the application's GWT module must include our linker by
* inheriting our module:
*
* <pre>
* &lt;inherits name='com.google.gwtexpui.linker.ServerPlannedIFrameLinker'/&gt;
* </pre>
*/
public class PermutationSelector {
private final String moduleName;
private final Map<String, Rule> rulesByName;
private final List<Rule> ruleOrder;
private final List<Permutation> permutations;
private final List<String> css;
/**
* Create an empty selector for a module.
* <p>
* {@link UserAgentRule} rule is automatically registered. Additional custom
* selector rules may be registered before {@link #init(ServletContext)} is
* called to finish the selector setup.
*
* @param moduleName the name of the module within the context.
*/
public PermutationSelector(final String moduleName) {
this.moduleName = moduleName;
this.rulesByName = new HashMap<String, Rule>();
this.ruleOrder = new ArrayList<Rule>();
this.permutations = new ArrayList<Permutation>();
this.css = new ArrayList<String>();
register(new UserAgentRule());
}
private void notInitialized() {
if (!ruleOrder.isEmpty()) {
throw new IllegalStateException("Already initialized");
}
}
/**
* Register a property selection rule.
*
* @param r the rule implementation.
*/
public void register(Rule r) {
notInitialized();
rulesByName.put(r.getName(), r);
}
/**
* Initialize the selector by reading the module's {@code permutations} file.
*
* @param ctx context to load the module data from.
* @throws ServletException
* @throws IOException
*/
public void init(ServletContext ctx) throws ServletException, IOException {
notInitialized();
final String tableName = "/" + moduleName + "/permutations";
final InputStream in = ctx.getResourceAsStream(tableName);
if (in == null) {
throw new ServletException("No " + tableName + " in context");
}
try {
BufferedReader r = new BufferedReader(new InputStreamReader(in, "UTF-8"));
for (;;) {
final String strongName = r.readLine();
if (strongName == null) {
break;
}
if (strongName.startsWith("css ")) {
css.add(strongName.substring("css ".length()));
continue;
}
Map<String, String> selections = new LinkedHashMap<String, String>();
for (;;) {
String permutation = r.readLine();
if (permutation == null || permutation.isEmpty()) {
break;
}
int eq = permutation.indexOf('=');
if (eq < 0) {
throw new ServletException(tableName + " has malformed content");
}
String k = permutation.substring(0, eq).trim();
String v = permutation.substring(eq + 1);
Rule rule = get(k);
if (!ruleOrder.contains(rule)) {
ruleOrder.add(rule);
}
if (selections.put(k, v) != null) {
throw new ServletException("Table " + tableName
+ " has multiple values for " + k + " within permutation "
+ strongName);
}
}
String cacheHtml = strongName + ".cache.html";
String[] values = new String[ruleOrder.size()];
for (int i = 0; i < values.length; i++) {
values[i] = selections.get(ruleOrder.get(i).getName());
}
permutations.add(new Permutation(this, cacheHtml, values));
}
} finally {
in.close();
}
}
private Rule get(final String name) {
Rule r = rulesByName.get(name);
if (r == null) {
r = new ClientSideRule(name);
register(r);
}
return r;
}
/** @return name of the module (within the application context). */
public String getModuleName() {
return moduleName;
}
/** @return all possible permutations */
public List<Permutation> getPermutations() {
return Collections.unmodifiableList(permutations);
}
/**
* Select the permutation that best matches the browser request.
*
* @param req current request.
* @return the selected permutation; null if no permutation can be fit to the
* request and the standard {@code nocache.js} loader must be used.
*/
public Permutation select(HttpServletRequest req) {
final String[] values = new String[ruleOrder.size()];
for (int i = 0; i < values.length; i++) {
final String value = ruleOrder.get(i).select(req);
if (value == null) {
// If the rule returned null it doesn't know how to compute
// the value for this HTTP request. Since we can't do that
// defer to JavaScript by not picking a permutation.
//
return null;
}
values[i] = value;
}
for (Permutation p : permutations) {
if (p.matches(values)) {
return p;
}
}
return null;
}
Collection<String> getCSS() {
return css;
}
}

View File

@@ -1,40 +0,0 @@
// Copyright (C) 2009 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.google.gwtexpui.linker.server;
import javax.servlet.http.HttpServletRequest;
/** A selection rule for a permutation property. */
public interface Rule {
/** @return the property name, for example {@code "user.agent"}. */
public String getName();
/**
* Compute the value for this property, given the current request.
* <p>
* This rule method must compute the proper permutation value, matching what
* the GWT module XML files use for this property. The rule may use any state
* available in the current servlet request.
* <p>
* If this method returns {@code null} server side selection will be aborted
* and selection for all properties will be handled on the client side by the
* {@code nocache.js} file.
*
* @param req the request
* @return the value for the property; null if the value cannot be determined
* on the server side.
*/
public String select(HttpServletRequest req);
}

View File

@@ -28,7 +28,7 @@ import javax.servlet.http.HttpServletRequest;
* <p>
* Ported from JavaScript in {@code com.google.gwt.user.UserAgent.gwt.xml}.
*/
public class UserAgentRule implements Rule {
public class UserAgentRule {
private static final Pattern msie = compile(".*msie ([0-9]+)\\.([0-9]+).*");
private static final Pattern gecko = compile(".*rv:([0-9]+)\\.([0-9]+).*");
@@ -36,7 +36,6 @@ public class UserAgentRule implements Rule {
return "user.agent";
}
@Override
public String select(HttpServletRequest req) {
String ua = req.getHeader("User-Agent");
if (ua == null) {

View File

@@ -71,7 +71,6 @@ gwt_module(
compile_deps = [
':diffy_logo',
'//gerrit-gwtexpui:CSS',
'//gerrit-gwtexpui:Linker',
'//lib:gwtjsonrpc',
'//lib:gwtjsonrpc_src',
'//lib:gwtorm',

View File

@@ -21,7 +21,6 @@
<inherits name='com.google.gwtexpui.clippy.Clippy'/>
<inherits name='com.google.gwtexpui.css.CSS'/>
<inherits name='com.google.gwtexpui.globalkey.GlobalKey'/>
<inherits name='com.google.gwtexpui.linker.ServerPlannedIFrameLinker'/>
<inherits name='com.google.gwtexpui.progress.Progress'/>
<inherits name='com.google.gwtexpui.safehtml.SafeHtml'/>
<inherits name='com.google.gerrit.extensions.Extensions'/>
@@ -37,6 +36,7 @@
<set-property name='locale' value='en'/>
<set-configuration-property name='UiBinder.useSafeHtmlTemplates' value='true'/>
<set-configuration-property name='CssResource.style' value='stable'/>
<add-linker name='xsiframe'/>
<set-property name='gwt.logging.logLevel' value='SEVERE'/>
<set-property name='gwt.logging.popupHandler' value='DISABLED'/>

View File

@@ -12,7 +12,6 @@ java_library2(
'//gerrit-common:annotations',
'//gerrit-common:server',
'//gerrit-extension-api:api',
'//gerrit-gwtexpui:linker_server',
'//gerrit-gwtexpui:server',
'//gerrit-patch-jgit:server',
'//gerrit-prettify:server',

View File

@@ -31,8 +31,6 @@ import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.SitePaths;
import com.google.gwtexpui.linker.server.Permutation;
import com.google.gwtexpui.linker.server.PermutationSelector;
import com.google.gwtexpui.server.CacheHeaders;
import com.google.gwtjsonrpc.server.JsonServlet;
import com.google.gwtjsonrpc.server.RPCServletUtils;
@@ -54,9 +52,7 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
@@ -83,7 +79,6 @@ public class HostPageServlet extends HttpServlet {
private final SitePaths site;
private final Document template;
private final String noCacheName;
private final PermutationSelector selector;
private final boolean refreshHeaderFooter;
private final StaticServlet staticServlet;
private volatile Page page;
@@ -107,7 +102,6 @@ public class HostPageServlet extends HttpServlet {
site = sp;
refreshHeaderFooter = cfg.getBoolean("site", "refreshHeaderFooter", true);
staticServlet = ss;
boolean checkUserAgent = cfg.getBoolean("site", "checkUserAgent", true);
final String pageName = "HostPage.html";
template = HtmlDomUtil.parseFile(getClass(), pageName);
@@ -152,10 +146,6 @@ public class HostPageServlet extends HttpServlet {
}
noCacheName = src;
selector = new PermutationSelector("gerrit_ui");
if (checkUserAgent && !IS_DEV) {
selector.init(servletContext);
}
page = new Page();
}
@@ -268,13 +258,8 @@ public class HostPageServlet extends HttpServlet {
Page pg = get();
if ("1".equals(req.getParameter("dbg"))) {
return pg.debug;
} else if ("0".equals(req.getParameter("s"))) {
// If s=0 is used in the URL, the user has explicitly asked us
// to not perform selection on the server side, perhaps due to
// it incorrectly guessing their user agent.
return pg.get(null);
}
return pg.get(selector.select(req));
return pg.opt;
}
private void insertETags(Element e) {
@@ -315,7 +300,7 @@ public class HostPageServlet extends HttpServlet {
private final FileInfo css;
private final FileInfo header;
private final FileInfo footer;
private final Map<Permutation, Content> permutations;
private final Content opt;
private final Content debug;
Page() throws IOException {
@@ -339,33 +324,16 @@ public class HostPageServlet extends HttpServlet {
data.appendChild(hostDoc.createTextNode(w.toString()));
data.appendChild(hostDoc.createComment(HPD_ID));
permutations = new HashMap<Permutation, Content>();
for (Permutation p : selector.getPermutations()) {
final Document d = HtmlDomUtil.clone(hostDoc);
Element nocache = HtmlDomUtil.find(d, "gerrit_module");
nocache.getParentNode().removeChild(nocache);
p.inject(d);
permutations.put(p, new Content(d));
}
Element nocache = HtmlDomUtil.find(hostDoc, "gerrit_module");
asScript(nocache);
nocache.removeAttribute("id");
nocache.setAttribute("src", noCacheName);
permutations.put(null, new Content(hostDoc));
opt = new Content(hostDoc);
nocache.setAttribute("src", "gerrit_ui/gerrit_dbg.nocache.js");
debug = new Content(hostDoc);
}
Content get(Permutation p) {
Content c = permutations.get(p);
if (c == null) {
c = permutations.get(null);
}
return c;
}
boolean isStale() {
return css.isStale() || header.isStale() || footer.isStale();
}