Статьи

Java EE 8 MVC + Демонстрация листовки с картой

В этой статье показано, как создать географическую карту с использованием Java EE 8 MVC и Leafleft. Моя цель — заменить эту статичную скучную карту чем-то более сексуальным . Для этого я использовал новые Java EE MVC и Leaflet.

Новая платформа MVC на основе действий в Java EE будет доступна в Java EE 8. Базовую информацию об Ozark (справочную реализацию MVC) можно получить  из этой статьи . Leaflet — это библиотека JavaScript с открытым исходным кодом для интерактивных карт.

Для разработки приложений я использую Glassfish 4.1.1 из Java EE SDK update 2. Для запуска приложения в работе я использовал Tomcat 7.

Архитектура

Приложение разделено (как требует шаблон проектирования MVC) на три уровня:

  • Контроллер — он получает запросы от веб-браузера, получает данные о качестве воздуха и заполняет их в Model.

  • Модель является контейнером для наших данных. Он может извлекать данные в виде строки Geo JSON.

  • Просмотр — это стандартная страница JSP. Он получает данные из модели и выводит данные в браузер.

заявка

Java EE MVC основан на JAX-RS. Итак, создать приложение просто с помощью аннотации:

@ApplicationPath("ozark")
public class GettingStartedApplication extends Application 
{
}

контроллер

Данные о качестве воздуха (в ближайшее время AQ), которые я хочу отобразить на карте, находятся в этом  файле JSON , который обновляется каждый час. Вы можете использовать онлайн- просмотрщик JSON, чтобы узнать его структуру. Здесь есть легенда (индекс качества воздуха) и информация об измерительных станциях и индексе качества воздуха.

Роль контроллера — читать и анализировать этот JSON и заполнять необходимую информацию в MapModel,  которая вводится CDI.

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.StandardCharsets;

import javax.inject.Inject;
import javax.json.Json;
import javax.json.JsonArray;
import javax.json.JsonObject;
import javax.json.JsonReader;
import javax.mvc.annotation.Controller;
import javax.ws.rs.GET;
import javax.ws.rs.Path;

/**
 * The MVC Controller. It waits for HTTP GET requests on his Path: 
 * <code>http://host:port/AppName/@ApplicationPath/@Path</code>
 * 
 * @author lvanek
 *
 */
@Controller
@Path("map")
public class MapController 
{
    /**
     * The MVC model
     */
    @Inject
    MapModel mapModel;


    /**
     * Method responds to HTTP GET requests
     * 
     * @return View path
     */
    @GET
    public String map()
    {
    createModel();

    return "/WEB-INF/jsp/mapView.jsp";
    }

    /**
     * Read JSON with Air quality data and populate MVC model
     * 
     * Use:
     * <code>http://jsonviewer.stack.hu/#http://portal.chmi.cz/files/portal/docs/uoco/web_generator/aqindex_eng.json</code><p>
     * for JSON browsing
     * 
     */
    private void createModel()
    {
    try
    {
    URL vvv = new URL("http://vvv.chmi.cz/uoco/aqindex_eng.json"); 
    URLConnection urlConnection = vvv.openConnection();

    try (final BufferedReader reader = new BufferedReader(new InputStreamReader(urlConnection.getInputStream(), StandardCharsets.UTF_8.displayName()))) 
    {
    JsonReader jsonReader = Json.createReader(reader);
    final JsonObject root = (JsonObject) jsonReader.read();

    mapModel.setActualized(root.getString("Actualized"));

    final JsonArray jsonLegend = root.getJsonArray("Legend");

    for (int i = 0; i < jsonLegend.size(); i++)
    {
    JsonObject jsonAqIndex = jsonLegend.getJsonObject(i);

    mapModel.putAirQualiryLegendItem(jsonAqIndex.getInt("Ix"), new AirQualiryLegendItem(jsonAqIndex.getInt("Ix"), 
    jsonAqIndex.getString("Color"), 
    jsonAqIndex.getString("ColorText"), 
    jsonAqIndex.getString("Description")));
    }

    final JsonArray jsonStates = root.getJsonArray("States");

    for (int s = 0; s < jsonStates.size(); s++)
    {
    final JsonObject jsonState = jsonStates.getJsonObject(s);
    final JsonArray jsonRegions = jsonState.getJsonArray("Regions");

    for (int i = 0; i < jsonRegions.size(); i++)
    {
    final JsonObject jsonRegion = jsonRegions.getJsonObject(i);

    final JsonArray jsonStations = jsonRegion.getJsonArray("Stations");
    for (int j = 0; j < jsonStations.size(); j++)
    {
    final JsonObject jsonStation = jsonStations.getJsonObject(j);

    if ((jsonStation.containsKey("Lat")) && (jsonStation.containsKey("Lon"))) // 'Prague center' and 'Prague periphery' not have position and components list
    {
    String wgs84Latitude = jsonStation.getString("Lat");
    String wgs84Longitude = jsonStation.getString("Lon");

    mapModel.addAirQualityMeasuringStation(new AirQualityMeasuringStation(jsonStation.getString("Code"),
    jsonStation.getString("Name"),
    jsonStation.getString("Owner"), 
    jsonStation.getString("Classif"), 
    Float.valueOf(wgs84Latitude), 
    Float.valueOf(wgs84Longitude), 
    jsonStation.getInt("Ix")));
    }
    }
    }
    }
    } 
    }
    catch (Exception e) 
    {
    e.printStackTrace();
    }
    }

