1

used jersey mvc and jsp, all requests to html or js files did through @Template or Viewable. example;

  @GET
  @Path(JS_URL + "{type}")
  @Template(name = "grid")
  @Produces("application/javascript")
  public Response buildJSGrid(@DefaultValue("") @PathParam("type") String type) {
     Grid grid = new Grid(type);
....
     return Response.ok(grid).build();
  }

where grid is grid.jsp file with pure javascript inside

<%@ page contentType="application/javascript;charset=UTF-8" language="java" %>

.....

also possible other variant with html and js, example;

@GET
  @Path(FORM_URL + "{type}")
  @Template(name = "form")
  @Produces(MediaType.TEXT_HTML)
  public Response buildAccountForm(@DefaultValue("") @PathParam("type") String type) {
     Form form = new Form(type);
....
     return Response.ok(form).build();
  }

where form is form.jsp with html and js inside <script>..</script>

<%@ page contentType="text/html;charset=UTF-8" language="java" %>

...

i need to minify result js and html/js before send to client, i try to use https://code.google.com/archive/p/htmlcompressor/ lib, but there need to pass String to htmlCompressor.compress(input);

tried use WriterInterceptor

public class MinifyJsInterceptor implements WriterInterceptor {
  @Override
  public void aroundWriteTo(WriterInterceptorContext context) throws IOException, WebApplicationException {
final OutputStream outputStream = context.getOutputStream();
// here need to convert outputStream to InputStream and after to String ?
// result string to htmlCompressor.compress(resultString);
// after that convert result minify string back to resultOutputStream and set to context ?
context.setOutputStream(new GZIPOutputStream(resultOutputStream));

is it correct way ? and i can`t converts that outputstream to string thanks

--update

answer to questions; html + js mean that in some jsp are html markup and js code

    <div id="form" style="width: 500px; display: none">
      <div class="w2ui-page page-0">
        <div class="w2ui-field">
    </div>....

    <script type="text/javascript">
      var uiElement = (function () {
        var config = {
            onOpen: function (event) {
      event.onComplete = function () {
        $('#formContainer').w2render('form');
      }
     ...
    }());
    </script>

on client that file requested by

         $('#tempContainer').load('that file name - also dynamic', function (data, status, xhr) {
           uiElement.init();
           w2ui[layout].content(layout_main, w2ui[uiElement.name]);
         });

And do you really return js-files in you resource methods?

        some js and html + js files are dynamic build, example;
        grid.jsp contains inside

<%@ page contentType="application/javascript;charset=UTF-8" language="java" %>

          var uiElement = (function () {
            var config = {
              grid: {
                name: ${it.name},
                listUrl:'${it.entityListUrl}',
                formUrl:'${it.entityFormUrl}',
                columns: ${it.columns},
                records: ${it.records},           
}}

there are ${it..} values from el expression and setting in resource method

@GET
      @Path(JS_URL + "{type}")
      @Template(name = "grid")
      @Produces("application/javascript")
      public Response buildJSGrid(@DefaultValue("") @PathParam("type") String type) {
         Grid grid = new Grid(type);
    ....
         return Response.ok(grid).build();
      }}

and from client that js 'file' called by

         $.getScript('dynamic js file name' - it is dynamic too).done(function (script, status, xhr) {
           //console.log(xhr.responseText);
           uiElement.init();
           w2ui[layout].content(layout_main, w2ui[uiElement.name]);
         });



       also some html blocks build dynamic


{ 
     <c:if test="${it.recid != 0}">
          <div class="w2ui-field">
            <label>active:</label>
            <div>
              <input name="active" type="checkbox"/>
            </div>
          </div>
            </c:if>
}

-- update description, grid builder;

one resource and one template for build any grid,

  @GET
  @Path(GRID + "{type}")
  @Template(name = W2UI_VIEW_PREFIX + "grid/grid")
  @Produces(MEDIA_TYPE_APPLICATION_JAVASCRIPT)
  public Response buildGrid(@DefaultValue("") @PathParam("type") String type) {
    for (W2UI ui : W2UI.values()) {
      if (type.equals(ui.getName())) {
        W2UIElement grid = ui.getUI();
        return Response.ok(grid).build();
      }
    }
    return Response.noContent().build();
  }

also possible different templates(jsp files) through Viewable(template, model)

somewhere in menu builder for menu.jsp template

List<MenuItem> items..
MenuItem item1 = new MenuItem(W2UI.TASK_GRID, W2UIService.GRID);
items.add(item1);

where

W2UIService.GRID is string url for client js request and for server method resource @Path() anno.

and

public enum W2UI {
  TASK_GRID("task_grid", "tasks", Type.SCRIPT){
    @Override
    public W2UIElement getUI() {
      return new TaskGrid(getName());
    }
  },
.....
}

TaskGrid is filled model for grid.jsp template with js code, so easy to add any type of grid with different sets of data and buttons.

type of component(Type.SCRIPT) processing on the client by $.getScript(), Type.HTML by $('#tempContainer').load()

---update factory and providers;

@Provider
@Priority(200)
@HtmlMinify
public class HtmlMinifyInterceptor implements WriterInterceptor {
  @Inject private HtmlCompressor compressor;

...

public class HtmlMinifierFactory implements Factory<HtmlCompressor> {
  private HtmlCompressor compressor;

  @Override
  public HtmlCompressor provide() {
    if (null == compressor) compressor = new HtmlCompressor();
    ClosureJavaScriptCompressor jsCompressor = new ClosureJavaScriptCompressor();
    jsCompressor.setCompilationLevel(CompilationLevel.SIMPLE_OPTIMIZATIONS);

..

@ApplicationPath("/")
public class MainRsConfig extends ResourceConfig {
  public MainRsConfig() {
..
    register(new AbstractBinder() {
      @Override
      protected void configure() {
        bindFactory(HtmlMinifierFactory.class).to(HtmlCompressor.class).in(Singleton.class);
      }
    });
..
Mr Lister
  • 45,515
  • 15
  • 108
  • 150
nnn
  • 23
  • 9
  • Did you get an error when compressing js? Or is It simply not compessed? – Meiko Rachimow Apr 10 '16 at 17:36
  • updated my answer, 2 points to get it working: 1) add the yui compressor jar, 2) enable inline css / js compression on the compressor... – Meiko Rachimow Apr 10 '16 at 18:33
  • error is - SyntaxError: missing } after function body – nnn Apr 10 '16 at 18:40
  • yuicompressor already added; net.alchim31.maven yuicompressor-maven-plugin 1.5.1 – nnn Apr 10 '16 at 18:41
  • also i tried compressor.setJavaScriptCompressor(new ClosureJavaScriptCompressor()) and other examples from https://code.google.com/archive/p/htmlcompressor/ – nnn Apr 10 '16 at 18:53
  • and com.google.javascript closure-compiler v20160208 – nnn Apr 10 '16 at 19:00
  • update; in html + js file, html is compressed, and error java.lang.ClassNotFoundException: com.google.javascript.jscomp.JSSourceFile at org.glassfish.web.loader.WebappClassLoader.loadClass(WebappClassLoader.java:1795) at org.glassfish.web.loader.WebappClassLoader.loadClass(WebappClassLoader.java:1645) – nnn Apr 10 '16 at 19:08
  • on pure js file, error SyntaxError: missing } after function body – nnn Apr 10 '16 at 19:08
  • to compress pure js files, I assume you have to use the YUI-Compressor directly. You can write a JSMinifyOutputStream which uses the YUI-Compressor to compress the javascript. I read the HtmlCompressor documentation and I think the support only HTML with inline JS and CSS. – Meiko Rachimow Apr 10 '16 at 19:26
  • `yuicompressor-maven-plugin` I used another dependency, you tried the maven plugin, not the pure library (see my answer, which dependency I used) – Meiko Rachimow Apr 10 '16 at 19:31
  • For the google closure compiler I used another version and it worked for me: `com.google.javascriptclosure-compilerr2388` after setting the js and css compressor like that: `compressor.setJavaScriptCompressor(new ClosureJavaScriptCompressor()); compressor.setCompressJavaScript(true); compressor.setCssCompressor(new YuiCssCompressor()); compressor.setCompressCss(true);` – Meiko Rachimow Apr 10 '16 at 19:33
  • Now I added that information in the answer too. – Meiko Rachimow Apr 10 '16 at 19:40
  • closure-compiler r2388 worked well, last version 20160315 and others new versions get error. strange – nnn Apr 10 '16 at 20:01
  • ah yes, htmlcompressor seems a little bit outdated: http://mvnrepository.com/artifact/com.googlecode.htmlcompressor/htmlcompressor/1.5.2 (Last update: Oct 09, 2011) – Meiko Rachimow Apr 10 '16 at 20:05
  • In general, I would not use inline css/js if possible. Instead I would send dynamic data as JSON and edit the JavaScript to load this data ... But sure, If you have the time to do this... I know this projects where this is not possible, sadly ;) – Meiko Rachimow Apr 10 '16 at 20:13
  • css is not inline, about js and html.. if requested some component, for example account grid(account table) then response is js(or html + js) with already filled all vars end objects and on client just executed (that did with jersey jsp mvc @Template or Viewable), if user press load next 100 account list on that table, then response will be json. – nnn Apr 10 '16 at 20:44
  • yes, but maybe you could generate the initial html with your jsp and let javascript only change the data... the initial data is set with html... the dynamic data is loaded with ajax. This is an approach I used often. – Meiko Rachimow Apr 10 '16 at 20:47
  • this is 'main' idea, not to write additional logic on the client and minimize the work with dom on js, the client receives the data and the visual part as needed, as a component html + js or js + filled primary data. – nnn Apr 10 '16 at 20:53
  • yes, it is a working approach too - nevermind, wanted only to share my (off-topic) thoughts ;) – Meiko Rachimow Apr 10 '16 at 20:57
  • for example planned about 10 - 20 different grids, with different sets of columns and service buttons. access to various tables depend on current account rights, or many forms with different sets of inputs, one form has 5 inputs, other form has 15 input and tab navigation on it, availability forms depend on account too, if client receives only primary html and js and json on demand after, then on client will be a lot of js logic(mvc for all grids and forms and etc..) and templates for building these grids or forms. – nnn Apr 10 '16 at 21:11
  • client used http://w2ui.com js ui lib, way for constructing and filling js w2ui component on the server through jsp mvc, It seemed the most interesting – nnn Apr 10 '16 at 21:32
  • yes interesting library, did not know it... I think I understand your problem now. – Meiko Rachimow Apr 10 '16 at 21:47
  • added some description in question – nnn Apr 10 '16 at 22:30

1 Answers1

2

You can use a custom implementation of a ByteArrayOutputStream as a wrapper to the OutputStream of the WriterInterceptorContext:

import com.googlecode.htmlcompressor.compressor.Compressor;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;

public class HtmlMinifyOutputStream extends ByteArrayOutputStream {

    private OutputStream origOut;
    private Compressor compressor;

    public HtmlMinifyOutputStream(OutputStream origOut, Compressor compressor) {
        this.origOut = origOut;
        this.compressor = compressor;
    }

    public void close() throws IOException {
        super.close();
        String compressedBody = compressor.compress(new String(this.buf));
        this.origOut.write(compressedBody.getBytes());
        this.origOut.close();
    }
}

The HtmlMinifyOutputStream can be used in the WriterInterceptor implementation. The HtmlCompressor instance is injected:

import com.googlecode.htmlcompressor.compressor.Compressor;
import javax.inject.Inject;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.ext.Provider;
import javax.ws.rs.ext.WriterInterceptor;
import javax.ws.rs.ext.WriterInterceptorContext;
import java.io.*;

@Provider
@HtmlMinify
public class MinifyHtmlInterceptor implements WriterInterceptor {

    @Inject
    private Compressor compressor;

    @Override
    public void aroundWriteTo(WriterInterceptorContext context) throws IOException, WebApplicationException {
        final OutputStream outputStream = context.getOutputStream();
        context.setOutputStream(new HtmlMinifyOutputStream(outputStream, compressor));
        context.proceed();
    }
}

@HtmlMinify is a NameBinding annotation, used to activate the MinifyHtmlInterceptor on specific resource methods. (see https://jersey.java.net/documentation/latest/filters-and-interceptors.html#d0e9988):

import javax.ws.rs.NameBinding;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@NameBinding
@Retention(value = RetentionPolicy.RUNTIME)
public @interface HtmlMinify {}

The HtmlCompressor can be created only once per application and used concurrently, because:

HtmlCompressor and XmlCompressor classes are considered thread safe* and can be used in multi-thread environment
(https://code.google.com/archive/p/htmlcompressor/)

Here is a HK2 factory (see: Implementing Custom Injection Provider) which creates the compressor instance and enables inline css and javascript compression:

import com.googlecode.htmlcompressor.compressor.Compressor;
import com.googlecode.htmlcompressor.compressor.HtmlCompressor;
import org.glassfish.hk2.api.Factory;

public class HtmlCompressorFactory implements Factory<Compressor> {

    private HtmlCompressor compressor;

    @Override
    public Compressor provide() {
        if(compressor == null) {
            compressor = new HtmlCompressor();
        }
        compressor.setCompressJavaScript(true);
        compressor.setCompressCss(true);
        return compressor;
    }

    @Override
    public void dispose(Compressor compressor) {}
}

The factory is registered with an AbstractBinder:

final ResourceConfig rc = new ResourceConfig().packages("com.example");
rc.register(new AbstractBinder() {
    @Override
    protected void configure() {
        bindFactory(HtmlCompressorFactory.class).to(Compressor.class).in(Singleton.class);
    }
});

If inline javascript or inline css compression is enabled:

HTML compressor with default settings doesn't require any dependencies.
Inline CSS compression requires YUI compressor library.
Inline JavaScript compression requires either YUI compressor library (by default) or Google Closure Compiler library.
(https://code.google.com/archive/p/htmlcompressor/)

I use maven, so I added this dependency to my pom.xml:

<dependency>
    <groupId>com.yahoo.platform.yui</groupId>
    <artifactId>yuicompressor</artifactId>
    <version>2.4.8</version>
</dependency>

If you want to use the Google Closure Compiler use this dependency:

<dependency>
    <groupId>com.google.javascript</groupId>
    <artifactId>closure-compiler</artifactId>
    <version>r2388</version>
</dependency>

and activate it:

compressor.setJavaScriptCompressor(new ClosureJavaScriptCompressor());
compressor.setCompressJavaScript(true);
compressor.setCssCompressor(new YuiCssCompressor());
compressor.setCompressCss(true);
return compressor;

If you want to compress pure JavaScript or CSS files, you cannot use the htmlcompressor. This library supports only HTML files with inline CSS/JS. But you could implement a MinifyJsInterceptor or MinifyCssInterceptor analog to the MinifyHtmlInterceptor, which uses the YUI-Compressor and/or Google Closure libraries directly.

For gzip compression you should implement another interceptor. So it is possible to configure the minification and compression separately. If you activate multiple interceptors, use javax.annotation.Priority to controll the order of execution. (see: https://jersey.java.net/documentation/latest/filters-and-interceptors.html#d0e9927)

Meiko Rachimow
  • 4,664
  • 2
  • 25
  • 43
  • htmlcompressor not work correctly on html + js files, but it is another question, thanks for OutputStream wrapper – nnn Apr 10 '16 at 12:42
  • I tried it only with html, there was no problem. What do you mean with "html + js files"? And do you really return js-files in you resource methods? – Meiko Rachimow Apr 10 '16 at 12:59
  • with gzip no problem in one WriterInterceptor, this way; context.setOutputStream(new GZIPOutputStream(new MinifyOutputStream(outputStream))); – nnn Apr 10 '16 at 18:45
  • yes, but it is better to use 2 Interceptors... the gzip interceptor could be enabled per default... the html interceptor should not enabled for json... thats why i would split this – Meiko Rachimow Apr 10 '16 at 18:58
  • Hi @user2728553 if this answer has solved your question please consider [accepting it](http://meta.stackexchange.com/q/5234/179419) by clicking the check-mark. This indicates to the wider community that you've found a solution and gives some reputation to both the answerer and yourself. There is no obligation to do this. – Meiko Rachimow Apr 11 '16 at 05:10
  • all worked ok, except factory and custom Injection, get error on deploy Caused by: org.jboss.weld.exceptions.DeploymentException: WELD-001408: Unsatisfied dependencies for type HtmlCompressor with qualifiers @Default at injection point [BackedAnnotatedField] Inject private gds.web.minifiers.HtmlMinifyInterceptor.compressor – nnn Apr 11 '16 at 21:16
  • Ok you use a JBoss server with weld... maybe you could try an answer from here: http://stackoverflow.com/questions/17224270/how-to-enable-cdi-inject-in-web-service-jaxrs-jersey-on-java-se-running-grizzl – Meiko Rachimow Apr 11 '16 at 21:25
  • The link above is for integrating hk2 with weld (jersey brings his own DI framework) or you could rewrite the factory in my answer to CDI ... – Meiko Rachimow Apr 11 '16 at 21:26
  • i use payara.fish, it`s glassfish 4 – nnn Apr 11 '16 at 21:27
  • use creation of specific compressor in each Interceptors, just try to use factory – nnn Apr 11 '16 at 21:30
  • yes ok... not the jboss server but you use WELD (the CDI impl.). I know some difficulties with the integration of DI frameworks like (Spring, Weld or Guice) with HK2 of jersey... I think the link should go into the right direction. – Meiko Rachimow Apr 11 '16 at 21:40
  • or possible i`ll try disable WELD and use HK2 instead, but in project also used ejb3 beans – nnn Apr 11 '16 at 21:55