Thursday, February 19, 2015

Creating a simple Streaming File Server with JAVA

Let's start by saying that we use the technology of servlets for our implementation. The whole problem started went we need to do a web application server that would display multimedia content (audio and video) and after several hours searching in google, the solution is to use some streaming server or implement it yourself (this is always the last option).

I finally found an implementation of a servlet, this site has the class and also makes a wide explanation for better understanding read the article. All the job is made by the servlet, here is the code:

package org.onedevelopment.servlet;

/*
 * net/balusc/webapp/FileServlet.java
 *
 * Copyright (C) 2009 BalusC
 *
 * This program is free software: you can redistribute it and/or modify it under the terms of the
 * GNU Lesser General Public License as published by the Free Software Foundation, either version 3
 * of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
 * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License along with this library.
 * If not, see <http://www.gnu.org/licenses/>.
 */

import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.zip.GZIPOutputStream;

import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * A file servlet supporting resume of downloads and client-side caching and GZIP of text content.
 * This servlet can also be used for images, client-side caching would become more efficient.
 * This servlet can also be used for text files, GZIP would decrease network bandwidth.
 *
 * @author BalusC
 * @link http://balusc.blogspot.com/2009/02/fileservlet-supporting-resume-and.html
 */
public class FileServlet extends HttpServlet {

    // Constants ----------------------------------------------------------------------------------

    /**
  * 
  */
 private static final long serialVersionUID = 1L;
 private static final int DEFAULT_BUFFER_SIZE = 10240; // ..bytes = 10KB.
    private static final long DEFAULT_EXPIRE_TIME = 604800000L; // ..ms = 1 week.
    private static final String MULTIPART_BOUNDARY = "MULTIPART_BYTERANGES";

    // Properties ---------------------------------------------------------------------------------

    private String basePath;

    // Actions ------------------------------------------------------------------------------------

    /**
     * Initialize the servlet.
     * @see HttpServlet#init().
     */
    public void init() throws ServletException {

        // Get base path (path to get all resources from) as init parameter.
        this.basePath = getInitParameter("basePath");

        // Validate base path.
        if (this.basePath == null) {
            throw new ServletException("FileServlet init param 'basePath' is required.");
        } else {
            File path = new File(this.basePath);
            if (!path.exists()) {
                throw new ServletException("FileServlet init param 'basePath' value '"
                    + this.basePath + "' does actually not exist in file system.");
            } else if (!path.isDirectory()) {
                throw new ServletException("FileServlet init param 'basePath' value '"
                    + this.basePath + "' is actually not a directory in file system.");
            } else if (!path.canRead()) {
                throw new ServletException("FileServlet init param 'basePath' value '"
                    + this.basePath + "' is actually not readable in file system.");
            }
        }
    }