    /**
     * Substitute an empty String, when a null value is encountered.
     * 
     * @param source String
     * @return Original string, or empty string, if parameter is null
     */
    static String nvl(String source)
    {
    return (source == null) ? "" : source;
    }
}

Последняя строка метода  map ()  перенаправляет обработку в наш  View .

модель

Модель представлена ​​тремя классами:

  • AirQualityLegendItem представляет один элемент легенды. Он содержит значение индекса AQ, его цвета и текстовое описание.

  • AirQualityMeasuringStation представляет собой станцию ​​измерения качества воздуха и содержит уникальный код, имя, положение WGS-84 и значение индекса AQ.

  • MapModel — сама модель. Содержит список станций и легенды.

public class AirQualityLegendItem 
{
private final int index;
private final String color;
private final String colorText;
private final String description;

/**
 * Constructor
 * 
 * @param index Air quality index value
 * @param color Background color
 * @param colorText Text color
 * @param description Index description
 */
public AirQualityLegendItem(int index, String color, String colorText, String description)
{
super();
this.index = index;
this.color = color;
this.colorText = colorText;
this.description = description;
}

// getters deleted for shorting
}

public class AirQualityMeasuringStation
{
private final String code;
private final String name;
private final String owner;
private final String classification;
private final float wgs84Latitude;
private final float wgs84Longitude;
private final int index;

/**
 * Constructor
 * 
 * @param code Station unique code
 * @param name Station name
 * @param owner Station owner
 * @param classification Station classification
 * @param wgs84Latitude WGS-84 latitude
 * @param wgs84Longitude WGS-84 longitude
 * @param index Air quality index value
 */
public AirQualityMeasuringStation(String code, String name, String owner, String classification, float wgs84Latitude, float wgs84Longitude, int index) 
{
super();
this.code = code;
this.name = name;
this.owner = owner;
this.classification = classification;
this.wgs84Latitude = wgs84Latitude;
this.wgs84Longitude = wgs84Longitude;
this.index = index;
} 

  // getters deleted for shorting
}

Класс модели карты аннотируется как @RequestScoped и имеет имя для CDI.

/**
 * The MVC model. Contains data for view:<p/>
 * <code>/WebContent/WEB-INF/jsp/mapView.jsp</code>
 * 
 * @author lvanek
 *
 */
