package com.google.sitebricks; import java.lang.annotation.Annotation; import java.util.Enumeration; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Properties; import java.util.ResourceBundle; import java.util.Set; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.google.inject.AbstractModule; import com.google.inject.Key; import com.google.inject.Scope; import com.google.inject.TypeLiteral; import com.google.inject.binder.ScopedBindingBuilder; import com.google.inject.multibindings.Multibinder; import com.google.sitebricks.compiler.FlatTemplateCompiler; import com.google.sitebricks.compiler.HtmlTemplateCompiler; import com.google.sitebricks.compiler.Parsing; import com.google.sitebricks.compiler.TemplateCompiler; import com.google.sitebricks.compiler.template.MvelTemplateCompiler; import com.google.sitebricks.compiler.template.freemarker.FreemarkerTemplateCompiler; import com.google.sitebricks.compiler.template.jsp.JspTemplateCompiler; import com.google.sitebricks.conversion.Converter; import com.google.sitebricks.conversion.ConverterUtils; import com.google.sitebricks.core.CaseWidget; import com.google.sitebricks.headless.Service; import com.google.sitebricks.http.Delete; import com.google.sitebricks.http.Get; import com.google.sitebricks.http.Head; import com.google.sitebricks.http.Patch; import com.google.sitebricks.http.Post; import com.google.sitebricks.http.Put; import com.google.sitebricks.http.Trace; import com.google.sitebricks.http.negotiate.Accept; import com.google.sitebricks.http.negotiate.Negotiation; import com.google.sitebricks.rendering.Strings; import com.google.sitebricks.routing.Action; /** * @author dhanji@gmail.com (Dhanji R. Prasanna) */ public class SitebricksModule extends AbstractModule implements PageBinder { private boolean enableServletSupport = true; protected void enableServletSupport(boolean bindServlets) { this.enableServletSupport = bindServlets; } // Configure defaults via this contructor. public SitebricksModule() { // By default these are the method annotations we dispatch against. // They can be overridden with custom annotation types. methods.put("get", Get.class); methods.put("post", Post.class); methods.put("put", Put.class); methods.put("patch", Patch.class); methods.put("delete", Delete.class); methods.put("head", Head.class); methods.put("trace", Trace.class); } @Override protected final void configure() { // Re-route all requests through sitebricks. install(servletModule()); if (enableServletSupport) install(new SitebricksServletSupportModule()); install(new SitebricksInternalModule()); // negotiations stuff (make sure we clean this up). negotiate("Accept").with(Accept.class); //TODO: yes this is not so nice, but will keep on trying to localize the converter code. jvz. converters = Multibinder.newSetBinder(binder(), Converter.class); // TODO remove when more of sitebricks internals is guiced requestStaticInjection(Parsing.class); // Call down to the implementation. configureSitebricks(); // These need to be registered after configureSitebricks because contributions made to the Multibinder // must be allowed to register before all the defaults are registered. In the acceptance tests where the // date format is non-default it tests will fail if the Multibinder is created and the default converters // registered immediately afterward. jvz. /* converters = */ConverterUtils.createConverterMultibinder(converters); //insert core widgets set packages.add(0, CaseWidget.class.getPackage()); bind(new TypeLiteral<List<Package>>() {}) .annotatedWith(Bricks.class) .toInstance(packages); bind(new TypeLiteral<List<LinkingBinder>>() {}) .annotatedWith(Bricks.class) .toInstance(bindings); // These are the HTTP methods that we listen for. bind(new TypeLiteral<Map<String, Class<? extends Annotation>>>() {}) .annotatedWith(Bricks.class) .toInstance(methods); // These are Content negotiation annotations. bind(new TypeLiteral<Map<String, Class<? extends Annotation>>>() {}) .annotatedWith(Negotiation.class) .toInstance(negotiations); Localizer.localizeAll(binder(), localizations); bind(new TypeLiteral<Map<Class<?>, Map<Locale, Localizer.Localization>>>() {}) .toInstance(localizationsMap); configureTemplateSystem(); } protected void configureTemplateSystem() { // // Map of all the implementations keyed by type they can handle // ImmutableMap.Builder<String, Class<? extends TemplateCompiler>> builder = ImmutableMap.builder(); builder.put("html", HtmlTemplateCompiler.class); builder.put("xhtml", HtmlTemplateCompiler.class); builder.put("flat", FlatTemplateCompiler.class); builder.put("mvel", MvelTemplateCompiler.class); builder.put("fml", FreemarkerTemplateCompiler.class); builder.put("jsp", JspTemplateCompiler.class); configureTemplateCompilers(builder); Map<String, Class<? extends TemplateCompiler>> map = builder.build(); bind(new TypeLiteral<Map<String, Class<? extends TemplateCompiler>>>() {}).toInstance(map); } protected void configureTemplateCompilers(ImmutableMap.Builder<String, Class<? extends TemplateCompiler>> compilers) { // Override to include custom template compilers: // compilers.put("mustache", MustacheTemplateCompiler.class); // Sitebricks obtains the provided compiler class via Guice. } /** * Optionally supply {@link javax.servlet.Servlet} and/or {@link javax.servlet.Filter} implementations to * Guice Servlet. See {@link com.google.sitebricks.SitebricksServletModule} for usage examples. * * @see com.google.sitebricks.SitebricksServletModule * * @return An instance of {@link com.google.sitebricks.SitebricksServletModule}. Implementing classes * must return a non-null value. */ protected SitebricksServletModule servletModule() { return new SitebricksServletModule(); } protected void configureSitebricks() { } // Bindings. private final List<LinkingBinder> bindings = Lists.newArrayList(); private final List<Package> packages = Lists.newArrayList(); private final Map<String, Class<? extends Annotation>> methods = Maps.newHashMap(); private final Map<String, Class<? extends Annotation>> negotiations = Maps.newHashMap(); private final Set<Localizer.Localization> localizations = Sets.newHashSet(); private final Map<Class<?>, Map<Locale, Localizer.Localization>> localizationsMap = Maps.newHashMap(); public final ShowBinder at(String uri) { LinkingBinder binding = new LinkingBinder(uri); bindings.add(binding); return binding; } public final EmbedAsBinder embed(Class<?> clazz) { LinkingBinder binding = new LinkingBinder(clazz); bindings.add(binding); return binding; } public final void bindMethod(String method, Class<? extends Annotation> annotation) { Strings.nonEmpty(method, "The REST method must be a valid non-empty string"); Preconditions.checkArgument(null != annotation); String methodNormal = method.toLowerCase(); methods.put(methodNormal, annotation); } public NegotiateWithBinder negotiate(final String header) { Strings.nonEmpty(header, "invalid request header string for negotiation."); return new NegotiateWithBinder() { public void with(Class<? extends Annotation> ann) { Preconditions.checkArgument(null != ann); negotiations.put(header, ann); } }; } public LocalizationBinder localize(final Class<?> iface) { Preconditions.checkArgument(iface.isInterface(), "localize() accepts an interface type only"); add(Localizer.defaultLocalizationFor(iface)); return new LocalizationBinder() { public void using(Locale locale, Map<String, String> messages) { add( new Localizer.Localization(iface, locale, messages)); } public void using(Locale locale, Properties properties) { Preconditions.checkArgument(null != properties, "Must provide a non-null resource bundle"); // A Properties object is always of type string/string @SuppressWarnings({ "unchecked", "rawtypes" }) Map<String, String> messages = (Map) properties; add(new Localizer.Localization(iface, locale, messages)); } public void using(Locale locale, ResourceBundle bundle) { Preconditions.checkArgument(null != bundle, "Must provide a non-null resource bundle"); Map<String, String> messages = Maps.newHashMap(); Enumeration<String> keys = bundle.getKeys(); while (keys.hasMoreElements()) { String key = keys.nextElement(); messages.put(key, bundle.getString(key)); } add(new Localizer.Localization(iface, locale, messages)); } public void usingDefault() { add(Localizer.defaultLocalizationFor(iface)); } }; } private void add(Localizer.Localization localization) { localizations.add(localization); Map<Locale, Localizer.Localization> localeLocalizer = localizationsMap.get(localization.getClazz()); if (localeLocalizer == null) { localeLocalizer = Maps.newHashMap(); localizationsMap.put(localization.getClazz(), localeLocalizer); } localeLocalizer.put(localization.getLocale(), localization); } protected final void scan(Package pack) { Preconditions.checkArgument(null != pack, "Package parameter to scan() cannot be null"); packages.add(pack); } static enum BindingKind { EMBEDDED, PAGE, SERVICE, STATIC_RESOURCE, ACTION } class LinkingBinder implements ShowBinder, ScopedBindingBuilder, EmbedAsBinder { BindingKind bindingKind; String embedAs; final String uri; Class<?> pageClass; private String resource; private boolean asEagerSingleton; private Class<? extends Annotation> scopeAnnotation; private Scope scope; List<ActionDescriptor> actionDescriptors = Lists.newArrayList(); public LinkingBinder(String uri) { this.uri = uri; this.pageClass = null; this.bindingKind = BindingKind.PAGE; } public LinkingBinder(Class<?> pageClass) { this.pageClass = pageClass; this.uri = null; this.bindingKind = BindingKind.EMBEDDED; } Export getResource() { return new Export() { public String at() { return uri; } public String resource() { return resource; } public Class<? extends Annotation> annotationType() { return Export.class; } }; } public ScopedBindingBuilder show(Class<?> clazz) { Preconditions.checkArgument(!clazz.isAnnotationPresent(Service.class), "Cannot show() a headless web service. Did you mean to call serve() instead?"); this.pageClass = clazz; this.bindingKind = BindingKind.PAGE; return this; } public ScopedBindingBuilder serve(Class<?> clazz) { this.pageClass = clazz; this.bindingKind = BindingKind.SERVICE; return this; } public void export(String glob) { resource = glob; this.bindingKind = BindingKind.STATIC_RESOURCE; } public ActionBinder perform(Action action) { this.bindingKind = BindingKind.ACTION; ActionDescriptor ad = new ActionDescriptor(action, this); actionDescriptors.add(ad); return ad; } public ActionBinder perform(Class<? extends Action> action) { this.bindingKind = BindingKind.ACTION; ActionDescriptor ad = new ActionDescriptor(Key.get(action), this); actionDescriptors.add(ad); return ad; } public ActionBinder perform(Key<? extends Action> action) { this.bindingKind = BindingKind.ACTION; ActionDescriptor ad = new ActionDescriptor(action, this); actionDescriptors.add(ad); return ad; } public ScopedBindingBuilder as(String annotation) { this.embedAs = annotation; return this; } public void in(Class<? extends Annotation> scopeAnnotation) { Preconditions.checkArgument(null == scope); Preconditions.checkArgument(!asEagerSingleton); this.scopeAnnotation = scopeAnnotation; } public void in(Scope scope) { Preconditions.checkArgument(null == scopeAnnotation); Preconditions.checkArgument(!asEagerSingleton); this.scope = scope; } public void asEagerSingleton() { Preconditions.checkArgument(null == scopeAnnotation); Preconditions.checkArgument(null == scope); this.asEagerSingleton = true; } } // // Converters // @SuppressWarnings("rawtypes") private Multibinder<Converter> converters; public final void converter(Converter<?, ?> converter) { Preconditions.checkArgument(null != converter, "Type converters cannot be null"); converters.addBinding().toInstance(converter); } public final void converter(Class<? extends Converter<?, ?>> clazz) { converters.addBinding().to(clazz); } }