From febbcb3e7d22d3321384c63dce694d99934a064e Mon Sep 17 00:00:00 2001
From: Tor-Einar Skog <tor-einar.skog@bioforsk.no>
Date: Mon, 18 Jan 2016 16:12:07 +0100
Subject: [PATCH] First usable version of universal messaging system

---
 nbactions.xml                                 |  28 +++
 .../NotificationSubscriptionController.java   | 166 ++++++++++++++
 .../no/nibio/vips/logic/entity/Message.java   |  20 ++
 .../logic/messaging/MessageLocalVersion.java  | 116 ++++++++++
 .../MessageNotificationSubscription.java      | 152 +++++++++++++
 .../logic/messaging/MessageRecipient.java     |  35 ++-
 .../vips/logic/messaging/MessagingBean.java   | 120 +++++++++-
 .../logic/messaging/UniversalMessage.java     | 156 ++++++++-----
 .../messaging/UniversalMessageFormat.java     |  77 +++++++
 .../vips/logic/util/IntegerArrayUserType.java | 213 ++++++++++++++++++
 src/main/resources/META-INF/persistence.xml   |   6 +-
 .../vips/logic/i18n/vipslogictexts.properties |  10 +
 .../logic/i18n/vipslogictexts_bs.properties   |  10 +
 .../logic/i18n/vipslogictexts_hr.properties   |  10 +
 .../logic/i18n/vipslogictexts_nb.properties   |  10 +
 .../logic/i18n/vipslogictexts_sr.properties   |  10 +
 src/main/webapp/WEB-INF/web.xml               |   8 +
 src/main/webapp/formdefinitions/userForm.json |   5 +
 .../notificationSubscriptionForm.ftl          |  88 ++++++++
 src/main/webapp/templates/userForm.ftl        |   9 +
 .../messaging/UniversalMessagingTest.java     |  50 +++-
 21 files changed, 1217 insertions(+), 82 deletions(-)
 create mode 100644 nbactions.xml
 create mode 100644 src/main/java/no/nibio/vips/logic/controller/servlet/NotificationSubscriptionController.java
 create mode 100644 src/main/java/no/nibio/vips/logic/messaging/MessageLocalVersion.java
 create mode 100644 src/main/java/no/nibio/vips/logic/messaging/MessageNotificationSubscription.java
 create mode 100644 src/main/java/no/nibio/vips/logic/messaging/UniversalMessageFormat.java
 create mode 100644 src/main/java/no/nibio/vips/logic/util/IntegerArrayUserType.java
 create mode 100644 src/main/webapp/templates/notificationSubscriptionForm.ftl

diff --git a/nbactions.xml b/nbactions.xml
new file mode 100644
index 00000000..476b75ff
--- /dev/null
+++ b/nbactions.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<actions>
+        <action>
+            <actionName>build</actionName>
+            <packagings>
+                <packaging>*</packaging>
+            </packagings>
+            <goals>
+                <goal>install</goal>
+            </goals>
+            <properties>
+                <skipTests>true</skipTests>
+            </properties>
+        </action>
+        <action>
+            <actionName>rebuild</actionName>
+            <packagings>
+                <packaging>*</packaging>
+            </packagings>
+            <goals>
+                <goal>clean</goal>
+                <goal>install</goal>
+            </goals>
+            <properties>
+                <skipTests>true</skipTests>
+            </properties>
+        </action>
+    </actions>
diff --git a/src/main/java/no/nibio/vips/logic/controller/servlet/NotificationSubscriptionController.java b/src/main/java/no/nibio/vips/logic/controller/servlet/NotificationSubscriptionController.java
new file mode 100644
index 00000000..66a41c55
--- /dev/null
+++ b/src/main/java/no/nibio/vips/logic/controller/servlet/NotificationSubscriptionController.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (c) 2016 NIBIO <http://www.nibio.no/>. 
+ * 
+ * This file is part of VIPSLogic.
+ * VIPSLogic is free software: you can redistribute it and/or modify
+ * it under the terms of the NIBIO Open Source License as published by 
+ * NIBIO, either version 1 of the License, or (at your option) any
+ * later version.
+ * 
+ * VIPSLogic 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
+ * NIBIO Open Source License for more details.
+ * 
+ * You should have received a copy of the NIBIO Open Source License
+ * along with VIPSLogic.  If not, see <http://www.nibio.no/licenses/>.
+ * 
+ */
+package no.nibio.vips.logic.controller.servlet;
+
+import freemarker.core.ParseException;
+import java.io.IOException;
+import java.util.List;
+import javax.persistence.EntityManager;
+import javax.persistence.PersistenceContext;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import no.nibio.vips.logic.controller.session.UserBean;
+import no.nibio.vips.logic.entity.VipsLogicRole;
+import no.nibio.vips.logic.entity.VipsLogicUser;
+import no.nibio.vips.logic.messaging.MessageNotificationSubscription;
+import no.nibio.vips.logic.messaging.UniversalMessageFormat;
+import no.nibio.vips.logic.util.SessionControllerGetter;
+import no.nibio.vips.util.ServletUtil;
+import no.nibio.web.forms.FormUtil;
+
+/**
+ * @copyright 2016 <a href="http://www.nibio.no/">NIBIO</a>
+ * @author Tor-Einar Skog <tor-einar.skog@nibio.no>
+ */
+
+
+public class NotificationSubscriptionController extends HttpServlet {
+
+    @PersistenceContext(unitName="VIPSLogic-PU")
+    EntityManager em;
+    
+    /**
+     * Processes requests for both HTTP <code>GET</code> and <code>POST</code>
+     * methods.
+     *
+     * @param request servlet request
+     * @param response servlet response
+     * @throws ServletException if a servlet-specific error occurs
+     * @throws IOException if an I/O error occurs
+     */
+    protected void processRequest(HttpServletRequest request, HttpServletResponse response)
+            throws ServletException, IOException {
+        
+        String action = request.getParameter("action");
+        VipsLogicUser user = (VipsLogicUser) request.getSession().getAttribute("user");
+        UserBean userBean = SessionControllerGetter.getUserBean();
+        
+        try
+        {
+            // Default: View list of user subscriptions
+            // TODO: Only superusers and organization admins should see other users' subscriptions
+            if(action == null)
+            {
+                Integer userId = Integer.valueOf(request.getParameter("userId"));
+                VipsLogicUser viewUser;
+                if(userBean.authorizeUser(user, VipsLogicRole.ORGANIZATION_ADMINISTRATOR, VipsLogicRole.SUPERUSER))
+                {
+                    viewUser = em.find(VipsLogicUser.class, userId);
+                }
+                else
+                {
+                    viewUser = user;
+                }
+                MessageNotificationSubscription subscription = SessionControllerGetter.getMessagingBean().getMessageNotificationSubscription(viewUser.getUserId());
+                request.setAttribute("messageNotificationSubscription",subscription);
+                request.setAttribute("userCropIds", subscription != null ? subscription.getCropIds() : new Integer[0]);
+                request.setAttribute("userMessageTagIds", subscription != null ? subscription.getMessageTagIds() : new Integer[0]);
+                request.setAttribute("viewUser", viewUser);
+                request.setAttribute("allCrops", em.createNamedQuery("Organism.findAllCrops").getResultList());
+                request.setAttribute("messageTagSet", em.createNamedQuery("MessageTag.findAll").getResultList());
+                request.setAttribute("universalMessageFormats", SessionControllerGetter.getMessagingBean().getAllUniversalMessageFormats());
+                request.setAttribute("messageKey", request.getParameter("messageKey"));
+                request.getRequestDispatcher("/notificationSubscriptionForm.ftl").forward(request, response);
+
+            }
+            else if(action.equals("notificationSubscriptionFormSubmit"))
+            {
+                Integer userId = Integer.valueOf(request.getParameter("userId"));
+                VipsLogicUser viewUser;
+                if(userBean.authorizeUser(user, VipsLogicRole.ORGANIZATION_ADMINISTRATOR, VipsLogicRole.SUPERUSER))
+                {
+                    viewUser = em.find(VipsLogicUser.class, userId);
+                }
+                else
+                {
+                    viewUser = user;
+                }
+                MessageNotificationSubscription subscription = new MessageNotificationSubscription();
+                subscription.setUniversalMessageFormatId(Integer.valueOf(request.getParameter("universalMessageFormatId")));
+                subscription.setCropIds(FormUtil.getIdsFromMultipleSelect(request.getParameterValues("cropOrganismIds")));
+                subscription.setMessageTagIds(FormUtil.getIdsFromMultipleSelect(request.getParameterValues("messageTagIds")));
+                subscription.setUserId(viewUser.getUserId());
+                SessionControllerGetter.getMessagingBean().storeMessageNotificationSubscription(subscription);
+                // Redirect to form with confirmation message
+                response.sendRedirect(new StringBuilder("http://")
+                            .append(ServletUtil.getServerName(request))
+                            .append("/user/notificationsubscription?userId=").append(viewUser.getUserId())
+                            .append("&messageKey=").append("notificationSubscriptionsUpdated").toString()
+                            
+                    );
+            }
+        }
+        catch(ParseException e)
+        {
+            e.printStackTrace();
+        }
+    }
+
+    // <editor-fold defaultstate="collapsed" desc="HttpServlet methods. Click on the + sign on the left to edit the code.">
+    /**
+     * Handles the HTTP <code>GET</code> method.
+     *
+     * @param request servlet request
+     * @param response servlet response
+     * @throws ServletException if a servlet-specific error occurs
+     * @throws IOException if an I/O error occurs
+     */
+    @Override
+    protected void doGet(HttpServletRequest request, HttpServletResponse response)
+            throws ServletException, IOException {
+        processRequest(request, response);
+    }
+
+    /**
+     * Handles the HTTP <code>POST</code> method.
+     *
+     * @param request servlet request
+     * @param response servlet response
+     * @throws ServletException if a servlet-specific error occurs
+     * @throws IOException if an I/O error occurs
+     */
+    @Override
+    protected void doPost(HttpServletRequest request, HttpServletResponse response)
+            throws ServletException, IOException {
+        processRequest(request, response);
+    }
+
+    /**
+     * Returns a short description of the servlet.
+     *
+     * @return a String containing servlet description
+     */
+    @Override
+    public String getServletInfo() {
+        return "Short description";
+    }// </editor-fold>
+
+}
diff --git a/src/main/java/no/nibio/vips/logic/entity/Message.java b/src/main/java/no/nibio/vips/logic/entity/Message.java
index 286a509d..1e2a36f0 100644
--- a/src/main/java/no/nibio/vips/logic/entity/Message.java
+++ b/src/main/java/no/nibio/vips/logic/entity/Message.java
@@ -22,6 +22,7 @@ package no.nibio.vips.logic.entity;
 import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.Date;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 import javax.persistence.Basic;