@Named(value="mapModel")
@RequestScoped
public class MapModel 
{
/**
 *  Air quality indexes, key is index value 
 */
private Map<Integer, AirQualityLegendItem> legend = new HashMap<>(8);

/**
 * Air quality measuring stations
 */
private List<AirQualityMeasuringStation> stations = new ArrayList<>();

/**
 * Date of Air quality index actualization
 */
private String actualized;

public String getActualized() {
return actualized;
}

public void setActualized(String actualized) {
this.actualized = actualized;
}

private static final JsonBuilderFactory bf = Json.createBuilderFactory(null);

public Map<Integer, AirQualityLegendItem> getLegend() {
return legend;
}

public void putAirQualiryLegendItem(int index, AirQualityLegendItem item)
{
legend.put(index, item);
}

public AirQualityLegendItem getAirQualiryLegendItem(int index)
{
return legend.get(index);
}

public void addAirQualityMeasuringStation(AirQualityMeasuringStation station)
{
stations.add(station);
}

/**
 * Create Geo JSON with Air quality data. It's requested in mapView.jsp by:<p/>
 * 
 * <code>var geojson = &lt;c:out value="${mapModel.geoJson}" escapeXml="false"/&gt; ;</code>
 * 
 * @return Geo JSON string
 */
public String getGeoJson()
{
JsonObjectBuilder featureCollection = bf.createObjectBuilder().add("type", "FeatureCollection");

JsonArrayBuilder features = Json.createArrayBuilder();

createFeatures(features);

featureCollection.add("features", features);

JsonObject geoJson = featureCollection.build();

return geoJson.toString();
}

/**
 * Populate given GEO JSON features list with Air quality stations data
 * 
 * @param features GEO JSON features
 */
private void createFeatures(JsonArrayBuilder features)
{
/*
 * Sort stations by Air quality index to have worst ones on top layer
 */
Comparator<AirQualityMeasuringStation> comparator = new Comparator<AirQualityMeasuringStation>()
{
public int compare(AirQualityMeasuringStation a1, AirQualityMeasuringStation a2) 
{
return Integer.compare(a1.getIndex(), a2.getIndex());
}
};


Collections.sort(stations, comparator);

/*
 * Traverse stations and create Geo JSON Features
 */
for(AirQualityMeasuringStation station : stations)
{
String title = station.getCode() + " - " + station.getName();

AirQualityLegendItem legendItem = getAirQualiryLegendItem(station.getIndex());

features.add(createFeature(title,
"#" + legendItem.getColorText(),
"#" + legendItem.getColor(), 
station.getWgs84Latitude(), 
station.getWgs84Longitude(),
station.getClassification(),
legendItem.getDescription()));
}
}

/**
 * Create Geo JSON feature
 * 
 * @param title Title for popup
 * @param color Marker text color
 * @param fillColor Marker background color
 * @param wgs84Latitude WGS-84 Latitude
 * @param wgs84Longitude WGS-84 Longitude
 * @param classification Locality classification
 * @param description Air quality index description
 * @return Geo JSON feature
 */
private JsonObjectBuilder createFeature(String title, String color, String fillColor, float wgs84Latitude, float wgs84Longitude, String classification, String description)
{
JsonObjectBuilder feature = Json.createObjectBuilder()
    .add("type", "Feature");

// Feature properties
JsonObjectBuilder properties = Json.createObjectBuilder()
         .add("popupContent", title)
         .add("description", description)
         .add("classification", classification)
         .add("style", Json.createObjectBuilder()
         .add("weight", 1) 
         .add("color", color)
         .add("fillColor", fillColor)
         .add("fillOpacity", 0.8f)
         .add("opacity", 1));

feature.add("properties", properties);

// Feature geometry
JsonObjectBuilder geometry = Json.createObjectBuilder()
         .add("type", "Point")
         .add("coordinates", Json.createArrayBuilder().add(wgs84Longitude).add(wgs84Latitude));

feature.add("geometry", geometry);

return feature;
}
}

Важный метод  getGeoJson () будет вызываться из View, и он возвращает GEO JSON как String . Для каждой станции создается  функция  с этой структурой:

{
  "type":"Feature",
 "properties":{
      "popupContent":"LFRTA - Frydlant",
      "description":"Index on this station is not determined",
      "classification":"rural",
      "style":{
        "weight":1,
        "color":"#000000",
        "fillColor":"#CFCFCF",
        "fillOpacity":0.800000011920929,
        "opacity":1}
    },
 "geometry":{"type":"Point",
                "coordinates":[15.0698,50.940650]}
}

Посмотреть

Наконец, mapView.jsp  отображается. Это не весь его контент, а только важнейшие части.

В головной части Leafleft JavaScript и CSS включены:

<%@page contentType="text/html" pageEncoding="UTF-8" %>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
...

<head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <link href="./../resources/css/styles.css" rel="stylesheet" type="text/css" />
        <link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet-0.7.5/leaflet.css" />
        <script src="http://cdn.leafletjs.com/leaflet-0.7.5/leaflet.js"></script> 
        <title>Java EE 8 MVC + Leaflet Map demo</title>
 </head>

Затем нам нужно вставить карту div на страницу:

 <div id="map" style="height: 800px; position: relative; padding: 0px; margin: 0 auto 0 auto;"></div>

И последнее, но не менее важное: нам нужен код JavaScript для инициализации карты. Важнейшая часть этой строки:


var geojson = <c: out value = «$ {mapModel.geoJson}» escapeXml = «false» />;

… как  вызывается метод  MapModel getGeoJson () . Затем обрабатывается GEO JSON. Для каждого созданного  объекта очки передаются в круги с соответствующими цветами.

<script> 
var map = L.map('map').setView([50.0, 15.5], 8);

L.tileLayer('https://api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}.png?access_token=pk.eyJ1IjoiZmx5aW5nYmlyZCIsImEiOiJjaWd1dXoycWIwYzZ6dmttNWhvdDJlaG5jIn0.I__xI-EzhnkmRI2BB-1SJg', {
maxZoom: 18,
minZoom: 7,
attribution: 'Map data &copy; <a href="http://openstreetmap.org">OpenStreetMap</a> contributors, ' +
'<a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, ' +
'Imagery © <a href="http://mapbox.com">Mapbox</a>',
 id: 'xxx', // Use your own Mapbox id !
 accessToken: 'xxx' // Use your own Mapbox accessToken !
}).addTo(map);


