PA165/Lab session Remoting

Z FI WIKI
Verze z 19. 11. 2013, 17:00; 3988@muni.cz (diskuse | příspěvky)

(rozdíl) ← Starší verze | zobrazit aktuální verzi (rozdíl) | Novější verze → (rozdíl)
Přejít na: navigace, hledání


RMI

At first we will try Java RMI remote calls.

The file noticeboard.jar contains just a Java interface:

 
package cz.muni.fi.pa165.rmi;
 
import java.rmi.Remote;
import java.rmi.RemoteException;
 
public interface NoticeBoard extends Remote {
 
      void publish(String text) throws RemoteException;
 
}

RMI Client

Let's create a client:

 
package cz.muni.fi.pa165.rmi;
 
import java.rmi.registry.Registry;
import java.rmi.registry.LocateRegistry;
import java.rmi.RemoteException;
import java.rmi.NotBoundException;
 
public class Publisher {
 
    public static void main(String[] args) throws RemoteException, NotBoundException {
        if(args.length!=2) {
            System.out.println("Usage: java "+Publisher.class.getName()+" host text");
            System.exit(1);
        }
        String host = args[0];
        String text = args[1];
 
        Registry remregistry = LocateRegistry.getRegistry(host);
        NoticeBoard noticeBoard = (NoticeBoard) remregistry.lookup("board");
 
        noticeBoard.publish(text);
    }
}
 

Run the client against the presenter's server running at nymfe31.fi.muni.cz.


(If you can't compile and run a java class from command line, then do:

 
  mkdir rmiclient
  cd rmiclient
  wget http://acrab.ics.muni.cz/~makub/rmi/noticeboard.jar
  unzip noticeboard.jar
  vi cz/muni/fi/pa165/rmi/Publisher.java
  javac cz/muni/fi/pa165/rmi/Publisher.java
  java cz.muni.fi.pa165.rmi.Publisher nymfe31 'something smart'
  

or in NetBeans create a new project File - New Project - Categories: Java - Projects: Java Application, in dialog 'Create Main Class enter cz.muni.fi.pa165.rmi.Publisher, then in Libraries choose Add JAR/Folder, add file noticeboard.jar, overwrite the source of the Publisher class, edit project properties Properties - Run - Arguments and type there nymfe31 'something smart' . Then run the project. It is easier from the command line, isn't it ? )

RMI server

Make a pair with your neighbor and try RMI with his or her server.

On the server machine, it it necessary to run the command

 mkdir rmiserver
 cd rmiserver
 wget http://acrab.ics.muni.cz/~makub/rmi/noticeboard.jar
 unzip noticeboard.jar
 rmiregistry &

and have a server implementation:

 
package cz.muni.fi.pa165.rmi;
 
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.server.ServerNotActiveException;
import java.rmi.server.UnicastRemoteObject;
 
public class NoticeBoardImpl implements NoticeBoard {
 
    synchronized public void publish(String text) throws RemoteException {
        try {
            System.out.println(UnicastRemoteObject.getClientHost() + ": " + text);
        } catch (ServerNotActiveException e) {
            e.printStackTrace();
        }
    }
 
    public static void main(String[] args) throws RemoteException {
        System.out.println("starting noticeboard ...");
        NoticeBoard stub = (NoticeBoard) UnicastRemoteObject.exportObject(new NoticeBoardImpl(), 0);
        LocateRegistry.getRegistry().rebind("board", stub);
    }
 
}

which must be compiled by

javac  cz/muni/fi/pa165/rmi/NoticeBoardImpl.java

and run with the correct parameters:

java -Djava.rmi.server.codebase=http://acrab.ics.muni.cz/~makub/rmi/noticeboard.jar   cz.muni.fi.pa165.rmi.NoticeBoardImpl

Beware, in B130 is wrong file /etc/hosts binding hostname to local address, you have to use the following:

LANG=C MYADDR=`ifconfig | awk -F':' '/inet addr/&&!/127.0.0.1/{split($2,_," ");print _[1]}'`
echo $MYADDR
java -Djava.rmi.server.codebase=http://acrab.ics.muni.cz/~makub/rmi/noticeboard.jar \
 -Djava.rmi.server.hostname=$MYADDR   cz.muni.fi.pa165.rmi.NoticeBoardImpl

SOAP/WSDL Web Services

We will try SOAP web services using the toolkit Apache CXF.

SOAP Server

