Skip to content

Commit

Permalink
Cache template parsing results (#1918)
Browse files Browse the repository at this point in the history
Fixes #1718
  • Loading branch information
Denis authored Jun 29, 2017
1 parent 2937249 commit f7f52f1
Show file tree
Hide file tree
Showing 9 changed files with 477 additions and 120 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.logging.Logger;

Expand All @@ -27,6 +28,7 @@
import com.vaadin.annotations.HtmlImport;
import com.vaadin.external.jsoup.Jsoup;
import com.vaadin.external.jsoup.nodes.Element;
import com.vaadin.flow.util.ReflectionCache;
import com.vaadin.server.VaadinRequest;
import com.vaadin.server.VaadinService;
import com.vaadin.server.VaadinServletRequest;
Expand All @@ -41,14 +43,17 @@
* The implementation scans all HTML imports annotations for the given template
* class and tries to find the one that contains template definition using the
* tag name.
*
*
* @see TemplateParser
*
*
* @author Vaadin Ltd
*
*/
public class DefaultTemplateParser implements TemplateParser {

private static final ReflectionCache<PolymerTemplate, AtomicBoolean> LOG_CACHE = new ReflectionCache<>(
clazz -> new AtomicBoolean());

@SuppressWarnings("rawtypes")
@Override
public Element getTemplateContent(Class<? extends PolymerTemplate> clazz,
Expand All @@ -59,10 +64,14 @@ public Element getTemplateContent(Class<? extends PolymerTemplate> clazz,
assert session != null;

ServletContext context = session.getHttpSession().getServletContext();

boolean logEnabled = LOG_CACHE.get(clazz).compareAndSet(false, true);

for (HtmlImport htmlImport : AnnotationReader.getAnnotationsFor(clazz,
HtmlImport.class)) {
String path = resolvePath(request, htmlImport.value());
getLogger().info(

log(logEnabled, Level.INFO,
String.format("Html import path '%s' is resolved to '%s'",
htmlImport.value(), path));
try (InputStream content = context.getResourceAsStream(path)) {
Expand All @@ -75,15 +84,16 @@ public Element getTemplateContent(Class<? extends PolymerTemplate> clazz,
Element templateElement = parseHtmlImport(content,
htmlImport.value());
if (isTemplateImport(templateElement, tag)) {
getLogger().info(String.format(
"Found a template file containing template "
+ "definition for the tag '%s' by the path '%s'",
tag, htmlImport.value()));
log(logEnabled, Level.INFO,
String.format(
"Found a template file containing template "
+ "definition for the tag '%s' by the path '%s'",
tag, htmlImport.value()));
return templateElement;
}
} catch (IOException exception) {
// ignore exception on close()
getLogger().log(Level.WARNING,
log(logEnabled, Level.WARNING,
"Couldn't close template input stream", exception);
}
}
Expand Down Expand Up @@ -115,8 +125,9 @@ private String resolvePath(VaadinRequest request, String path) {
assert servletPath != null;
if (!servletPath.endsWith("/") && !uri.startsWith("/")) {
servletPath += "/";
} else if(servletPath.endsWith("/") && uri.startsWith("/")) {
servletPath = servletPath.substring(0, servletPath.length()-1);
} else if (servletPath.endsWith("/") && uri.startsWith("/")) {
servletPath = servletPath.substring(0,
servletPath.length() - 1);
}
// "Revert" the `../` from uri resolver so that we point to the
// context root.
Expand Down Expand Up @@ -150,6 +161,19 @@ private Element parseHtmlImport(InputStream content, String path) {
}
}

private void log(boolean enabled, Level level, String msg) {
if (enabled) {
getLogger().log(level, msg);
}
}

private void log(boolean enabled, Level level, String msg,
Exception exception) {
if (enabled) {
getLogger().log(level, msg, exception);
}
}

private Logger getLogger() {
return Logger.getLogger(DefaultTemplateParser.class.getName());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
Expand All @@ -34,7 +35,9 @@
import com.vaadin.flow.dom.ShadowRoot;
import com.vaadin.flow.dom.impl.BasicElementStateProvider;
import com.vaadin.flow.nodefeature.AttachTemplateChildFeature;
import com.vaadin.flow.util.ReflectionCache;
import com.vaadin.server.CustomElementRegistry;
import com.vaadin.server.VaadinService;
import com.vaadin.ui.Component;
import com.vaadin.util.ReflectTools;

Expand All @@ -55,7 +58,57 @@ public class TemplateInitializer {
// id to Element map
private Map<String, Element> registeredCustomElements = new HashMap<String, Element>();

private final Function<String, Optional<String>> tagProvider;
@SuppressWarnings("rawtypes")
private static final ReflectionCache<PolymerTemplate, ParserData> CACHE = new ReflectionCache<>(
clazz -> new ParserData());

private boolean useCache;

private ParserData parseData;

private com.vaadin.external.jsoup.nodes.Element contentElement;

private static class SubTemplateData {
private String id;
private String tag;
private JsonArray path;

private SubTemplateData(String id, String tag, JsonArray path) {
this.id = id;
this.tag = tag;
this.path = path;
}
}

private static class ParserData
implements Function<String, Optional<String>> {

private final Map<String, String> tagById = new HashMap<>();

private final Collection<SubTemplateData> subTemplates = new ArrayList<>();

@Override
public Optional<String> apply(String id) {
return Optional.ofNullable(tagById.get(id));
}

private void addTag(String id, String tag) {
if (isProduction()) {
tagById.put(id, tag);
}
}

private void addSubTemplate(String id, String tag, JsonArray path) {
if (isProduction()) {
subTemplates.add(new SubTemplateData(id, tag, path));
}
}

private boolean isProduction() {
return VaadinService.getCurrent().getDeploymentConfiguration()
.isProductionMode();
}
}

/**
* Creates a new initializer instance.
Expand All @@ -69,8 +122,23 @@ public TemplateInitializer(PolymerTemplate<?> template,
TemplateParser parser) {
this.template = template;

tagProvider = parseTemplate(parser.getTemplateContent(
template.getClass(), getElement().getTag()));
boolean productionMode = VaadinService.getCurrent()
.getDeploymentConfiguration().isProductionMode();
useCache = CACHE.contains(template.getClass()) && productionMode;
if (productionMode) {
parseData = CACHE.get(template.getClass());
} else {
// always initialize parseData to avoid check against null
parseData = new ParserData();
}

if (useCache) {
createSubTemplates();
} else {
contentElement = parser.getTemplateContent(template.getClass(),
getElement().getTag());
parseTemplate();
}
}

/**
Expand All @@ -92,15 +160,13 @@ private void inspectCustomElements(Node node,
}
}

private Function<String, Optional<String>> parseTemplate(
com.vaadin.external.jsoup.nodes.Element element) {
Elements templates = element.getElementsByTag("template");
private void parseTemplate() {
assert contentElement != null;
Elements templates = contentElement.getElementsByTag("template");
if (!templates.isEmpty()) {
inspectCustomElements(templates.get(0), templates.get(0),
registeredCustomElements);
}
return id -> Optional.ofNullable(element.getElementById(id))
.map(com.vaadin.external.jsoup.nodes.Element::tagName);
}

private boolean isInsideTemplate(
Expand All @@ -127,30 +193,37 @@ private void requestAttachCustomElement(
+ "sub-templates are not supported. Sub-template found: \n"
+ element);
}
StateNode customNode = BasicElementStateProvider
.createStateNode(tag);
Element customElement = Element.get(customNode);
CustomElementRegistry.getInstance()
.wrapElementIfNeeded(customElement);

if (element.hasAttr("id")) {
registeredCustomElements.put(element.attr("id"), customElement);
}

// make sure that shadow root is available
getShadowRoot();
String id = element.hasAttr("id") ? element.attr("id") : null;
JsonArray path = getPath(element, templateRoot);
assert !useCache;
parseData.addSubTemplate(id, tag, path);
doRequestAttachCustomElement(id, tag, path);
}
}

StateNode stateNode = getElement().getNode();
private void doRequestAttachCustomElement(String id, String tag,
JsonArray path) {
StateNode customNode = BasicElementStateProvider.createStateNode(tag);
Element customElement = Element.get(customNode);
CustomElementRegistry.getInstance().wrapElementIfNeeded(customElement);

stateNode.runWhenAttached(ui -> {
stateNode.getFeature(AttachTemplateChildFeature.class)
.register(getElement(), customNode);
ui.getPage().executeJavaScript(
"this.attachCustomElement($0, $1, $2, $3);",
getElement(), tag, customNode.getId(),
getPath(element, templateRoot));
});
if (id != null) {
registeredCustomElements.put(id, customElement);
}

// make sure that shadow root is available
getShadowRoot();

StateNode stateNode = getElement().getNode();

stateNode.runWhenAttached(ui -> {
stateNode.getFeature(AttachTemplateChildFeature.class)
.register(getElement(), customNode);
ui.getPage().executeJavaScript(
"this.attachCustomElement($0, $1, $2, $3);", getElement(),
tag, customNode.getId(), path);
});
}

private JsonArray getPath(com.vaadin.external.jsoup.nodes.Element element,
Expand Down Expand Up @@ -195,21 +268,20 @@ private void mapComponents(Class<?> cls) {
.filter(field -> !field.isSynthetic());

annotatedComponentFields
.forEach(field -> tryMapComponentOrElement(field, tagProvider,
.forEach(field -> tryMapComponentOrElement(field,
registeredCustomElements));
}

@SuppressWarnings("unchecked")
private void tryMapComponentOrElement(Field field,
Function<String, Optional<String>> tagProvider,
Map<String, Element> registeredCustomElements) {
Optional<Id> idAnnotation = AnnotationReader.getAnnotationFor(field,
Id.class);
if (!idAnnotation.isPresent()) {
return;
}
String id = idAnnotation.get().value();
Optional<String> tagName = tagProvider.apply(id);

Optional<String> tagName = getTagName(id);
if (!tagName.isPresent()) {
throw new IllegalStateException(String.format(
"There is no element with "
Expand All @@ -227,6 +299,20 @@ private void tryMapComponentOrElement(Field field,
}
}

private Optional<String> getTagName(String id) {
if (useCache) {
return parseData.apply(id);
} else {
Optional<String> tag = Optional
.ofNullable(contentElement.getElementById(id))
.map(com.vaadin.external.jsoup.nodes.Element::tagName);
if (tag.isPresent()) {
parseData.addTag(id, tag.get());
}
return tag;
}
}

private void injectServerSideElement(Element element, Field field) {
if (getElement().equals(element)) {
throw new IllegalArgumentException(
Expand Down Expand Up @@ -347,4 +433,10 @@ private void handleAttach(Element element, Field field) {
}
}

private void createSubTemplates() {
parseData.subTemplates
.forEach(data -> doRequestAttachCustomElement(data.id, data.tag,
data.path));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@

import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;

import com.vaadin.annotations.EventData;
import com.vaadin.annotations.EventHandler;
Expand All @@ -42,6 +43,8 @@
import com.vaadin.flow.dom.impl.BasicElementStateProvider;
import com.vaadin.flow.template.PolymerTemplate;
import com.vaadin.flow.template.model.TemplateModel;
import com.vaadin.server.DeploymentConfiguration;
import com.vaadin.server.VaadinService;

import elemental.json.Json;
import elemental.json.JsonObject;
Expand Down Expand Up @@ -117,7 +120,6 @@ private static Map<String, Method> getEventHandlerNamesAndMethods(
@SuppressWarnings("unchecked")
@Before
public void setUp() {

Collection<Class<? extends NodeFeature>> features = BasicElementStateProvider
.getFeatures();
stateNode = new StateNode(features.toArray(new Class[features.size()]));
Expand All @@ -128,6 +130,14 @@ public void setUp() {
CorrectAnnotationUsage.class);
wronglyAnnotatedHandlers = getEventHandlerNamesAndMethods(
WrongAnnotationUsage.class);

VaadinService service = Mockito.mock(VaadinService.class);
DeploymentConfiguration configuration = Mockito
.mock(DeploymentConfiguration.class);
Mockito.when(configuration.isProductionMode()).thenReturn(true);
Mockito.when(service.getDeploymentConfiguration())
.thenReturn(configuration);
VaadinService.setCurrent(service);
}

private void addAndVerifyMethod(Method method) {
Expand Down
Loading

0 comments on commit f7f52f1

Please sign in to comment.