@@ -128,6 +129,16 @@ public class Message implements Serializable {
     public Set<MessageTag> getMessageTagSet() {
         return messageTagSet;
     }
+    
+    @Transient
+    public List<Integer> getMessageTagIds(){
+        List<Integer> retVal = new ArrayList<>();
+        for(MessageTag tag: this.getMessageTagSet())
+        {
+            retVal.add(tag.getMessageTagId());
+        }
+        return retVal;
+    }
 
     public void setMessageTagSet(Set<MessageTag> messageTagSet) {
         this.messageTagSet = messageTagSet;
@@ -150,6 +161,15 @@ public class Message implements Serializable {
     public void setMessageLocaleSet(Set<MessageLocale> messageLocaleSet) {
         this.messageLocaleSet = messageLocaleSet;
     }
+    
+    public void addMessageLocale(MessageLocale messageLocale)
+    {
+        if(this.messageLocaleSet == null)
+        {
+            this.messageLocaleSet = new HashSet<>();
+        }
+        this.messageLocaleSet.add(messageLocale);
+    }
 
     @Override
     public int hashCode() {
diff --git a/src/main/java/no/nibio/vips/logic/messaging/MessageLocalVersion.java b/src/main/java/no/nibio/vips/logic/messaging/MessageLocalVersion.java
new file mode 100644
index 00000000..563d7430
--- /dev/null
+++ b/src/main/java/no/nibio/vips/logic/messaging/MessageLocalVersion.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (c) 2016 NIBIO <http://www.nibio.no/>. 
+ * 
+ * This file is part of VIPSLogic.
+ * VIPSLogic is free software: you can redistribute it and/or modify
+ * it under the terms of the NIBIO Open Source License as published by 
+ * NIBIO, either version 1 of the License, or (at your option) any
+ * later version.
+ * 
+ * VIPSLogic 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
+ * NIBIO Open Source License for more details.
+ * 
+ * You should have received a copy of the NIBIO Open Source License
+ * along with VIPSLogic.  If not, see <http://www.nibio.no/licenses/>.
+ * 
+ */
+
+package no.nibio.vips.logic.messaging;
+
+/**
+ * @copyright 2016 <a href="http://www.nibio.no/">NIBIO</a>
+ * @author Tor-Einar Skog <tor-einar.skog@nibio.no>
+ */
+public class MessageLocalVersion {
+    private String locale, msgSubject, msgLeadParagraph, msgBody, msgDownloadUrl;
+
+    public MessageLocalVersion(){}
+    
+    public MessageLocalVersion(
+        String locale,
+        String msgSubject,
+        String msgLeadParagraph,
+        String msgBody,
+        String msgDownloadUrl
+    )
+    {
+        this.locale = locale;
+        this.msgSubject = msgSubject;
+        this.msgLeadParagraph = msgLeadParagraph;
+        this.msgBody = msgBody;
+        this.msgDownloadUrl = msgDownloadUrl;
+    }
+    
+    /**
+     * @return the locale
+     */
+    public String getLocale() {
+        return locale;
+    }
+
+    /**
+     * @param locale the locale to set
+     */
+    public void setLocale(String locale) {
+        this.locale = locale;
+    }
+
+    /**
+     * @return the msgSubject
+     */
+    public String getMsgSubject() {
+        return msgSubject;
+    }
+
+    /**
+     * @param msgSubject the msgSubject to set
+     */
+    public void setMsgSubject(String msgSubject) {
+        this.msgSubject = msgSubject;
+    }
+
+    /**
+     * @return the msgLeadParagraph
+     */
+    public String getMsgLeadParagraph() {
+        return msgLeadParagraph;
+    }
+
+    /**
+     * @param msgLeadParagraph the msgLeadParagraph to set
+     */
+    public void setMsgLeadParagraph(String msgLeadParagraph) {
+        this.msgLeadParagraph = msgLeadParagraph;
+    }
+
+    /**
+     * @return the msgBody
+     */
+    public String getMsgBody() {
+        return msgBody;
+    }
+
+    /**
+     * @param msgBody the msgBody to set
+     */
+    public void setMsgBody(String msgBody) {
+        this.msgBody = msgBody;
+    }
+
+    /**
+     * @return the msgDownloadUrl
+     */
+    public String getMsgDownloadUrl() {
+        return msgDownloadUrl;
+    }
+
+    /**
+     * @param msgDownloadUrl the msgDownloadUrl to set
+     */
+    public void setMsgDownloadUrl(String msgDownloadUrl) {
+        this.msgDownloadUrl = msgDownloadUrl;
+    }
+    
+}
diff --git a/src/main/java/no/nibio/vips/logic/messaging/MessageNotificationSubscription.java b/src/main/java/no/nibio/vips/logic/messaging/MessageNotificationSubscription.java
new file mode 100644
index 00000000..ebc36e81
--- /dev/null
+++ b/src/main/java/no/nibio/vips/logic/messaging/MessageNotificationSubscription.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (c) 2016 NIBIO <http://www.nibio.no/>. 
+ * 
+ * This file is part of VIPSLogic.
+ * VIPSLogic is free software: you can redistribute it and/or modify
+ * it under the terms of the NIBIO Open Source License as published by 
+ * NIBIO, either version 1 of the License, or (at your option) any
+ * later version.
+ * 
+ * VIPSLogic 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
+ * NIBIO Open Source License for more details.
+ * 
+ * You should have received a copy of the NIBIO Open Source License
+ * along with VIPSLogic.  If not, see <http://www.nibio.no/licenses/>.
+ * 
+ */
+
+package no.nibio.vips.logic.messaging;
+
+import java.io.Serializable;
+import java.util.List;
+import javax.persistence.Basic;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import javax.persistence.Table;
+import no.nibio.vips.logic.util.IntegerArrayUserType;
+import org.hibernate.annotations.Type;
+import org.hibernate.annotations.TypeDef;
+import org.hibernate.annotations.TypeDefs;
+
+/**
+ * @copyright 2016 <a href="http://www.nibio.no/">NIBIO</a>
+ * @author Tor-Einar Skog <tor-einar.skog@nibio.no>
+ */
+@Entity
+@Table(name = "message_notification_subscription", schema = "messaging")
+@TypeDefs( {@TypeDef( name= "IntegerArray", typeClass = IntegerArrayUserType.class)})
+public class MessageNotificationSubscription implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    @Basic(optional = false)
+    @Column(name = "user_id")
+    private Integer userId;
+    
+    @Column(name = "universal_message_format_id")
+    private Integer universalMessageFormatId;
+    
+    @Type(type = "IntegerArray")
+    @Column(name = "message_tag_ids")
+    private Integer[] messageTagIds;
+    
+    @Type(type = "IntegerArray")
+    @Column(name = "crop_ids")
+    private Integer[] cropIds;
+
+    public Integer getUserId() {
+        return this.userId;
+    }
+
+    public void setUserId(Integer userId) {
+        this.userId = userId;
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = 0;
+        hash += (userId != null ? userId.hashCode() : 0);
+        return hash;
+    }
+
+    @Override
+    public boolean equals(Object object) {
+        // TODO: Warning - this method won't work in the case the id fields are not set
+        if (!(object instanceof MessageNotificationSubscription)) {
+            return false;
+        }
+        MessageNotificationSubscription other = (MessageNotificationSubscription) object;
+        if ((this.userId == null && other.userId != null) || (this.userId != null && !this.userId.equals(other.userId))) {
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    public String toString() {
+        return "no.nibio.vips.logic.messaging.MessageNotificationSubscription[ userId=" + userId + " ]";
+    }
+
+    /**
+     * @return the universalMessageFormatId
+     */
+    public Integer getUniversalMessageFormatId() {
+        return universalMessageFormatId;
+    }
+
+    /**
+     * @param universalMessageFormatId the universalMessageFormatId to set
+     */
+    public void setUniversalMessageFormatId(Integer universalMessageFormatId) {
+        this.universalMessageFormatId = universalMessageFormatId;
+    }
+
+    /**
+     * @return the messageTagIds
+     */
+    public Integer[] getMessageTagIds() {
+        return messageTagIds;
+    }
+
+    /**
+     * @param messageTagIds the messageTagIds to set
+     */
+    public void setMessageTagIds(Integer[] messageTagIds) {
+        this.messageTagIds = messageTagIds;
+    }
+    
+    /**
+     * For your convenience
+     * @param messageTagIds 
+     */
+    public void setMessageTagIds(List<Integer> messageTagIds)
+    {
+        this.messageTagIds = messageTagIds.toArray(new Integer[messageTagIds.size()]);
+    }
+
+    /**
+     * @return the cropIds
+     */
+    public Integer[] getCropIds() {
+        return cropIds;
+    }
+
+    /**
+     * @param cropIds the cropIds to set
+     */
+    public void setCropIds(Integer[] cropIds) {
+        this.cropIds = cropIds;
+    }
+    
+    public void setCropIds(List<Integer> cropIds)
+    {
+        this.cropIds = cropIds.toArray(new Integer[cropIds.size()]);
+    }
+
+}
diff --git a/src/main/java/no/nibio/vips/logic/messaging/MessageRecipient.java b/src/main/java/no/nibio/vips/logic/messaging/MessageRecipient.java
index 9d229880..9af4c996 100644
--- a/src/main/java/no/nibio/vips/logic/messaging/MessageRecipient.java
+++ b/src/main/java/no/nibio/vips/logic/messaging/MessageRecipient.java
@@ -19,27 +19,42 @@
 
 package no.nibio.vips.logic.messaging;
 
+import java.io.Serializable;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.Id;
+
 /**
- * @copyright 2015 <a href="http://www.nibio.no/">NIBIO</a>
+ * @copyright 2016 <a href="http://www.nibio.no/">NIBIO</a>
  * @author Tor-Einar Skog <tor-einar.skog@nibio.no>
  */
-public class MessageRecipient {
+@Entity
+public class MessageRecipient implements Serializable {
+    @Column(name = "preferred_locale")
+    private String preferredLocale;
     private String type;
+    @Column(name = "msg_delivery_address")
     private String msgDeliveryAddress;
     private String name;
+    @Id
+    @Column(name = "recipient_id")
     private String recipientId;
     
+    public MessageRecipient(){}
+    
     public MessageRecipient(
             String recipientId,
             String name,
             String type,
-            String msgDeliveryAddress
+            String msgDeliveryAddress,
+            String preferredLocale
     )
     {
         this.recipientId = recipientId;
         this.name = name;
         this.type=type;
         this.msgDeliveryAddress = msgDeliveryAddress;
+        this.preferredLocale = preferredLocale;
     }
 
     /**
@@ -97,4 +112,18 @@ public class MessageRecipient {
     public void setRecipientId(String recipientId) {
         this.recipientId = recipientId;
     }
+
+    /**
+     * @return the preferredLocale
+     */
+    public String getPreferredLocale() {
+        return preferredLocale;
+    }
+
+    /**
+     * @param preferredLocale the preferredLocale to set
+     */
+    public void setPreferredLocale(String preferredLocale) {
+        this.preferredLocale = preferredLocale;
+    }
 }
diff --git a/src/main/java/no/nibio/vips/logic/messaging/MessagingBean.java b/src/main/java/no/nibio/vips/logic/messaging/MessagingBean.java
index e95f13d2..618644e2 100644
--- a/src/main/java/no/nibio/vips/logic/messaging/MessagingBean.java
+++ b/src/main/java/no/nibio/vips/logic/messaging/MessagingBean.java
@@ -19,16 +19,26 @@
 
 package no.nibio.vips.logic.messaging;
 
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
 import java.util.ArrayList;
 import java.util.Date;
 import java.util.List;
 import javax.ejb.Stateless;
 import javax.persistence.EntityManager;
+import javax.persistence.NoResultException;
 import javax.persistence.PersistenceContext;
+import javax.persistence.Query;
+import javax.persistence.TemporalType;
+import javax.ws.rs.core.Response;
 import no.nibio.vips.logic.entity.Message;
 import no.nibio.vips.logic.entity.MessageLocale;
 import no.nibio.vips.logic.entity.VipsLogicUser;
+import no.nibio.vips.logic.util.RESTAuthenticator;
 import no.nibio.vips.logic.util.SessionControllerGetter;
+import org.jboss.resteasy.client.jaxrs.ResteasyClient;
+import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder;
+import org.jboss.resteasy.client.jaxrs.ResteasyWebTarget;
 
 /**
  * @copyright 2015 <a href="http://www.nibio.no/">NIBIO</a>
@@ -45,7 +55,7 @@ public class MessagingBean {
         Message m = SessionControllerGetter.getMessageBean().getMessage(5);
         MessageLocale ml = m.getLocalMessage("nn");
         UniversalMessage um = new UniversalMessage(
-                ml.getHeading(), ml.getLeadParagraph(), ml.getBody(), "http://localhost:8000/messages/" + m.getMessageId()
+                "nn",ml.getHeading(), ml.getLeadParagraph(), ml.getBody(), "http://localhost:8000/messages/" + m.getMessageId()
         );
         um.setExpiresAt(new Date());
         List<MessageRecipient> distributionList = new ArrayList<>();
@@ -55,7 +65,8 @@ public class MessagingBean {
                     String.valueOf(user.getUserId()),
                     user.getFirstName() + " " + user.getLastName(), 
                     "Mail", 
-                    user.getEmail()
+                    user.getEmail(),
+                    user.getPreferredLocale()
             );
             distributionList.add(r);
         }
@@ -65,22 +76,119 @@ public class MessagingBean {
         List<UniversalMessage> ums = em.createNamedQuery("UniversalMessage.findAll").getResultList();
         for(UniversalMessage uMe : ums)
         {
-            System.out.println(uMe.getMsgSubject());
+            for(MessageLocalVersion lv:uMe.getMessageLocalVersionObjects())
+            {
+                System.out.println(lv.getMsgSubject());
+            }
+            
         }
     }
     
     public void sendUniversalMessage(Message message)
     {
+        String msgDownloadUrlTpl = "http://www.vips-landbruk.no/";
         // Create a universal message from the message
         // TODO: When UniversalMessage has changed, pick
-        MessageLocale ml = message.getLocalMessageWithFallback("nb", "nb");
+        UniversalMessage uMessage = new UniversalMessage();
+        uMessage.setExpiresAt(message.getDateValidTo());
+        for(MessageLocale ml : message.getMessageLocaleSet())
+        {
+            uMessage.addMessageLocalVersion(ml.getMessageLocalePK().getLocale(), ml.getHeading(), ml.getLeadParagraph(), ml.getBody(), msgDownloadUrlTpl);
+        }
+        
         // Find the suscribers, create distribution list
+        uMessage.setDistributionList(this.getMessageNotificationSubscribers(message));
+        
+        // Send it
+        this.sendUniversalMessage(uMessage);
+        // Log it
+        
+    }
+    
+    public void sendUniversalMessage(UniversalMessage uMessage)
+    {
+        if(uMessage.getDistributionListObjects().isEmpty())
+        {
+            return;
+        }
         
         // Store it
+        em.persist(uMessage);
         
-        // Send it
+        ResteasyClient client = new ResteasyClientBuilder().build();
+        //client.register(new RESTAuthenticator("user", "userPass"));
+        client.register(new RESTAuthenticator("VIPSLogic", "plmoknijbuhv000"));
+        //ResteasyWebTarget target = client.target("http://kart13utv.ad.skogoglandskap.no:8080");
+        ResteasyWebTarget target = client.target("http://localhost:8080");
+        //ResteasyWebTarget target = client.target("http://logic.testvips2.nibio.no");
+        UniversalMessagingServiceClient umClient = target.proxy(UniversalMessagingServiceClient.class);
+
+
+        try
+        {
+        //System.out.println(new ObjectMapper().writeValueAsString(um));
+        Response r = umClient.sendMessage(new ObjectMapper().writeValueAsString(uMessage));
         
-        // Log it
+        // TODO: Handle result
+
+        String result = r.readEntity(String.class);
+        }
+        catch(JsonProcessingException ex)
+        {
+            // Handle error
+        }
+    }
+
+    private List<MessageRecipient> getMessageNotificationSubscribers(Message message) {
+        
+        Query q = em.createNativeQuery(
+                "SELECT \n" +
+                "	u.preferred_locale,\n" +
+                "	umf.format_name AS type,\n" +
+                "	CASE mns.universal_message_format_id " + 
+                "           WHEN " + UniversalMessageFormat.FORMAT_EMAIL + " THEN u.email " +
+                "           WHEN " + UniversalMessageFormat.FORMAT_SMS + " THEN u.phone " +
+                "           ELSE '' " +
+                "       END AS msg_delivery_address, \n" + // Needs update as more options are added
+                "	u.first_name || ' ' || u.last_name AS name,\n" +
+                "	u.user_id AS recipient_id\n" +
+                "FROM public.vips_logic_user u, messaging.message_notification_subscription mns, messaging.universal_message_format umf\n" +
+                "WHERE mns.user_id=u.user_id\n" +
+                "AND mns.universal_message_format_id = umf.universal_message_format_id\n" +
+                "AND u.user_id IN (\n" +
+                "	SELECT user_id FROM messaging.message_notification_subscription\n" +
+                "	WHERE  message_tag_ids && ARRAY" + message.getMessageTagIds().toString() + " \n" + // && is the array_overlaps operator
+                ");\n",
+                MessageRecipient.class);
         
+        //q.setParameter("messageTagIds", message.getMessageTagIds());
+        return q.getResultList();
+    }
+    
+    public MessageNotificationSubscription getMessageNotificationSubscription(Integer userId)
+    {
+        Query q = em.createNativeQuery(
+                "SELECT * FROM messaging.message_notification_subscription m "
+                + "WHERE m.user_id=:userId",
+                MessageNotificationSubscription.class
+        ).setParameter("userId", userId);
+        try
+        {
+            return (MessageNotificationSubscription) q.getSingleResult();
+        }
+        catch(NoResultException ex)
+        {
+            return null;
+        }
+    }
+    
+    public void storeMessageNotificationSubscription(MessageNotificationSubscription subscription)
+    {
+        em.merge(subscription);
+    }
+    
+    public List<UniversalMessageFormat> getAllUniversalMessageFormats()
+    {
+        return em.createNamedQuery("UniversalMessageFormat.findAll").getResultList();
     }
 }
diff --git a/src/main/java/no/nibio/vips/logic/messaging/UniversalMessage.java b/src/main/java/no/nibio/vips/logic/messaging/UniversalMessage.java
index f9381958..1e0ea75f 100644
--- a/src/main/java/no/nibio/vips/logic/messaging/UniversalMessage.java
+++ b/src/main/java/no/nibio/vips/logic/messaging/UniversalMessage.java
@@ -20,18 +20,19 @@
 package no.nibio.vips.logic.messaging;
 
 import com.fasterxml.jackson.annotation.JsonIgnore;
-import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.annotation.JsonRawValue;
 import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
 import com.fasterxml.jackson.databind.ObjectMapper;
+import java.io.IOException;
 import java.io.Serializable;
+import java.util.ArrayList;
 import java.util.Date;
 import java.util.List;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 import javax.persistence.Basic;
 import javax.persistence.Column;
-import javax.persistence.Convert;
 import javax.persistence.Entity;
 import javax.persistence.GeneratedValue;
 import javax.persistence.GenerationType;
@@ -41,16 +42,15 @@ import javax.persistence.NamedQuery;
 import javax.persistence.Table;
 import javax.persistence.Temporal;
 import javax.persistence.TemporalType;
-import javax.validation.constraints.Size;
+import javax.persistence.Transient;
 import javax.xml.bind.annotation.XmlRootElement;
-import no.nibio.vips.logic.util.PostgresJSONStringConverter;
 import no.nibio.vips.logic.util.StringJsonUserType;
 import org.hibernate.annotations.Type;
 import org.hibernate.annotations.TypeDef;
 import org.hibernate.annotations.TypeDefs;
 
 /**
- * @copyright 2015 <a href="http://www.nibio.no/">NIBIO</a>
+ * @copyright 2016 <a href="http://www.nibio.no/">NIBIO</a>
  * @author Tor-Einar Skog <tor-einar.skog@nibio.no>
  */
 @Entity
@@ -60,10 +60,6 @@ import org.hibernate.annotations.TypeDefs;
 @NamedQueries({
     @NamedQuery(name = "UniversalMessage.findAll", query = "SELECT u FROM UniversalMessage u"),
     @NamedQuery(name = "UniversalMessage.findByUniversalMessageId", query = "SELECT u FROM UniversalMessage u WHERE u.universalMessageId = :universalMessageId"),
-    @NamedQuery(name = "UniversalMessage.findByMsgSubject", query = "SELECT u FROM UniversalMessage u WHERE u.msgSubject = :msgSubject"),
-    @NamedQuery(name = "UniversalMessage.findByMsgLeadParagraph", query = "SELECT u FROM UniversalMessage u WHERE u.msgLeadParagraph = :msgLeadParagraph"),
-    @NamedQuery(name = "UniversalMessage.findByMsgBody", query = "SELECT u FROM UniversalMessage u WHERE u.msgBody = :msgBody"),
-    @NamedQuery(name = "UniversalMessage.findByMsgDownloadUrl", query = "SELECT u FROM UniversalMessage u WHERE u.msgDownloadUrl = :msgDownloadUrl"),
     @NamedQuery(name = "UniversalMessage.findByExpiresAt", query = "SELECT u FROM UniversalMessage u WHERE u.expiresAt = :expiresAt")})
 public class UniversalMessage implements Serializable {
     private static final long serialVersionUID = 1L;
@@ -72,18 +68,6 @@ public class UniversalMessage implements Serializable {
     @Basic(optional = false)
     @Column(name = "universal_message_id")
     private Integer universalMessageId;
-    @Size(max = 2047)
-    @Column(name = "msg_subject")
-    private String msgSubject;
-    @Size(max = 2147483647)
-    @Column(name = "msg_lead_paragraph")
-    private String msgLeadParagraph;
-    @Size(max = 2147483647)
-    @Column(name = "msg_body")
-    private String msgBody;
-    @Size(max = 1023)
-    @Column(name = "msg_download_url")
-    private String msgDownloadUrl;
     @Column(name = "expires_at")
     @Temporal(TemporalType.TIMESTAMP)
     private Date expiresAt;
@@ -94,21 +78,41 @@ public class UniversalMessage implements Serializable {
     @Column(name = "distribution_list")
     private String distributionList;
     
+    @Type(type = "StringJsonObject")
+    @Column(name = "message_local_versions")
+    private String messageLocalVersions;
+    
+    @Transient
+    private ObjectMapper objectMapper;
+    
 
     public UniversalMessage() {
     }
     
     public UniversalMessage(
-            String msgSubject,
-            String msgLeadParagraph,
-            String msgBody,
-            String msgDownloadUrl
+        String locale,
+        String msgSubject,
+        String msgLeadParagraph,
+        String msgBody,
+        String msgDownloadUrl
+    )
+    {
+        this.addMessageLocalVersion(locale, msgSubject, msgLeadParagraph, msgBody, msgDownloadUrl);
+    }
+    
+    public final void addMessageLocalVersion(
+        String locale,
+        String msgSubject,
+        String msgLeadParagraph,
+        String msgBody,
+        String msgDownloadUrl
     )
     {
-        this.msgSubject = msgSubject;
-        this.msgLeadParagraph = msgLeadParagraph;
-        this.msgBody = msgBody;
-        this.msgDownloadUrl = msgDownloadUrl;
+        
+        MessageLocalVersion newLocalVersion = new MessageLocalVersion(locale, msgSubject, msgLeadParagraph, msgBody, msgDownloadUrl);
+        List<MessageLocalVersion> existingLocalVersions = this.getMessageLocalVersionObjects();
+        existingLocalVersions.add(newLocalVersion);
+        this.setMessageLocalVersions(existingLocalVersions);
     }
 
     public UniversalMessage(Integer universalMessageId) {
@@ -124,38 +128,6 @@ public class UniversalMessage implements Serializable {
         this.universalMessageId = universalMessageId;
     }
 
-    public String getMsgSubject() {
-        return msgSubject;
-    }
-
-    public void setMsgSubject(String msgSubject) {
-        this.msgSubject = msgSubject;
-    }
-
-    public String getMsgLeadParagraph() {
-        return msgLeadParagraph;
-    }
-
-    public void setMsgLeadParagraph(String msgLeadParagraph) {
-        this.msgLeadParagraph = msgLeadParagraph;
-    }
-
-    public String getMsgBody() {
-        return msgBody;
-    }
-
-    public void setMsgBody(String msgBody) {
-        this.msgBody = msgBody;
-    }
-
-    public String getMsgDownloadUrl() {
-        return msgDownloadUrl;
-    }
-
-    public void setMsgDownloadUrl(String msgDownloadUrl) {
-        this.msgDownloadUrl = msgDownloadUrl;
-    }
-
     @JsonIgnore
     public Date getExpiresAt() {
         return expiresAt;
@@ -176,11 +148,24 @@ public class UniversalMessage implements Serializable {
     
     public void setDistributionList(List<MessageRecipient> distributionList) {
         try {
-            this.distributionList = new ObjectMapper().writeValueAsString(distributionList);
+            this.distributionList = this.getObjectMapper().writeValueAsString(distributionList);
         } catch (JsonProcessingException ex) {
             Logger.getLogger(UniversalMessage.class.getName()).log(Level.SEVERE, null, ex);
         }
     }
+    
+    @JsonIgnore
+    @Transient
+    public List<MessageRecipient> getDistributionListObjects()
+    {
+        List<MessageRecipient> retVal = null;
+        try {
+            retVal = this.getObjectMapper().readValue(this.getDistributionList(), new TypeReference<List<MessageRecipient>>(){});
+        } catch (IOException | NullPointerException ex) {
+            Logger.getLogger(UniversalMessage.class.getName()).log(Level.SEVERE, null, ex);
+        }
+        return retVal != null ? retVal : new ArrayList<MessageRecipient>();
+    }
 
     @Override
     public int hashCode() {
@@ -207,4 +192,51 @@ public class UniversalMessage implements Serializable {
         return "no.nibio.vips.logic.messaging.UniversalMessage[ universalMessageId=" + universalMessageId + " ]";
     }
 
+    /**
+     * @return the messageLocalVersions
+     */
+    @JsonRawValue
+    public String getMessageLocalVersions() {
+        return messageLocalVersions != null ? this.messageLocalVersions : "[]";
+    }
+    
+    @JsonIgnore
+    @Transient
+    public List<MessageLocalVersion> getMessageLocalVersionObjects()
+    {
+        
+        List<MessageLocalVersion> retVal = null;
+        try {
+            retVal = this.getObjectMapper().readValue(this.getMessageLocalVersions(), new TypeReference<List<MessageLocalVersion>>(){});
+        } catch (IOException | NullPointerException ex) {
+            Logger.getLogger(UniversalMessage.class.getName()).log(Level.SEVERE, null, ex);
+        }
+        return retVal != null ? retVal : new ArrayList<MessageLocalVersion>();
+    }
+
+    /**
+     * @param messageLocalVersions the messageLocalVersions to set
+     */
+    public void setMessageLocalVersions(String messageLocalVersions) {
+        this.messageLocalVersions = messageLocalVersions;
+    }
+    
+    public void setMessageLocalVersions(List<MessageLocalVersion> messageLocalVersions)
+    {
+        try {
+            this.messageLocalVersions = this.getObjectMapper().writeValueAsString(messageLocalVersions);
+        } catch (JsonProcessingException ex) {
+            Logger.getLogger(UniversalMessage.class.getName()).log(Level.SEVERE, null, ex);
+        }
+    }
+
+    
+    private ObjectMapper getObjectMapper()
+    {
+        if(this.objectMapper == null)
+        {
+            this.objectMapper = new ObjectMapper();
+        }
+        return this.objectMapper;
+    }
 }
diff --git a/src/main/java/no/nibio/vips/logic/messaging/UniversalMessageFormat.java b/src/main/java/no/nibio/vips/logic/messaging/UniversalMessageFormat.java
new file mode 100644
index 00000000..c95a6e32
--- /dev/null
+++ b/src/main/java/no/nibio/vips/logic/messaging/UniversalMessageFormat.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) 2016 NIBIO <http://www.nibio.no/>. 
+ * 
+ * This file is part of VIPSLogic.
+ * VIPSLogic is free software: you can redistribute it and/or modify
+ * it under the terms of the NIBIO Open Source License as published by 
+ * NIBIO, either version 1 of the License, or (at your option) any
+ * later version.
+ * 
+ * VIPSLogic 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
+ * NIBIO Open Source License for more details.
+ * 
+ * You should have received a copy of the NIBIO Open Source License
+ * along with VIPSLogic.  If not, see <http://www.nibio.no/licenses/>.
+ * 
+ */
+
+package no.nibio.vips.logic.messaging;
+
+import java.io.Serializable;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import javax.persistence.NamedQueries;
+import javax.persistence.NamedQuery;
+import javax.persistence.Table;
+
+/**
+ * @copyright 2016 <a href="http://www.nibio.no/">NIBIO</a>
+ * @author Tor-Einar Skog <tor-einar.skog@nibio.no>
+ */
+@Entity
+@Table(name = "universal_message_format", schema = "messaging")
+@NamedQueries({
+    @NamedQuery(name = "UniversalMessageFormat.findAll", query = "SELECT u FROM UniversalMessageFormat u")
+})
+public class UniversalMessageFormat implements Serializable{
+    
+    public static final Integer FORMAT_EMAIL = 1;
+    public static final Integer FORMAT_SMS = 2;
+    @Id
+    @Column(name = "universal_message_format_id")
+    private Integer universalMessageFormatId;
+    @Column(name = "format_name")
+    private String formatName;
+    
+
+    /**
+     * @return the universalMessageFormatId
+     */
+    public Integer getUniversalMessageFormatId() {
+        return universalMessageFormatId;
+    }
+
+    /**
+     * @param universalMessageFormatId the universalMessageFormatId to set
+     */
+    public void setUniversalMessageFormatId(Integer universalMessageFormatId) {
+        this.universalMessageFormatId = universalMessageFormatId;
+    }
+
+    /**
+     * @return the formatName
+     */
+    public String getFormatName() {
+        return formatName;
+    }
+
+    /**
+     * @param formatName the formatName to set
+     */
+    public void setFormatName(String formatName) {
+        this.formatName = formatName;
+    }
+}
diff --git a/src/main/java/no/nibio/vips/logic/util/IntegerArrayUserType.java b/src/main/java/no/nibio/vips/logic/util/IntegerArrayUserType.java
new file mode 100644
index 00000000..1a765f70
--- /dev/null
+++ b/src/main/java/no/nibio/vips/logic/util/IntegerArrayUserType.java
@@ -0,0 +1,213 @@
+/*
+ * Copyright (c) 2015 NIBIO <http://www.nibio.no/>. 
+ * 
+ * This file is part of VIPSLogic.
+ * VIPSLogic is free software: you can redistribute it and/or modify
+ * it under the terms of the NIBIO Open Source License as published by 
+ * NIBIO, either version 1 of the License, or (at your option) any
+ * later version.
+ * 
+ * VIPSLogic 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
+ * NIBIO Open Source License for more details.
+ * 
+ * You should have received a copy of the NIBIO Open Source License
+ * along with VIPSLogic.  If not, see <http://www.nibio.no/licenses/>.
+ * 
+ */
+
+package no.nibio.vips.logic.util;
+
+import java.io.Serializable;
+import java.sql.Array;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Types;
+import org.hibernate.HibernateException;
+import org.hibernate.engine.spi.SessionImplementor;
+import org.hibernate.usertype.UserType;
+
+/**
+ * Adapted from this: http://stackoverflow.com/questions/21940642/hibernate-postgres-array-type
+ * @copyright 2015 <a href="http://www.nibio.no/">NIBIO</a>
+ * @author Tor-Einar Skog <tor-einar.skog@nibio.no>
+ */
+
+public class IntegerArrayUserType implements UserType {
+
+    /**
+     * Return the SQL type codes for the columns mapped by this type. The
+     * codes are defined on <tt>java.sql.Types</tt>.
+     *
+     * @return int[] the typecodes
+     * @see java.sql.Types
+     */
+    @Override
+    public int[] sqlTypes() {
+        return new int[] { Types.ARRAY };
+    }
+
+    /**
+     * The class returned by <tt>nullSafeGet()</tt>.
+     *
+     * @return Class
+     */
+    @Override
+    public Class returnedClass() {
+        return Integer[].class;
+    }
+
+    /**
+     * Compare two instances of the class mapped by this type for persistence "equality".
+     * Equality of the persistent state.
+     *
+     * @param x
+     * @param y
+     * @return boolean
+     */
+    @Override
+    public boolean equals(Object x, Object y) throws HibernateException {
+
+        if( x== null){
+
+            return y== null;
+        }
+
+        return x.equals( y);
+    }
+
+    /**
+     * Get a hashcode for the instance, consistent with persistence "equality"
+     */
+    @Override
+    public int hashCode(Object x) throws HibernateException {
+
+        return x.hashCode();
+    }
+
+    /**
+     * Retrieve an instance of the mapped class from a JDBC resultset. Implementors
+     * should handle possibility of null values.
+     *
+     * @param rs      a JDBC result set
+     * @param names   the column names
+     * @param session
+     * @param owner   the containing entity  @return Object
+     * @throws org.hibernate.HibernateException
+     *
+     * @throws java.sql.SQLException
+     */
+    @Override
+    public Object nullSafeGet(
+            ResultSet rs, 
+            String[] names, 
+            SessionImplementor session, 
+            Object owner) throws HibernateException, SQLException {
+       if (rs.wasNull()) {
+        return null;
+    }
+
+    Integer[] array = (Integer[]) rs.getArray(names[0]).getArray();
+    return array;
+    }
+
+    /**
+     * Write an instance of the mapped class to a prepared statement. Implementors
+     * should handle possibility of null values. A multi-column type should be written
+     * to parameters starting from <tt>index</tt>.
+     *
+     * @param st      a JDBC prepared statement
+     * @param value   the object to write
+     * @param index   statement parameter index
+     * @param session
+     * @throws org.hibernate.HibernateException
+     *
+     * @throws java.sql.SQLException
+     */
+    @Override
+    public void nullSafeSet(PreparedStatement st, Object value, int index, SessionImplementor session) throws HibernateException, SQLException {
+        if (value == null) {
+            st.setNull(index, Types.OTHER);
+            return;
+        }
+
+        Integer[] castObject = (Integer[]) value;
+        Array array = session.connection().createArrayOf("integer", castObject); // The postgres array data type
+        st.setArray(index, array);
+    }
+
+    /**
+     * Return a deep copy of the persistent state, stopping at entities and at
+     * collections. It is not necessary to copy immutable objects, or null
+     * values, in which case it is safe to simply return the argument.
+     *
+     * @param value the object to be cloned, which may be null
+     * @return Object a copy
+     */
+    @Override
+    public Object deepCopy(Object value) throws HibernateException {
+
+        return value;
+    }
+
+    /**
+     * Are objects of this type mutable?
+     *
+     * @return boolean
+     */
+    @Override
+    public boolean isMutable() {
+        return true;
+    }
+
+    /**
+     * Transform the object into its cacheable representation. At the very least this
+     * method should perform a deep copy if the type is mutable. That may not be enough
+     * for some implementations, however; for example, associations must be cached as
+     * identifier values. (optional operation)
+     *
+     * @param value the object to be cached
+     * @return a cachable representation of the object
+     * @throws org.hibernate.HibernateException
+     *
+     */
+    @Override
+    public Serializable disassemble(Object value) throws HibernateException {
+        return (Integer[])this.deepCopy( value);
+    }
+
+    /**
+     * Reconstruct an object from the cacheable representation. At the very least this
+     * method should perform a deep copy if the type is mutable. (optional operation)
+     *
+     * @param cached the object to be cached
+     * @param owner  the owner of the cached object
+     * @return a reconstructed object from the cachable representation
+     * @throws org.hibernate.HibernateException
+     *
+     */
+    @Override
+    public Object assemble(Serializable cached, Object owner) throws HibernateException {
+        return this.deepCopy( cached);
+    }
+
+    /**
+     * During merge, replace the existing (target) value in the entity we are merging to
+     * with a new (original) value from the detached entity we are merging. For immutable
+     * objects, or null values, it is safe to simply return the first parameter. For
+     * mutable objects, it is safe to return a copy of the first parameter. For objects
+     * with component values, it might make sense to recursively replace component values.
+     *
+     * @param original the value from the detached entity being merged
+     * @param target   the value in the managed entity
+     * @return the value to be merged
+     */
+    @Override
+    public Object replace(Object original, Object target, Object owner) throws HibernateException {
+        return original;
+    }
+
+
+}
diff --git a/src/main/resources/META-INF/persistence.xml b/src/main/resources/META-INF/persistence.xml
index 260cd06e..bfaf5ef4 100644
--- a/src/main/resources/META-INF/persistence.xml
+++ b/src/main/resources/META-INF/persistence.xml
@@ -21,12 +21,12 @@
 -->
 <persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
   <persistence-unit name="VIPSLogic-PU" transaction-type="JTA">
-      <provider>org.hibernate.ejb.HibernatePersistence</provider>
+    <provider>org.hibernate.ejb.HibernatePersistence</provider>
     <jta-data-source>java:/jboss/datasources/vipslogic</jta-data-source>
+    <class>no.nibio.vips.logic.messaging.MessageNotificationSubscription</class>
     <exclude-unlisted-classes>false</exclude-unlisted-classes>
-    
     <properties>
-        <property name="hibernate.dialect" value="org.hibernate.spatial.dialect.postgis.PostgisDialect"/>
+      <property name="hibernate.dialect" value="org.hibernate.spatial.dialect.postgis.PostgisDialect"/>
     </properties>
   </persistence-unit>
 </persistence>
diff --git a/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts.properties b/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts.properties
index add2ccfd..1a8499fd 100644
--- a/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts.properties
+++ b/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts.properties
@@ -294,3 +294,13 @@ point=Point
 polygon=Polygon
 clearAll=Clear all
 clearOne=Clear one
+phone=Telephone
+notificationSubscriptions=Notification subscriptions
+userAccountInformation=User account information
+notificationSubscriptionsUpdated=Notification subscriptions was successfully updated
+messageNotifications=Message notifications
+forecastNotifications=Forecast notifications
+notificationSubscriptionDescription=You can subscribe to different kinds of notifications below. Choose the format for notification, for instance email or SMS
+messageFormat=Message format
+Mail=Email
+Sms=SMS
diff --git a/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_bs.properties b/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_bs.properties
index 8aea8010..e5d8895c 100644
--- a/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_bs.properties
+++ b/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_bs.properties
@@ -294,3 +294,13 @@ point=point
 polygon=Polygon
 clearAll=Clear all
 clearOne=Clear one
+phone=Telephone
+notificationSubscriptions=Notification subscriptions
+userAccountInformation=User account information
+notificationSubscriptionsUpdated=Notification subscriptions was successfully updated
+messageNotifications=Message notifications
+forecastNotifications=Forecast notifications
+notificationSubscriptionDescription=You can subscribe to different kinds of notifications below. Choose the format for notification, for instance email or SMS
+messageFormat=Message format
+Mail=Email
+Sms=SMS
diff --git a/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_hr.properties b/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_hr.properties
index d7988747..7d7af415 100644
--- a/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_hr.properties
+++ b/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_hr.properties
@@ -293,3 +293,13 @@ point=point
 polygon=Polygon
 clearAll=Clear all
 clearOne=Clear one
+phone=Telephone
+notificationSubscriptions=Notification subscriptions
+userAccountInformation=User account information
+notificationSubscriptionsUpdated=Notification subscriptions was successfully updated
+messageNotifications=Message notifications
+forecastNotifications=Forecast notifications
+notificationSubscriptionDescription=You can subscribe to different kinds of notifications below. Choose the format for notification, for instance email or SMS
+messageFormat=Message format
+Mail=Email
+Sms=SMS
diff --git a/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_nb.properties b/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_nb.properties
index e48c63d1..f948f783 100644
--- a/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_nb.properties
+++ b/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_nb.properties
@@ -294,3 +294,13 @@ point=Punkt
 polygon=Omr\u00e5de
 clearAll=Slett alt
 clearOne=Slett ett element
+phone=Telefon
+notificationSubscriptions=Meldingsabonnement
+userAccountInformation=Brukerkontoinformasjon
+notificationSubscriptionsUpdated=Meldingsabonnementene ble oppdatert
+messageNotifications=Meldingsmeldinger
+forecastNotifications=Varslingsmeldinger
+notificationSubscriptionDescription=Du kan abonnere p\u00e5 ulike typer meldinger nedenfor. Velg meldingsformat, for eksempel e-post eller SMS.
+messageFormat=Meldingsformat
+Mail=E-post
+Sms=SMS
diff --git a/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_sr.properties b/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_sr.properties
index 88fc8bce..a16fa416 100644
--- a/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_sr.properties
+++ b/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_sr.properties
@@ -294,3 +294,13 @@ point=point
 polygon=Polygon
 clearAll=Clear all
 clearOne=Clear one
+phone=Telephone
+notificationSubscriptions=Notification subscriptions
+userAccountInformation=User account information
+notificationSubscriptionsUpdated=Notification subscriptions was successfully updated
+messageNotifications=Message notifications
+forecastNotifications=Forecast notifications
+notificationSubscriptionDescription=You can subscribe to different kinds of notifications below. Choose the format for notification, for instance email or SMS
+messageFormat=Message format
+Mail=Email
+Sms=SMS
diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml
index f658a7b2..90687a86 100644
--- a/src/main/webapp/WEB-INF/web.xml
+++ b/src/main/webapp/WEB-INF/web.xml
@@ -63,6 +63,10 @@
         <servlet-name>ObservationServlet</servlet-name>
         <servlet-class>no.nibio.vips.logic.controller.servlet.ObservationController</servlet-class>
     </servlet>
+    <servlet>
+        <servlet-name>NotificationSubscriptionController</servlet-name>
+        <servlet-class>no.nibio.vips.logic.controller.servlet.NotificationSubscriptionController</servlet-class>
+    </servlet>
     <servlet-mapping>
         <servlet-name>PointOfInterestController</servlet-name>
         <url-pattern>/poi/*</url-pattern>
@@ -111,6 +115,10 @@
         <servlet-name>ObservationServlet</servlet-name>
         <url-pattern>/observation</url-pattern>
     </servlet-mapping>
+    <servlet-mapping>
+        <servlet-name>NotificationSubscriptionController</servlet-name>
+        <url-pattern>/user/notificationsubscription</url-pattern>
+    </servlet-mapping>
     <welcome-file-list>
         <welcome-file>index.html</welcome-file>
     </welcome-file-list>
diff --git a/src/main/webapp/formdefinitions/userForm.json b/src/main/webapp/formdefinitions/userForm.json
index 57094484..66b14ad8 100644
--- a/src/main/webapp/formdefinitions/userForm.json
+++ b/src/main/webapp/formdefinitions/userForm.json
@@ -23,6 +23,11 @@
             "dataType" : "EMAIL",
             "required" : true
         },
+        {
+            "name" : "phone",
+            "dataType" : "STRING",
+            "required" : false
+        },
         {
             "name" : "firstName",
             "dataType" : "STRING",
diff --git a/src/main/webapp/templates/notificationSubscriptionForm.ftl b/src/main/webapp/templates/notificationSubscriptionForm.ftl
new file mode 100644
index 00000000..09c35eee
--- /dev/null
+++ b/src/main/webapp/templates/notificationSubscriptionForm.ftl
@@ -0,0 +1,88 @@
+<#-- 
+  Copyright (c) 2016 NIBIO <http://www.nibio.no/>. 
+  
+  This file is part of VIPSLogic.
+  VIPSLogic is free software: you can redistribute it and/or modify
+  it under the terms of the NIBIO Open Source License as published by 
+  NIBIO, either version 1 of the License, or (at your option) any
+  later version.
+  
+  VIPSLogic 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
+  NIBIO Open Source License for more details.
+  
+  You should have received a copy of the NIBIO Open Source License
+  along with VIPSLogic.  If not, see <http://www.nibio.no/licenses/>.
+--><#include "master.ftl">
+<#macro page_head>
+        <title>${i18nBundle.user}</title>
+</#macro>
+<#macro custom_js>
+	<script src="/js/resourcebundle.js"></script>
+	<script src="/js/validateForm.js"></script>
+	<script type="text/javascript" src="/js/3rdparty/chosen.jquery.min.js"></script>
+	<script type="text/javascript">
+		// Activating chosen plugin
+		$(".chosen-select").chosen();
+		// Load main form definition (for validation)
+		//loadFormDefinition("userForm");
+	</script>
+</#macro>
+<#macro custom_css>
+	<link href="/css/3rdparty/chosen.min.css" rel="stylesheet" />
+</#macro>
+<#macro page_contents>
+	<p><a href="/user" class="btn btn-default back" role="button">${i18nBundle.back}</a></p>
+        <h1><#if viewUser.userId?has_content>${viewUser.firstName!""} ${viewUser.lastName}<#else>${i18nBundle.newUser}</#if></h1>
+        <a href="/user?action=viewUser&userId=${viewUser.userId}" class="btn btn-default" role="button">${i18nBundle.userAccountInformation}</a>
+        <button type="button" class="btn btn-primary">${i18nBundle.notificationSubscriptions}</button>
+        <div id="errorMsgEl" class="alert alert-danger" <#if !formValidation?has_content> style="display:none;"</#if>>
+		<#if formValidation?has_content>${formValidation.validationMessages?replace("\n", "<br>")}</#if>
+	</div>
+	<#if messageKey?has_content>
+		<div class="alert alert-success">${i18nBundle(messageKey)}</div>
+	</#if>
+	<p>${i18nBundle.notificationSubscriptionDescription}</p>
+        <#assign formId = "notificationSubscriptionForm">
+        <form id="${formId}" role="form" action="/user/notificationsubscription" method="POST" onsubmit="return validateForm(this);">
+        <input type="hidden" name="action" value="notificationSubscriptionFormSubmit"/>
+        <input type="hidden" name="userId" value="${viewUser.userId}"/>
+          <h2>${i18nBundle.messageNotifications}</h2>
+          <div class="form-group">
+		<label for="universalMessageFormatId">${i18nBundle.messageFormat}</label>
+		<select name="universalMessageFormatId" onblur="validateField(this);">
+			<#list universalMessageFormats as format>
+			<option value="${format.universalMessageFormatId}"
+			<#if messageNotificationSubscription.universalMessageFormatId == format.universalMessageFormatId> selected="selected"</#if>
+			>${i18nBundle(format.formatName)}</option>
+		</#list>
+		</select>
+	</div>
+          <div class="form-group">
+		<label for="cropOrganismIds">${i18nBundle.cropOrganismIds}</label>
+		<select class="form-control chosen-select" name="cropOrganismIds" onblur="validateField(this);" multiple="multiple">
+			<#list allCrops?sort_by("latinName") as organism>
+			<option value="${organism.organismId}"
+			<#if userCropIds?seq_contains(organism.organismId)> selected="selected"</#if>
+			>${organism.latinName!""} / ${organism.tradeName!""} / ${organism.getLocalName(currentLocale.language)!""}</option>
+		</#list>
+		</select>
+	</div>
+	<div class="form-group">
+	    <label for="messageTagIds">${i18nBundle.messageTags}</label>
+	    <select class="form-control chosen-select" name="messageTagIds" onblur="validateField(this);" multiple="multiple">
+		<#list messageTagSet as messageTag>
+			<option value="${messageTag.messageTagId}"
+				<#if userMessageTagIds?seq_contains(messageTag.messageTagId)> selected="selected"</#if>
+			>${(messageTag.getLocalName(currentLocale))!messageTag.defaultTagName}</option>
+		</#list>
+	     </select>
+	</div>
+          <h2>${i18nBundle.forecastNotifications}</h2>
+          <p>Under construction</p>
+          <button type="submit" class="btn btn-default">${i18nBundle.submit}</button>
+	</form>
+
+</#macro>
+<@page_html/>
diff --git a/src/main/webapp/templates/userForm.ftl b/src/main/webapp/templates/userForm.ftl
index d00942aa..3f16eed3 100644
--- a/src/main/webapp/templates/userForm.ftl
+++ b/src/main/webapp/templates/userForm.ftl
@@ -29,6 +29,10 @@
 <#macro page_contents>
 	<p><a href="/user" class="btn btn-default back" role="button">${i18nBundle.back}</a></p>
         <h1><#if viewUser.userId?has_content>${viewUser.firstName!""} ${viewUser.lastName}<#else>${i18nBundle.newUser}</#if></h1>
+        <#if viewUser.userId?has_content>
+		<button type="button" class="btn btn-primary">${i18nBundle.userAccountInformation}</button>
+		<a href="/user/notificationsubscription?userId=${viewUser.userId}" class="btn btn-default" role="button">${i18nBundle.notificationSubscriptions}</a>
+        </#if>
         <div id="errorMsgEl" class="alert alert-danger" <#if !formValidation?has_content> style="display:none;"</#if>>
 		<#if formValidation?has_content>${formValidation.validationMessages?replace("\n", "<br>")}</#if>
 	</div>
@@ -44,6 +48,11 @@
 	    <input type="email" class="form-control" name="email" placeholder="${i18nBundle.email}" value="${viewUser.email!""}" onblur="validateField(this);" />
 	    <span class="help-block" id="${formId}_email_validation"></span>
 	  </div>
+	  <div class="form-group">
+	    <label for="phone">${i18nBundle.phone}</label>
+	    <input type="tel" class="form-control" name="phone" placeholder="${i18nBundle.phone}" value="${viewUser.phone!""}" onblur="validateField(this);" />
+	    <span class="help-block" id="${formId}_phone_validation"></span>
+	  </div>
 	  <div class="form-group">
 	    <label for="firstName">${i18nBundle.firstName}</label>
 	    <input type="text" class="form-control" name="firstName" placeholder="${i18nBundle.firstName}" value="${viewUser.firstName!""}" onblur="validateField(this);"/>
diff --git a/src/test/java/no/nibio/vips/logic/messaging/UniversalMessagingTest.java b/src/test/java/no/nibio/vips/logic/messaging/UniversalMessagingTest.java
index f8008461..25805a70 100644
--- a/src/test/java/no/nibio/vips/logic/messaging/UniversalMessagingTest.java
+++ b/src/test/java/no/nibio/vips/logic/messaging/UniversalMessagingTest.java
@@ -18,6 +18,7 @@
  */
 package no.nibio.vips.logic.messaging;
 
+import com.fasterxml.jackson.core.type.TypeReference;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import java.io.IOException;
 import java.util.ArrayList;
@@ -65,6 +66,26 @@ public class UniversalMessagingTest {
     // @Test
     // public void hello() {}
     
+    @Test
+    public void deserializeMessageLocalVersionTest()
+    {
+        System.out.println("deserializeMessageLocalVersionTest");
+        try
+        {
+            String jsonText = "[{\"locale\":\"no\",\"msgSubject\":\"Bladveps funnet på Kyrksæterøra\",\"msgLeadParagraph\":\"Se opp for svermende veps med brodder\",\"msgBody\":\"Lorem ipsum Dolores Sanctum Nobliatarus Factorum des nomine in sanctus spiritu benedictus qui venit dies irae Lorem ipsum Dolores Sanctum Nobliatarus Factorum des nomine in sanctus spiritu benedictus qui venit dies irae Lorem ipsum Dolores Sanctum Nobliatarus Factorum des nomine in sanctus spiritu benedictus qui venit dies irae\",\"msgDownloadUrl\":\"http://www.nibio.no/\"}]";
+
+            ObjectMapper mapper = new ObjectMapper();
+            //mapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true);
+            //mapper.configure(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS,true);
+            List<MessageLocalVersion> result = mapper.readValue(jsonText, new TypeReference<List<MessageLocalVersion>>(){});
+        }
+        catch(Exception e)
+        {
+            e.printStackTrace();
+            fail(e.getMessage());
+        }
+    }
+    
     @Test
     public void serviceTest()
     {
@@ -73,15 +94,26 @@ public class UniversalMessagingTest {
         try
         {
             UniversalMessage um = new UniversalMessage(
+                    "nb",
                     "Bladveps funnet på Kyrksæterøra",
                     "Se opp for svermende veps med brodder",
                     "Lorem ipsum Dolores Sanctum Nobliatarus Factorum des nomine in sanctus spiritu benedictus qui venit dies irae Lorem ipsum Dolores Sanctum Nobliatarus Factorum des nomine in sanctus spiritu benedictus qui venit dies irae Lorem ipsum Dolores Sanctum Nobliatarus Factorum des nomine in sanctus spiritu benedictus qui venit dies irae",
                     "http://www.nibio.no/"
             );
             
+            um.addMessageLocalVersion(
+                    "en", 
+                    "Leaf wasp found in Kyrksæterøra", 
+                    "Look out for swarming wasps and their dangerous sting", 
+                    "Yesterday, all my troubles seemed so far away. Now it looks as though they're here to stay.", 
+                    "http://www.nibio.no/"
+            );
+            
             
             String[][] recipients = {
-                {"1","Tor-Einar Skog","Mail","tor-einar.skog@nibio.no"}//,
+                {"1","Tor-Einar Skog","Mail","tor-einar.skog@nibio.no","nb"},
+                {"2","Tor-Einar Skog","Mail","tor.einar.skog@gmail.com","en"},
+                {"3","Tor-Einar Skog","Mail","tor.einar.skog@gmail.com","bs"}//,
                 //{"1","Tor-Einar Skog","Sms","91303819"},
                 //{"2","Lars Aksel Opsahl", "Mail","lars.opsahl@nibio.no"}
             };
@@ -93,7 +125,8 @@ public class UniversalMessagingTest {
                         recipient[0],
                         recipient[1],
                         recipient[2],
-                        recipient[3]
+                        recipient[3],
+                        recipient[4]
                     )
                 );
             }
@@ -103,22 +136,23 @@ public class UniversalMessagingTest {
             //client.register(new RESTAuthenticator("user", "userPass"));
             client.register(new RESTAuthenticator("VIPSLogic", "plmoknijbuhv000"));
             //ResteasyWebTarget target = client.target("http://kart13utv.ad.skogoglandskap.no:8080");
-            //ResteasyWebTarget target = client.target("http://localhost:8080");
-            ResteasyWebTarget target = client.target("http://logic.testvips2.nibio.no");
+            ResteasyWebTarget target = client.target("http://localhost:8080");
+            //ResteasyWebTarget target = client.target("http://logic.testvips2.nibio.no");
             UniversalMessagingServiceClient umClient = target.proxy(UniversalMessagingServiceClient.class);
             
         
-            System.out.println(new ObjectMapper().writeValueAsString(um));
+            //System.out.println(new ObjectMapper().writeValueAsString(um));
             Response r = umClient.sendMessage(new ObjectMapper().writeValueAsString(um));
-            assertEquals(200,r.getStatus());
+            
             
             result = r.readEntity(String.class);
-            System.out.println(result);
+            //System.out.println(result);
+            assertEquals(200,r.getStatus());
             //Response r = umClient.sendMessage(um);
         }
         catch(IOException | IllegalArgumentException | NullPointerException e)
         {
-            e.printStackTrace();
+            //e.printStackTrace();
             fail(e.getMessage());
         }
         assertNotNull(result);
-- 
GitLab