var geojson = <c:out value="${mapModel.geoJson}" escapeXml="false"/>;

function onEachFeature(feature, layer) 
{
var popupContent = "<div style='background-color: " + feature.properties.style.fillColor + ";  padding-left: 2px; padding-right: 2px;'><p style='color: " + feature.properties.style.color + ";'>";

if (feature.properties && feature.properties.popupContent) {
popupContent += "<b> " + feature.properties.popupContent + "</b><br/>";
}

popupContent += "Classification: " + feature.properties.classification + 
"<br/>Air quality: <b>" + feature.properties.description + "</b></p></div>";

layer.bindPopup(popupContent);
}

L.geoJson(geojson, {

style: function (feature) {
return feature.properties && feature.properties.style;
},

onEachFeature: onEachFeature,

pointToLayer: function (feature, latlng) 
{
return L.circleMarker(latlng, {
radius: 8,
fillColor: feature.properties.fillColor,
color: feature.properties.color, 
weight: feature.properties.weight,
opacity: feature.properties.opacity,
fillOpacity: feature.properties.fillOpacity
});
}
}).addTo(map);


</script>

Резюме

Создать простое приложение с будущими инфраструктурами Java MVC очень просто.  Я был очень приятно удивлен скоростью разработки.

Бегущий на Glassfish

Glassfish 4.1.1 содержит все необходимые технологии, а не Ozark. Я использую инструменты Eclipse для разработки (не Maven), поэтому я помещаю только в файлы / WebContent / WEB-INF / lib файлы  ozark-1.0.0-m02.jar и  javax.mvc-api-1.0-edr2.jar .

Вы можете найти полный исходный код проекта Eclipse для Glassfish здесь .

Работает на Apache Tomcat 7

Здесь ситуация намного сложнее. Нам нужно скачать реализацию CDI WELD ( weld-servlet.jar ), JAX-RS API  Jersey и включить в приложение с помощью  jstl-1.2_1.jar и javax.json.jar, как показано на рисунке выше.

jersey-cdi * .jar ‘позаимствованы’ у Glassfish 4.1.1.

Заставь CDI работать

Создайте WebContent / META-INF / context.xml:

<?xml version="1.0" encoding="UTF-8"?>
<Context reloadable="true">

    <!-- Default set of monitored resources -->
    <WatchedResource>WEB-INF/web.xml</WatchedResource>

   <Resource name="BeanManager"
      auth="Container"
      type="javax.enterprise.inject.spi.BeanManager"
      factory="org.jboss.weld.resources.ManagerObjectFactory"/>
</Context>

Получить Джерси на работу

В web.xml добавьте этот контент:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
xmlns="http://java.sun.com/xml/ns/javaee" 
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" 
id="IskoOzarkApp"
version="3.0">

<display-name>OzarkMapApp</display-name>
<welcome-file-list>
    <welcome-file>index.html</welcome-file>
  </welcome-file-list>

<servlet>
<servlet-name>Jersey REST Service</servlet-name>
<servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class>


<!-- Register JAX-RS Application, if needed. -->
        <init-param>
            <param-name>javax.ws.rs.Application</param-name>
            <param-value>learning.javaee8.mvc.GettingStartedApplication</param-value>
        </init-param>

<!-- Register resources and providers -->
<init-param>
<param-name>jersey.config.server.provider.packages</param-name>
<param-value>idea.isko.mvc.map</param-value>
</init-param>

<!-- Enable Tracing support. -->
        <init-param>
            <param-name>jersey.config.server.tracing</param-name>
            <param-value>ALL</param-value>
        </init-param>

<load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
<servlet-name>Jersey REST Service</servlet-name>
<url-pattern>/rest/*</url-pattern>
</servlet-mapping>

  <resource-env-ref>
  <description>
      Needed to add CDI to Tomcat via Weld 2. It assumes that you have added weld-servlet.jar to WEB-INF/lib and that you
      edited META-INF/context.xml (or the default context.xml) and added a Resource entry for CDI. 

      See http://jsf2.com/using-cdi-and-jsf-2.2-faces-flow-in-tomcat/
    </description>
    <resource-env-ref-name>BeanManager</resource-env-ref-name>
    <resource-env-ref-type>javax.enterprise.inject.spi.BeanManager</resource-env-ref-type>
  </resource-env-ref>

</web-app>

На Tomcat путь к приложению: http: // хост: порт / OzarkMapApp / rest / map