Create a new Maven project named soap-cxf-server. Add to pom.xml the following:

 
   <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <cxf.version>2.7.7</cxf.version>
    </properties>
 
    <dependencies>
        <dependency>
            <groupId>org.apache.cxf</groupId>
            <artifactId>cxf-rt-frontend-jaxws</artifactId>
            <version>${cxf.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.cxf</groupId>
            <artifactId>cxf-rt-transports-http</artifactId>
            <version>${cxf.version}</version>
        </dependency>
        <!-- Jetty is needed if you're are not using the CXFServlet -->
        <dependency>
            <groupId>org.apache.cxf</groupId>
            <artifactId>cxf-rt-transports-http-jetty</artifactId>
            <version>${cxf.version}</version>
        </dependency>
        <!-- SLF4J binding -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-jdk14</artifactId>
            <version>1.7.2</version>
        </dependency>
    </dependencies>

Create package cz.muni.fi.pa165.soap with the following classes:

 
package cz.muni.fi.pa165.soap;
 
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.ws.WebFault;
 
@WebFault(name = "MyFault")
@XmlAccessorType(XmlAccessType.FIELD)
public class MyFault extends RuntimeException {
    String reason;
 
    public MyFault(String message,String reason) {
        super(message);
        this.reason = reason;
    }
}
 
package cz.muni.fi.pa165.soap;
 
import javax.jws.WebService;
import javax.jws.WebParam;
 
@WebService
public interface NoticeBoard {
    
    void publish(@WebParam(name = "text")String text) throws MyFault;
 
}
 
package cz.muni.fi.pa165.soap;
 
import javax.annotation.Resource;
import javax.jws.WebService;
import javax.servlet.http.HttpServletRequest;
import javax.xml.ws.WebServiceContext;
import javax.xml.ws.handler.MessageContext;
 
@WebService(endpointInterface = "cz.muni.fi.pa165.soap.NoticeBoard", serviceName = "NoticeBoard")
public class NoticeBoardImpl implements NoticeBoard {
 
    @Resource
    WebServiceContext wsCtx;
 
    public void publish(String text) throws MyFault {
        if (text == null || text.isEmpty()) throw new MyFault("empty text", "the entered string is empty");
 
        System.out.println(getClient() + ":" + text);
    }
 
    private String getClient() {
        MessageContext msgCtx = wsCtx.getMessageContext();
        HttpServletRequest req = (HttpServletRequest) msgCtx.get(MessageContext.SERVLET_REQUEST);
        return req.getRemoteHost();
    }
 
}
 
package cz.muni.fi.pa165.soap;
 
import javax.xml.ws.Endpoint;
 
public class Server {
    public static void main(String[] args) throws InterruptedException {
        String YOUR_IP = "147.251.53.41";
        String address = "http://"+YOUR_IP+":9000/board";
        Endpoint.publish(address, new NoticeBoardImpl());
        System.out.println("Server ready...");
    }
}

Run the class cz.muni.fi.pa165.soap.Server and visit the URL http://localhost:9000/board?wsdl.

SOAP Client

Without closing the previous project, create a new project soap-cxf-client.

We use separate project to demonstrate that the only connection between the client and the server is the WSDL description of the service.

Add to pom.xml

 
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <cxf.version>2.7.7</cxf.version>
    </properties>
 
    <dependencies>
        <dependency>
            <groupId>org.apache.cxf</groupId>
            <artifactId>cxf-rt-frontend-jaxws</artifactId>
            <version>${cxf.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.cxf</groupId>
            <artifactId>cxf-rt-transports-http</artifactId>
            <version>${cxf.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.cxf</groupId>
            <artifactId>cxf-rt-transports-http-jetty</artifactId>
            <version>${cxf.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.cxf</groupId>
            <artifactId>cxf-common-utilities</artifactId>
            <version>2.5.11</version>
        </dependency>
    </dependencies>
 
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.cxf</groupId>
                <artifactId>cxf-codegen-plugin</artifactId>
                <version>${cxf.version}</version>
                <executions>
                    <execution>
                        <id>generate-sources</id>
                        <configuration>
                            <wsdlOptions>
                                <wsdlOption>
                                    <wsdl>http://localhost:9000/board?wsdl</wsdl>
                                </wsdlOption>
                            </wsdlOptions>
                        </configuration>
                        <goals>
                            <goal>wsdl2java</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>2.5.1</version>
                <configuration>
                    <source>6</source>
                    <target>6</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-dependency-plugin</artifactId>
                <configuration>
                    <outputDirectory>
                        ${project.build.directory}
                    </outputDirectory>
                </configuration>
            </plugin>
        </plugins>
    </build>

Add the following class:

 
package cz.muni.fi.pa165.soap;
 
public class Client {
    public static void main(String[] args) {
        NoticeBoard board = new NoticeBoard_Service().getNoticeBoardImplPort();
        try {
            
            board.publish("hello!");
            System.out.println("sent ..."); 
            
            //board.publish("");
 
        } catch (MyFault_Exception e) {
            e.printStackTrace();
            System.out.println(e.getFaultInfo().getReason());
        }
    }
}

In NetBeans run Build. Alternatively you can run on command line mvn generate-sources and mvn package.

Run the class Client.

For copies of all needed libraries use command mvn dependency:copy-dependencies.

The service URL comes from the WSDL document. if you need to change it, use:

 
((BindingProvider) board).getRequestContext()
                .put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY, "http://nymfe31.fi.muni.cz:9000/board");

REST

REST server

Create new project of type Maven - Web Application, named rest-noticeboard-server.

Add to pom.xml:

 
<packaging>war</packaging>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    </properties>
    <dependencies>
        <!-- Jackson JSON processor -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.2.3</version>
        </dependency>
        <!-- servlet API 3.0 -->
        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-web-api</artifactId>
            <version>6.0</version>
            <scope>provided</scope>
        </dependency>
        <!-- JSTL , just for escaping HTML -->
        <dependency>
            <groupId>taglibs</groupId>
            <artifactId>standard</artifactId>
            <version>1.1.2</version>
        </dependency>
    </dependencies>
 
    <build>
        <plugins>
            <!-- Java language version -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>6</source>
                    <target>6</target>
                </configuration>
            </plugin>
            <!-- Servlet 3.0 without web.xml -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-war-plugin</artifactId>
                <version>2.1.1</version>
                <configuration>
                    <failOnMissingWebXml>false</failOnMissingWebXml>
                </configuration>
            </plugin>
        </plugins>
    </build>

Create new class

 
package cz.muni.fi.pa165.rest;
 
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.taglibs.standard.functions.Functions;
 
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import java.util.*;
 
@WebServlet(urlPatterns = "/board/*")
public class NoticeBoardServlet extends HttpServlet {
 
    final List<Map<String, String>> records = Collections.synchronizedList(new ArrayList<Map<String, String>>());
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
 
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String pathInfo = request.getPathInfo();
        if (pathInfo != null && pathInfo.matches("/\\d+")) {
            response.setContentType("application/json");
            ObjectMapper mapper = new ObjectMapper();
            mapper.writeValue(response.getOutputStream(), records.get(Integer.parseInt(pathInfo.substring(1))));
        } else {
            if ("html".equals(request.getParameter("type"))) {
                response.setContentType("text/html;charset=utf-8");
                PrintWriter out = response.getWriter();
                response.setHeader("Refresh", "5");
                out.println("<h1>Noticeboard at " + request.getContextPath() + "/board</h2>");
                out.println("<table border=\"1\">");
                synchronized (records) {
                    for (int i = records.size(); --i >= 0;) {
                        Map<String, String> m = records.get(i);
                        out.println("<tr><td><a href=\""+request.getContextPath()+"/board/"+i+"\">" + i + "</a>"
                                + "<td>" + m.get("when")
                                + "<td>" + m.get("who") + "<td>" + Functions.escapeXml(m.get("what")));
                    }
                }
                out.println("</table>");
            } else {
                response.setContentType("application/json");
                ObjectMapper mapper = new ObjectMapper();
                mapper.writeValue(response.getOutputStream(), records);
            }
        }
    }
 
    @Override
    protected synchronized void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        getServletContext().log("doPost() contentType="+request.getContentType());
        request.setCharacterEncoding("utf-8");
        String publish = request.getParameter("publish");
        if (publish != null) {
            Map<String, String> m = new LinkedHashMap<String, String>();
            m.put("when", sdf.format(new Date()));
            m.put("who", request.getRemoteHost());
            m.put("what", publish);
            records.add(m);
            response.sendRedirect(request.getContextPath() + "/board/" + (records.size() - 1));
        } else if (request.getContentType().startsWith("application/json")) {
            ObjectMapper mapper = new ObjectMapper();
            JsonNode jsonNode = mapper.readValue(request.getInputStream(), JsonNode.class);
            JsonNode jsonPublish = jsonNode.get("publish");
            if (!jsonNode.isMissingNode()) {
                Map<String, String> m = new LinkedHashMap<String, String>();
                m.put("when", sdf.format(new Date()));
                m.put("who", request.getRemoteHost());
                m.put("what", jsonPublish.textValue());
                records.add(m);
                response.sendRedirect(request.getContextPath() + "/board/" + (records.size() - 1));
            } else {
                System.err.println("Missing key");
                response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Missing required data key.");
            }
        } else {
            System.err.println("Unknown request type " + request.getContentType());
            response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Unknown request type.");
        }
    }
}

After starting the server, an HTML version of the noticeboard should be visible at http://localhost:8080/board/?type=html empty so far.

At http://localhost:8080/board/ then JSON data should be visible, empty too.

REST Client

Create new project of type Maven - Java Application named rest-noticeboard-client.

Add to pom.xml

 
 <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    </properties>
 
    <build>
        <plugins>
            <!-- Java language version -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>6</source>
                    <target>6</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
 
    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-web</artifactId>
            <version>3.2.4.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.2.3</version>
        </dependency>
    </dependencies>

Create a REST client, which will add a new record and get all available records:

 
package cz.muni.fi.pa165.rest.client;
 
import com.fasterxml.jackson.databind.JsonNode;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
 
import java.net.URI;
import java.util.HashMap;
import java.util.Map;
 
public class BoardRestClient {
    
    static String HOST = "localhost";
    static int PORT = 8080;
    static String webapp = "rest-noticeboard-server";
    
    public static void main(String[] args) {
        RestTemplate rt = new RestTemplate();
 
        String url = "http://"+HOST+":"+PORT+"/"+webapp+"/board";
        
        //POST for adding a new record
        URI uri = rt.postForLocation(url+"?publish={text}", null, "příliš žluťoučký kůň");
        System.out.println("new record URI  = " + uri);
 
        //GET for getting all records
        JsonNode jsonNode = rt.getForObject(url, JsonNode.class);
 
        for(JsonNode m : jsonNode) {
            System.out.println("record = " + m);
        }
    }
}

We use Spring 3.x RestTemplate (javadoc for RestTemplate).

We can observe coming new records in the URL above.

Fixing encoding issues

Notice that character encoding has problems, we will fix it by changing the client to use POST request either with JSON data or with form data instead of a URL parameter:


 
        //POST with JSON
        Map<String, String> map = new HashMap<String, String>();
        map.put("publish", "příliš žlutoučká kráva - json");
        rt.postForLocation(url, map);
 
        //POST with application/x-www-form-urlencoded
        MultiValueMap<String, String> mvmap = new LinkedMultiValueMap<String, String>();
        mvmap.add("publish", "příliš žlutoučká kráva - form");
        rt.postForLocation(url, mvmap);

HTML page client

We can do the same POST request from a simple HTMl page generated by a JSP. In the server project (rest-noticeboard-server), create src/main/webapp/index.jsp with the following content:

 
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!DOCTYPE html>
<html lang="en">
<head>
    <title>Noticeboard</title>
</head>
<body>
 <h1>Noticeboard HTML</h1>
 
 <form action="${pageContext.request.contextPath}/board" method="post">
     <label for="publish">Text:</label><input id="publish" type="text" name="publish" value="štíhlá laň pádí k útesům" size="30">
     <input type="submit" value="Publish">
 </form>
</body>
</html>

However the REST interface is not prepared for HTML forms, it returns a redirect to a JSON response.

We would better create an AJAX page:

 
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!DOCTYPE html>
<html lang="en">
<head>
    <title>Noticeboard</title>
    <!-- load JQuery -->
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
    <script>
        $(document).ready(function () {
            window.setInterval(updateBoard,1000);
        });
 
        function updateBoard() {
            $('#board').load('${pageContext.request.contextPath}/board?type=html');
            console.log("updated board");
        }
 
        function sendText() {
            var text = $('#publish').val();
            $.ajax({
                type: 'POST',
                url: '${pageContext.request.contextPath}/board',
                data: { 'publish': text },
                success: function (data) {
                    console.log('got ' + data);
                }
            });
        }
    </script>
</head>
<body>
 <div id="board">
    <h1>Noticeboard HTML</h1>
 </div>
 <label for="publish">Text:</label><input id="publish" type="text" name="publish" value="štíhlá laň pádí k útesům" size="30">
 <button onclick="sendText();">Publish</button>
</body>
</html>