    /**
     * Process HEAD request. This returns the same headers as GET request, but without content.
     * @see HttpServlet#doHead(HttpServletRequest, HttpServletResponse).
     */
    protected void doHead(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException
    {
        // Process request without content.
        processRequest(request, response, false);
    }

    /**
     * Process GET request.
     * @see HttpServlet#doGet(HttpServletRequest, HttpServletResponse).
     */
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException
    {
        // Process request with content.
        processRequest(request, response, true);
    }

    /**
     * Process the actual request.
     * @param request The request to be processed.
     * @param response The response to be created.
     * @param content Whether the request body should be written (GET) or not (HEAD).
     * @throws IOException If something fails at I/O level.
     */
    private void processRequest
        (HttpServletRequest request, HttpServletResponse response, boolean content)
            throws IOException
    {
        // Validate the requested file ------------------------------------------------------------

        // Get requested file by path info.
        String requestedFile = request.getPathInfo();

        // Check if file is actually supplied to the request URL.
        if (requestedFile == null) {
            // Do your thing if the file is not supplied to the request URL.
            // Throw an exception, or send 404, or show default/warning page, or just ignore it.
            response.sendError(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        // URL-decode the file name (might contain spaces and on) and prepare file object.
        File file = new File(basePath, URLDecoder.decode(requestedFile, "UTF-8"));

        // Check if file actually exists in filesystem.
        if (!file.exists()) {
            // Do your thing if the file appears to be non-existing.
            // Throw an exception, or send 404, or show default/warning page, or just ignore it.
            response.sendError(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        // Prepare some variables. The ETag is an unique identifier of the file.
        String fileName = file.getName();
        long length = file.length();
        long lastModified = file.lastModified();
        String eTag = fileName + "_" + length + "_" + lastModified;
        long expires = System.currentTimeMillis() + DEFAULT_EXPIRE_TIME;


        // Validate request headers for caching ---------------------------------------------------

        // If-None-Match header should contain "*" or ETag. If so, then return 304.
        String ifNoneMatch = request.getHeader("If-None-Match");
        if (ifNoneMatch != null && matches(ifNoneMatch, eTag)) {
            response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
            response.setHeader("ETag", eTag); // Required in 304.
            response.setDateHeader("Expires", expires); // Postpone cache with 1 week.
            return;
        }

        // If-Modified-Since header should be greater than LastModified. If so, then return 304.
        // This header is ignored if any If-None-Match header is specified.
        long ifModifiedSince = request.getDateHeader("If-Modified-Since");
        if (ifNoneMatch == null && ifModifiedSince != -1 && ifModifiedSince + 1000 > lastModified) {
            response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
            response.setHeader("ETag", eTag); // Required in 304.
            response.setDateHeader("Expires", expires); // Postpone cache with 1 week.
            return;
        }


        // Validate request headers for resume ----------------------------------------------------

        // If-Match header should contain "*" or ETag. If not, then return 412.
        String ifMatch = request.getHeader("If-Match");
        if (ifMatch != null && !matches(ifMatch, eTag)) {
            response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
            return;
        }

        // If-Unmodified-Since header should be greater than LastModified. If not, then return 412.
        long ifUnmodifiedSince = request.getDateHeader("If-Unmodified-Since");
        if (ifUnmodifiedSince != -1 && ifUnmodifiedSince + 1000 <= lastModified) {
            response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
            return;
        }


        // Validate and process range -------------------------------------------------------------

        // Prepare some variables. The full Range represents the complete file.
        Range full = new Range(0, length - 1, length);
        List<Range> ranges = new ArrayList<Range>();

        // Validate and process Range and If-Range headers.
        String range = request.getHeader("Range");
        if (range != null) {

            // Range header should match format "bytes=n-n,n-n,n-n...". If not, then return 416.
            if (!range.matches("^bytes=\\d*-\\d*(,\\d*-\\d*)*$")) {
                response.setHeader("Content-Range", "bytes */" + length); // Required in 416.
                response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
                return;
            }

            // If-Range header should either match ETag or be greater then LastModified. If not,
            // then return full file.
            String ifRange = request.getHeader("If-Range");
            if (ifRange != null && !ifRange.equals(eTag)) {
                try {
                    long ifRangeTime = request.getDateHeader("If-Range"); // Throws IAE if invalid.
                    if (ifRangeTime != -1 && ifRangeTime + 1000 < lastModified) {
                        ranges.add(full);
                    }
                } catch (IllegalArgumentException ignore) {
                    ranges.add(full);
                }
            }

            // If any valid If-Range header, then process each part of byte range.
            if (ranges.isEmpty()) {
                for (String part : range.substring(6).split(",")) {
                    // Assuming a file with length of 100, the following examples returns bytes at:
                    // 50-80 (50 to 80), 40- (40 to length=100), -20 (length-20=80 to length=100).
                    long start = sublong(part, 0, part.indexOf("-"));
                    long end = sublong(part, part.indexOf("-") + 1, part.length());

                    if (start == -1) {
                        start = length - end;
                        end = length - 1;
                    } else if (end == -1 || end > length - 1) {
                        end = length - 1;
                    }

                    // Check if Range is syntactically valid. If not, then return 416.
                    if (start > end) {
                        response.setHeader("Content-Range", "bytes */" + length); // Required in 416.
                        response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
                        return;
                    }

                    // Add range.
                    ranges.add(new Range(start, end, length));
                }
            }
        }


        // Prepare and initialize response --------------------------------------------------------

        // Get content type by file name and set default GZIP support and content disposition.
        String contentType = getServletContext().getMimeType(fileName);
        boolean acceptsGzip = false;
        String disposition = "inline";

        // If content type is unknown, then set the default value.
        // For all content types, see: http://www.w3schools.com/media/media_mimeref.asp
        // To add new content types, add new mime-mapping entry in web.xml.
        if (contentType == null) {
            contentType = "application/octet-stream";
        }

        // If content type is text, then determine whether GZIP content encoding is supported by
        // the browser and expand content type with the one and right character encoding.
        if (contentType.startsWith("text")) {
            String acceptEncoding = request.getHeader("Accept-Encoding");
            acceptsGzip = acceptEncoding != null && accepts(acceptEncoding, "gzip");
            contentType += ";charset=UTF-8";
        } 

        // Else, expect for images, determine content disposition. If content type is supported by
        // the browser, then set to inline, else attachment which will pop a 'save as' dialogue.
        else if (!contentType.startsWith("image")) {
            String accept = request.getHeader("Accept");
            disposition = accept != null && accepts(accept, contentType) ? "inline" : "attachment";
        }

        // Initialize response.
        response.reset();
        response.setBufferSize(DEFAULT_BUFFER_SIZE);
        response.setHeader("Content-Disposition", disposition + ";filename=\"" + fileName + "\"");
        response.setHeader("Accept-Ranges", "bytes");
        response.setHeader("ETag", eTag);
        response.setDateHeader("Last-Modified", lastModified);
        response.setDateHeader("Expires", expires);


        // Send requested file (part(s)) to client ------------------------------------------------

        // Prepare streams.
        RandomAccessFile input = null;
        OutputStream output = null;

        try {
            // Open streams.
            input = new RandomAccessFile(file, "r");
            output = response.getOutputStream();

            if (ranges.isEmpty() || ranges.get(0) == full) {

                // Return full file.
                Range r = full;
                response.setContentType(contentType);
                response.setHeader("Content-Range", "bytes " + r.start + "-" + r.end + "/" + r.total);

                if (content) {
                    if (acceptsGzip) {
                        // The browser accepts GZIP, so GZIP the content.
                        response.setHeader("Content-Encoding", "gzip");
                        output = new GZIPOutputStream(output, DEFAULT_BUFFER_SIZE);
                    } else {
                        // Content length is not directly predictable in case of GZIP.
                        // So only add it if there is no means of GZIP, else browser will hang.
                        response.setHeader("Content-Length", String.valueOf(r.length));
                    }

                    // Copy full range.
                    copy(input, output, r.start, r.length);
                }

            } else if (ranges.size() == 1) {

                // Return single part of file.
                Range r = ranges.get(0);
                response.setContentType(contentType);
                response.setHeader("Content-Range", "bytes " + r.start + "-" + r.end + "/" + r.total);
                response.setHeader("Content-Length", String.valueOf(r.length));
                response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); // 206.

                if (content) {
                    // Copy single part range.
                    copy(input, output, r.start, r.length);
                }

            } else {

                // Return multiple parts of file.
                response.setContentType("multipart/byteranges; boundary=" + MULTIPART_BOUNDARY);
                response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); // 206.

                if (content) {
                    // Cast back to ServletOutputStream to get the easy println methods.
                    ServletOutputStream sos = (ServletOutputStream) output;

                    // Copy multi part range.
                    for (Range r : ranges) {
                        // Add multipart boundary and header fields for every range.
                        sos.println();
                        sos.println("--" + MULTIPART_BOUNDARY);
                        sos.println("Content-Type: " + contentType);
                        sos.println("Content-Range: bytes " + r.start + "-" + r.end + "/" + r.total);

                        // Copy single part range of multi part range.
                        copy(input, output, r.start, r.length);
                    }

                    // End with multipart boundary.
                    sos.println();
                    sos.println("--" + MULTIPART_BOUNDARY + "--");
                }
            }
        } finally {
            // Gently close streams.
            close(output);
            close(input);
        }
    }

    // Helpers (can be refactored to public utility class) ----------------------------------------

    /**
     * Returns true if the given accept header accepts the given value.
     * @param acceptHeader The accept header.
     * @param toAccept The value to be accepted.
     * @return True if the given accept header accepts the given value.
     */
    private static boolean accepts(String acceptHeader, String toAccept) {
        String[] acceptValues = acceptHeader.split("\\s*(,|;)\\s*");
        Arrays.sort(acceptValues);
        return Arrays.binarySearch(acceptValues, toAccept) > -1
            || Arrays.binarySearch(acceptValues, toAccept.replaceAll("/.*$", "/*")) > -1
            || Arrays.binarySearch(acceptValues, "*/*") > -1;
    }

    /**
     * Returns true if the given match header matches the given value.
     * @param matchHeader The match header.
     * @param toMatch The value to be matched.
     * @return True if the given match header matches the given value.
     */
    private static boolean matches(String matchHeader, String toMatch) {
        String[] matchValues = matchHeader.split("\\s*,\\s*");
        Arrays.sort(matchValues);
        return Arrays.binarySearch(matchValues, toMatch) > -1
            || Arrays.binarySearch(matchValues, "*") > -1;
    }

    /**
     * Returns a substring of the given string value from the given begin index to the given end
     * index as a long. If the substring is empty, then -1 will be returned
     * @param value The string value to return a substring as long for.
     * @param beginIndex The begin index of the substring to be returned as long.
     * @param endIndex The end index of the substring to be returned as long.
     * @return A substring of the given string value as long or -1 if substring is empty.
     */
    private static long sublong(String value, int beginIndex, int endIndex) {
        String substring = value.substring(beginIndex, endIndex);
        return (substring.length() > 0) ? Long.parseLong(substring) : -1;
    }

    /**
     * Copy the given byte range of the given input to the given output.
     * @param input The input to copy the given range to the given output for.
     * @param output The output to copy the given range from the given input for.
     * @param start Start of the byte range.
     * @param length Length of the byte range.
     * @throws IOException If something fails at I/O level.
     */
    private static void copy(RandomAccessFile input, OutputStream output, long start, long length)
        throws IOException
    {
        byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
        int read;

        if (input.length() == length) {
            // Write full range.
            while ((read = input.read(buffer)) > 0) {
                output.write(buffer, 0, read);
            }
        } else {
            // Write partial range.
            input.seek(start);
            long toRead = length;

            while ((read = input.read(buffer)) > 0) {
                if ((toRead -= read) > 0) {
                    output.write(buffer, 0, read);
                } else {
                    output.write(buffer, 0, (int) toRead + read);
                    break;
                }
            }
        }
    }

    /**
     * Close the given resource.
     * @param resource The resource to be closed.
     */
    private static void close(Closeable resource) {
        if (resource != null) {
            try {
                resource.close();
            } catch (IOException ignore) {
                // Ignore IOException. If you want to handle this anyway, it might be useful to know
                // that this will generally only be thrown when the client aborted the request.
            }
        }
    }

    // Inner classes ------------------------------------------------------------------------------

    /**
     * This class represents a byte range.
     */
    protected class Range {
        long start;
        long end;
        long length;
        long total;

        /**
         * Construct a byte range.
         * @param start Start of the byte range.
         * @param end End of the byte range.
         * @param total Total length of the byte source.
         */
        public Range(long start, long end, long total) {
            this.start = start;
            this.end = end;
            this.length = end - start + 1;
            this.total = total;
        }

    }

}

Of course it is important to define in the web.xml our servlet:


<servlet>
 <servlet-name>fileServlet</servlet-name>
 <servlet-class>org.onedevelopment.servlet.FileServlet</servlet-class>
 <init-param>
  <param-name>basePath</param-name>
  <param-value>c:/Temp</param-value>
 </init-param>
</servlet>
<servlet-mapping>
 <servlet-name>fileServlet</servlet-name>
 <url-pattern>/files/*</url-pattern>
</servlet-mapping>


The most important point here is to define the physical path of the multimedia files, in the example is "c:/temp", This is the path to where the servlet will look for files to load and return the output to the client.


<param-value>c:/Temp</param-value>
   
On the client we can request the media files in the file "index.jsp" in this way, using the multimedia tag of HTML5:



<video width="320" height="240" controls>
  <source src="files/music.mp3" type="audio/mpeg">
  Your browser does not support the audio element.
</video>
<br/>
<video width="620" height="440" controls>
  <source src="files/video.mp4" type="video/mp4">
  Your browser does not support the audio element.
</video>


Finally the result would be something like this:


I hope this help, the codes can be downloaded here.

Sunday, December 28, 2014

Appfuse, customise pagination like Bootstrap pagination using DisplayTag

As in the previous post we saw how to update to version 3.3.1 of Bootstrap in our application with AppFuse, in this post I want to share with you how to customize paging used in the default application generated by AppFuse.

First we have to start by saying that the generated application with AppFuse used to display lists of data in tables is a library called "DisplayTags" if you want to learn more about DisplayTags you can go to the documentation on the home page. By default DisplayTags has a style like this one shown below:



Thanks to the AppFuse development team, they did a great job including in the skeleton generated application styles like Bootstrap 3, and this is the result obtained:



In this article we want to share and show DisplayTags paging component like Bootstrap style. For  doing this we need to go to the file "displaytag.properties" found in the Web module of our application in this path "web/src/main/resources/displaytag.properties" It is the content:


# For a list of settings you can customize, see
# http://displaytag.sourceforge.net/configuration.html

basic.empty.showtable=true
paging.banner.onepage=
export.banner=
export.pdf=true

This file is used by DisplayTags for configuration and personalization, in our case we want to show the paging component like Bootstrap paging, then we add this new configuration:

paging.banner.placement=top
paging.banner.no_items_found=<span class="pagebanner">No {0} found.</span>
paging.banner.one_item_found=<span class="pagebanner">One {0} found.</span>
paging.banner.all_items_found=<span class="pagebanner">{0} {1} found, displaying all {2}.</span>
paging.banner.some_items_found=<span class="pagebanner">{0} {1} found, displaying {2} to {3}.</span>

paging.banner.full=<nav><ul class="pagination"><li><a href="{1}"><span aria-hidden="true">&laquo;</span><span class="sr-only">Previous</span></a></li>{0}<li><a href="{4}"><span aria-hidden="true">&raquo;</span><span class="sr-only">Next</span></a></li></ul></nav>
#<nav><div class="pagination"><ul><li class="disabled"><a href="{1}"><span aria-hidden="true">&laquo;</span><span class="sr-only">Previous</span></a></li><li class="prev disabled"><a href="{2}" class="next" title="previous"></a></li>{0}<li><a href="{3}" class="next" title="next"></li><li class="next"><a href="{4}" class="next" title="lest"></li></ul></div></nav>
paging.banner.first=<nav><ul class="pagination"><li class="disabled"><a href="{1}"><span aria-hidden="true">&laquo;</span><span class="sr-only">Previous</span></a></li>{0}<li><a href="{4}"><span aria-hidden="true">&raquo;</span><span class="sr-only">Next</span></a></li></ul></nav>
#<nav><div class="pagination"><ul><li class="prev"><a href="{1}" class="next" title="first"></a></li><li class="prev"><a href="{2}" class="next" title="previous"></li>{0}<li><a href="{3}" class="next" title="next"></a></li><li class="next"><a href="{4}" class="next" title="lest"></li></ul></div></nav>
paging.banner.last=<nav><ul class="pagination"><li class="disabled"><a href="{1}"><span aria-hidden="true">&laquo;</span><span class="sr-only">Previous</span></a></li>{0}<li class="disabled"><a href="{4}"><span aria-hidden="true">&raquo;</span><span class="sr-only">Next</span></a></li></ul></nav>
paging.banner.onepage=<nav><ul class="pagination">{0}</ul></nav>

paging.banner.page.selected=<li class="disabled"><span>{0}</span></li>
paging.banner.page.link=<li><a href="{1}">{0}</a></li>
paging.banner.page.separator=

Then we save the changes and deploy our project and we will see this new design:

I hope this help.

Saturday, December 27, 2014

Appfuse, updating the bootstrap version to v3.3.1.

Appfuse, updating the bootstrap version to v3.3.1.

I have been initiating me with AppFuse and I found very good job by the development team, the idea seems very interesting, but does not become a framework for code generation, but it is a good start. In this first article I want to share with you, as the first of several that I want to share my experiences with AppFuse.

To begin we will assume that you have used the archetype for a generation skeleton of our application that comes with some basic implementations out-of-the-box which can be found at the homepage of AppFuse. In this case for this sample the following image:


mvn archetype: generate -B -DarchetypeGroupId = org.appfuse.archetypes -DarchetypeArtifactId = appfuse-modular-spring-archetype -DarchetypeVersion = 3.0.0 -DgroupId = org.onedevelopment -DartifactId = RegPlusX -DarchetypeRepository = https://oss.sonatype.org/content/repositories/appfuse

After we have completed this process, copy and paste the command line generated in the console in the path of our workspace:


Upon completion of this process we have in our workspace directory a folder with our generated code. In our example case we will not explain the process of starting with AppFuse, I recommend following the Quick Start at official site.
As our interest is to upgrade the version of Bootstrap we go to our generated code in the folder that maven created us, in this example "RegPlusX" then we must find our root "pom.xml", open it and verify the version of Bootstrap in this case is:

<bootstrap.version>3.0.2</bootstrap.version>

we changed to version "3.3.1" the current version at this moment, this will be this:

<bootstrap.version>3.3.1</bootstrap.version>

Then we will make all other changes for proper operation and execution of our application. Very important, in this example we are use multi-module so we will have a core module, where the core functions of our application are and a Web module for our web app. Then let see the file "pom.xml" found within the Web module and edit this section where Bootstrap specified:

To understand the method in which the javascripts and css are used, or static resources is recommend reading this article: Adding web resource fingerprinting to AppFuse With wro4j.

<!-- Add Bootstrap via WebJars -->
<dependency>
 <groupId>org.webjars</groupId>
 <artifactId>bootstrap</artifactId>
</dependency>
<dependency>
 <groupId>org.webjars</groupId>
 <artifactId>bootstrap-datepicker</artifactId>
 <version>1.2.0</version>
</dependency>
<!-- Downgrade jQuery so it works with HtmlUnit -->
<dependency>
 <groupId>org.webjars</groupId>
 <artifactId>jquery</artifactId>
 <version>1.8.3</version>
</dependency>

Then we make the following changes, in the above code with this:

<!-- Add Bootstrap via WebJars -->
<!-- Updated to newest version -->
<dependency>
 <groupId>org.webjars</groupId>
 <artifactId>bootstrap</artifactId>
 <!-- <version>3.3.1</version> -->
</dependency>
<!-- <dependency> -->
 <!-- <groupId>org.webjars</groupId> -->
 <!-- <artifactId>bootswatch</artifactId> -->
 <!-- <version>3.3.1+2</version> -->
<!-- </dependency> -->
<!-- Updated to newest version -->
<dependency>
 <groupId>org.webjars</groupId>
 <artifactId>bootstrap-datepicker</artifactId>
 <version>1.3.0-3</version>
</dependency>
<!-- Updated to newest version -->
<dependency>
 <groupId>org.webjars</groupId>
 <artifactId>jquery-cookie</artifactId>
 <version>1.4.1-1</version>
</dependency>
<!-- Downgrade jQuery so it works with HtmlUnit -->
<!-- Updated to newest version -->
<dependency>
 <groupId>org.webjars</groupId>
 <artifactId>jquery</artifactId>
 <version>1.11.1</version>
</dependency>
<!-- We add 1.7.7 of Wro4j because the old version 1.7.2 is not working with this version of boostrap -->
<dependency>
 <groupId>ro.isdc.wro4j</groupId>
 <artifactId>wro4j-core</artifactId>
 <version>1.7.7</version>
</dependency>

As can be seen, Bootstrap is updated to the version 3.3.1, but this implies also update "bootstrap-datepicker" to the version 1.3.0-3, "jquery-cookie" to the version 1.4.1-1, "jquery "to version 1.11.1 and also need to add the version 1.7.7 of " wro4j-core " as dependency because old versions does not work.

The next step is to change the setting of "Wro4j" found at the following path in the Web module "src/main/webapp/WEB-INF/wro.xml". Here we change the settings as follows:


<css>classpath:META-INF/resources/webjars/bootswatch/3.0.0/spacelab/bootstrap.min.css</css>
<css>/styles/style.css</css>
<js>classpath:META-INF/resources/webjars/jquery/1.8.3/jquery.min.js</js>
<js>classpath:META-INF/resources/webjars/bootstrap/3.0.2/js/bootstrap.min.js</js>
<js>classpath:META-INF/resources/webjars/jquery-cookie/1.3.1/jquery.cookie.js</js>
<js>/scripts/script.js</js>

With this changes shown:

<!-- If you wanto to use the default Appfuse stye uncomment this line -->
<!--<css>classpath:META-INF/resources/webjars/bootswatch/3.3.1+2/spacelab/bootstrap.min.css</css> -->
<css>classpath:META-INF/resources/webjars/bootstrap/3.3.1/css/bootstrap.min.css</css>
<css>/styles/style.css</css>
<js>classpath:META-INF/resources/webjars/jquery/1.11.1/jquery.min.js</js>
<js>classpath:META-INF/resources/webjars/bootstrap/3.3.1/js/bootstrap.min.js</js>
<js>classpath:META-INF/resources/webjars/jquery-cookie/1.4.1-1/jquery.cookie.js</js>
<js>/scripts/script.js</js>

If you can notice, we change the versions that need to be updated, it is important to note that the above changes if you want to keep the default AppFuse style instead of using the style of Bootstrap, then include this library "bootswatch " and use this.

Finally test our changes to the browser, run this command in the console for the "core" module:

mvn install –DskipTests=true

Then go to "Web" module and run the same command before and finally test our code in jetty running following command over de "Web" module too:

mvn jetty: run

And the result is our AppFuse application running with version 3.3.1 Bootstrap and style too:

I hope this help, the source code can be downloaded from this location.

Friday, October 5, 2012

Trabajando organizado en GWT

Los que seguro hemos empezado con GWT nos damos cuenta que es un framework que se utiliza bastante, es desarrollado por el equipo de Google y basicamante lo que hace es convertir codigo java a javascript y es muy comodo porque no tienes que tener conocimientos de javascript, ademas existen muchas bibliotecas para basadas en GWT.

Lo intersante del tema es que cuando empecé me encontre con un problema de desorganización en el codigo, y buscando información sobre el tema en google encontré un framework que desarrollaba una arquitectura basada en el patrón MVP (Model-View-Presenter) y su nombre es MVP4G.

Hoy les quiero compartir el como trabajar con este framework.Para integrar Mvp4g en su proyecto, se necesitan los pasos siguientes:

  1. —Agregar la biblioteca necesitada a su proyecto o agregar una dependencia de Maven.
  2. —Modificar el archivo de configuración de GWT (*. gwt.xml).
  3. —Configurar el punto de entrada (Entry Point).
  4. —Crear EventBus, Presentadores y las Vistas.
  5. —Configurar para el uso de Mvp4g APT (optativo). 
1. Las bibliotecas asi como unos ejemplos pueden ser descargados de la siguiente url:

Las bibliotecas necesarias de GIN:  Desargar aqui
Mvp4g: Descargar aqui

para los usuarios que usan maven las Dependencias de Maven son:
<dependency>
       <groupId>com.googlecode.mvp4g</groupId>      
       <artifactId>mvp4g</artifactId>        
       <version>1.4.0</version>
</dependency>

Mvp4g está disponible en el repositorio central de Maven.



2. agregarle al fichero de configuracion de GWT la siguiente linea:
<inherits name='com.mvp4g.Mvp4gModule'/>


3. Para configurar el punto de entrada o entry point

Si se quiere usar dentro del RootPanel:
Poner a Mvp4g Entry Point como el punto de entrada modificando el fichero de configuración de GWT:

<entry-point class='com.mvp4g.client.Mvp4gEntryPoint'/>

O poniendo las siguientes lineas y solo estas lineas dentro de el punto de entrada de la aplicación:

Mvp4gModule module = (Mvp4gModule)GWT.create( Mvp4gModule.class ); module.createAndStartModule(); RootPanel.get().add( (Widget)module.getStartView() );



4. El Bus de eventos

La meta principal de Mvp4g es permitirle crear un Bus de Eventos y Eventos fácilmente sin crear clases e interfaces para cada evento. Otra meta es ver que Presentador maneja fácilmente cada evento. 
 
Un evento se define por: 
 
    su nombre (o tipo) 
    los objetos que pueden dispararse con él.  
 
Todo lo que usted tiene que hacer para crear un Bus de Eventos es crear una interfaz EventBus y definir un método para cada evento. Una implementación de esta interfaz se generará automáticamente por el framework y se inyectará a cada ponente. Un patrón Singleton se usa para crear el Bus de Eventos.




Para crear el Bus de Eventos se necesita:

Crear una interfaz que extiende de com.mvp4g.client.event.EventBus

Anótelo con @Events
 
La anotación @Events tiene los atributos siguientes: 
 
startView (obligatorio) y startViewName (optativo), vea StartView. 
historyOnStart (optativo, por defecto falso). 
module (optativo, por defecto Mvp4gModule), se usa en caso del multi-módulo. 
ginModules (optativo).

@Events(startView = MainView.class)
public interface MainEventBus extends EventBus { ... }


Crear un Evento se necesita:

Crear un método en la interfaz del Bus de Eventos. Este método no debe devolver nada y puede tener los tantos parámetros como usted desee. 

Anótelo con @Event.

@Events(startView = MainView.class)
public interface MainEventBus extends EventBusWithLookup {

    @Event(...)
    public void goToCompany();

    @Event(...)
    public void changeBody(Widget newBody);

    ...
}

La anotación @Event tiene muchos atributos, pero nos centramos en el mas importante:
handlers: Nombre de los presentadores que van manipular el evento. Solo presentadores y Eventos puede manipular eventos.

@Event(handlers=CompanyListPresenter.class)
public void companyDeleted(CompanyBean newBean);

@Event(handlers={CompanyListPresenter.class, CompanyDisplayPresenter.class})
public void companyCreated(CompanyBean newBean);

Para lanzar un evento solamente hacemos uso de la siguiente línea de código, que no es mas que llamar a los métodos definido en la interfaz:

eventBus.companyCreated(new CompanyBean("company"));

El usuario no tiene que manejar creación del eventbus y la implementación, el framework lo hará para usted. También lo inyectará  automaticamente a todos sus presentadores.


Start View: Cada módulo necesita definir una Vista de Inicio. La Vista de Inicio es la vista que se agrega al RootPanel o RootLayoutPanel cuando la aplicación se inicia.  Para definir una start view, usted necesita especificar el atributo 'startView' de la anotación @Events de la interfaz del Bus de eventos.

Start Event: Cuando la aplicación empieza, se puedes querer disparar un evento automáticamente para que las acciones que se necesitaran al principio puedan ejecutarse. Para definir un Evento de Inicio, se necesita anotarlo con @Start. Usted puede tener sólo un Evento de Inicio por modulo.

@Events(...)
public interface OneEventBus extends EventBus {

     @Start
     @Event(...)
     void start();

}

Mvp4g define principalmente 3 tipos de elementos:

Presenter (o Manejador de Evento): Este elemento permite el manejo de los eventos, la interacción con la Vista y Manejo de los Servicios. 
View: Este elemento permite la creación de los objetos visuales.
Services: Este elemento permite la interacción con el servidor


Crear a un Presentador:
 
Extender de BasePresenter <V,E>: 
        V: el tipo de la vista que se inyectará en el Presentador
        E: el tipo de la interfaz del Bus de Eventos.  
 
Usted también debe sobrescribir el método Bind para enlazar la vista a su Presentador. 

Tener un constructor sin parámetros o compatible con GIN.  

Anotarlo con @Presenter y establecer el atributo ‘view'. Este atributo debe definir la clase de la vista que implementa la interfaz de vista asociada al presentador.


@Presenter( view = WestView.class )
public class WestPresenter extends BasePresenter<WestPresenter.IWestView, CpanelEventBus> {   
public interface IWestView {       
    public Panel getViewWidget();   
}   
public void onStart() {       
    eventBus. goToCompany();   
}   
public void bind() {   
    view.getNewerButton().addClickHandler( new ClickHandler() {       
             public void onClick( ClickEvent event ) {                   
                 eventBus. companyCreated(newBean);           
    }} );
}

Injectando servicios:

Con GIN, se puede inyectar muy facilmente gracias la anotación @Inject:

@Presenter(view=OneView.class)
public class OnePresenter extends BasePresenter<IOneView, OneEventBus>{

ServiceAsync service = null;

@InjectService
public void setService(ServiceAsync service) {
    this.service = service;
}
}

Las Vistas:

@Singleton
public class WestView extends Composite implements WestPresenter.IWestView {   
private Panel westPanel;   

@Inject   
public WestView() {       
    initWidget();   
}   
@Override   
public Panel getViewWidget() {       
    return westPanel;   
}


Realmente MVP4G es un framework que esta bien realizado y viene a hacernos la vida mucho mas facil a la hora de desarrollar nuestros proyectos de GWT, es muy facil de usar, para profundizar un poco mas se puede visitar la pagina del proyecto: Proyecto en Google pero además se pueden descargar los ejemplos que se encuentran en la pagina de descarga para obtener una mejor visión de su funcionamiento lo que si es importante destacar la organización que se logra en nuestros proyectos a la hora de organizar nuestro código esero que le hay servido de algo a aquellas personas que esten empesando con GWT y quieran conocer las mejores practicas.

5. Configurando Mvp4g APT

Por defecto, todos los errores debido a una mala configuración de Mvp4g se descubre cuando se empieza la recopilación de GWT o el GWT dev-mode. Gracias a Mvp4g APT se pueden descubrir la mayoría de estos errores directamente en el IDE. Puede visitar el siguiente enlace para mas información: http://code.google.com/p/mvp4g/wiki/APT














Mi primer Post

Este es el primer post que realizo en mi blog, espero poder estar activo y publicar información de las notas sobre mis desarrollos con los diferentes lenguajes de programación y que estos le sirvan a la comunidad, aportando mi granito de arena a esta causa.