# Online Status eines Benutzers abrufen



## beta20 (15. Dez 2019)

Hallo,

ich möchte gerne in meine Applikation den Online Status eines Benutzers abrufen.
In der DB zu speichern macht nur bedingt Sinn, da:

a) User kann Browser schließen, ohne sich auszuloggen  -> wie bekommt meine Applikation nun mit, dass der User offline ist?

Wie kann ich das realisieren?


----------



## mrBrown (15. Dez 2019)

wer 'a' sagt, muss auch 'b' sagen 


Regelmäßig den Client an den Server melden lassen, dass er noch online ist, zB mit Websockets oder regelmäßigen normalen Http-Requests.
Wenn er sich meldet ist er online, sonst offline


----------



## mihe7 (15. Dez 2019)

beta20 hat gesagt.:


> User kann Browser schließen, ohne sich auszuloggen -> wie bekommt meine Applikation nun mit, dass der User offline ist?


Ggf. gar nicht. Daher gibt es das Session Timeout. Um den Online-Status aller Benutzer zu verwalten, kannst Du einen HttpSessionListener verwenden. S. beispielsweise http://programmersought.com/article/724060146/


----------



## beta20 (15. Dez 2019)

Ok, das kann ich machen. Das einzige Problem das ich sehe:
-User schließt den Browser. Wie bekomme ich das dann mit?


----------



## mrBrown (15. Dez 2019)

beta20 hat gesagt.:


> -User schließt den Browser. Wie bekomme ich das dann mit?


In welchem der beiden Fälle?

Wenn du Session Timeout dafür nutzt, dann "automatisch" wenn die Zeit abgelaufen ist.

Wenn der Nutzer regelmäßig seinen Status aktualisieren muss, dann bekommt man das mit, weil er sich nicht mehr meldet.


----------



## mihe7 (15. Dez 2019)

beta20 hat gesagt.:


> User schließt den Browser. Wie bekomme ich das dann mit?


Das Prinzip ist: wenn sich der User eine Zeit lang nicht meldet, bekommt der Server das mit. Du kannst natürlich clientseitig auch noch versuchen, über ein JS-Event einen entsprechenden Logout-Request abzusetzen. Verlassen würde ich mich darauf aber nicht.


----------



## beta20 (15. Dez 2019)

Ja, solang der User den Browser auf hat, ist das kein Problem.

Bzgl. dem Session Listener: wie mache ich hier eine Verbindung zum User Objekt selbst?
Klar könnte ich die Session ID dem User Objekt hinzufügen und sobald der Session Listener in die expired Methode geht, sucht er die Session Id bei einem User...

Aber:
- Session ID ist nicht aufsteigend, sodass theoretisch mehrere User die gleiche Session haben können
- User könnte sich mehrmals gleichzeitig in verschiedenen Browser einloggen und ich hätte kein 1:1 Mapping zum User mehr...

irgendwelche Ideen?


----------



## mihe7 (15. Dez 2019)

beta20 hat gesagt.:


> Ja, solang der User den Browser auf hat, ist das kein Problem.


Und wenn der User den Browser nicht auf hat, ist das auch kein Problem. Dann nämlich tut er sich schwer, sich beim Server zu melden.



beta20 hat gesagt.:


> Klar könnte ich die Session ID dem User Objekt hinzufügen


Wozu?!? 

In dem Beispiel unter http://programmersought.com/article/724060146/ wird einfach der Benutzername als Attribut der Session gespeichert und einer Liste (im application scope) hinzugefügt bzw. von dieser entfernt. 

Wenn Du mehrere Sessions parallel verwalten willst, brauchst Du lediglich statt einer Liste z. B. eine Map<String,Integer> zu verwenden, die die Anzahl Sessions mitzählt. Beim Anmelden wird der Eintrag mit einem Zähler von 1 eingerichtet bzw. der Zähler erhöht, beim Abmelden wird der Zähler verringert und, wenn er 0 erreicht hat, der Eintrag entfernt.


----------



## beta20 (15. Dez 2019)

Ok, danke....

Aber wo und wann setze ich das:

```
String username = (String) session.getAttribute("username");// Remove the username from the online list
List onlineUserList = (List) application.getAttribut("onlineUserList");
```


----------



## mihe7 (15. Dez 2019)

beta20 hat gesagt.:


> Aber wo und wann setze ich das:


Das steht in dem Beispiel.


----------



## beta20 (15. Dez 2019)

Habe den Code mal testweise bei mir probiert.... Bekomme aber einen Compiler Fehler:
Warum? Wie kann ich das fixen?

Müsste das nicht:
List onlineUserList = (List) application.getAttribute("onlineUserList");

heißen?


----------



## mihe7 (15. Dez 2019)

Ernsthaft jetzt?



Spoiler



getAttribute nicht getAttribut - ist auch schön rot unterringelt.


----------



## beta20 (15. Dez 2019)

Habe nochmal ein paar Fragen:
1)
- Wie kann ich den Status erst setzen (sessionCreated Methode) ausführen, wenn der User sich einloggt?
-> Derzeit wird sessionCreated ja bereits ausgeführt, sobald der User die Webpage öffnet...

2) Ebenfalls möchte ich die destroy Methode ausführen, sobald der User sich ausloggt...


----------



## mihe7 (15. Dez 2019)

1) kannst Du z. B. über eine SessionScoped-Bean erreichen, die den Benutzer setzt. Das setzt voraus, dass Du die Bean auch irgendwo auf der Seite verwendest, was aber kein Problem sein dürfte, weil Du im Normalfall eh irgendwo seinen Benutzernamen anzeigst.
2) geht automatisch, wenn sich der User ausloggt. Wie der Logout funktioniert siehst Du z. B. hier: https://stackoverflow.com/questions/5619827/how-to-invalidate-session-in-jsf-2-0


----------



## beta20 (15. Dez 2019)

1) Genau - eine SessionBean habe ich bereits.... Das heißt aber, dass ich den Status in meiner Datenbank setze, sodass auch andere Clients übergreifend den Status sehen können, korrekt? Wenn ich den Status nur in der SessionBean setze, dann sieht ja nur der eingeloggte User den Status?


----------



## mihe7 (15. Dez 2019)

Was willst Du denn ständig mit Deiner DB? Ob wer online ist oder nicht, ist doch kein persistenter Zustand. 

In Deiner SessionBean setzt Du einfach das Attribut username:

```
ExternalContext ec = FacesContext.getCurrentInstace().getExternalContext();
Principal principal = ec.getUserPrincipal();
if (principal != null) {
    HttpSession session = (HttpSession) ec.getSession(false);
    if (session != null) {
        session.setAttribute("username", principal.getName());
    }
}
```

Der HttpSessionBindingListener wird über diese Änderung informiert, holt sich über das Session-Attribut username den Benutzernamen und legt diesen in die anwendungsweit gültige Liste der Online-Benutzer (bzw. die Map mit Zähler). 

Die sessionDestroyed-Methode des HttpSessionListeners wird beim Abmelden (oder Timeout) automatisch aufgerufen.


----------



## beta20 (16. Dez 2019)

ok, danke...
Aber brauche ich zwingend Principal?

In:
session.setAttribute("username", MYVALUE);

-> In MYVALUE könnte ja auch einfach die ID von meinem User - Objekt dann stehen, oder?


----------



## mihe7 (16. Dez 2019)

beta20 hat gesagt.:


> In MYVALUE könnte ja auch einfach die ID von meinem User - Objekt dann stehen, oder?


Klar.


----------



## beta20 (16. Dez 2019)

danke....

Habe nochmal eine weitere Frage:
Nun würde ich den Online Status gerne in meiner JSF Seite abrufen....
Wie kann ich das machen?

*1) Frage*
Stelle mir sowas vor:
-> Wie fülle ich aber "event"

```
public boolean checkOnlineStatus(ChatUser chatUser) {

        HttpSession session = event.getSession();

        // Get the login username
        ServletContext application = session.getServletContext();

        List onlineUserList = (List) application.getAttribute("onlineUserList");

        if (onlineUserList.contains(chatUser.getId()))
            return true;

        else

            return false;
    }
```

*2) Frage*
Ich will nicht nur true oder false zurückgeben. Ich möchte mind.
- Online
- Abwesend
- Offline

Wie kann ich das machen? 
Könnte mir dann sowas vorstellen:

```
public String checkOnlineStatus(ChatUser chatUser) {

        HttpSession session = event.getSession();

        // Get the login username
        ServletContext application = session.getServletContext();

        List onlineUserList = (List) application.getAttribute("onlineUserList");

        if (onlineUserList.contains(chatUser.getId())){

// Prüfen wann Client zuletzt aktiv war? Wie kann ich das prüfen?
// Wenn kleiner 5 min
 return "ONLINE",
else 
return "AWAY";
}
       
        else
return "OFFLINE";

            return false;
    }
```

Irgendeine Idee?


----------



## mihe7 (16. Dez 2019)

beta20 hat gesagt.:


> Wie fülle ich aber "event"


Gar nicht. Du kommst an die Liste über den FacesContext (bzw. ExternalContext) ran:

```
ExternalContext ec = FacesContext.getCurrentInstace().getExternalContext();
List onlineUserList = (List) ec.getApplicationMap().get("onlineUserList");
```



beta20 hat gesagt.:


> Wie kann ich das machen?


Erstens kannst Du in den Application Scope nicht nur Strings sondern auch andere (serialisierbare) Objekte ablegen. Im einfachsten Fall könntest Du statt einer List eine `Map<String, Instant>` verwenden, die einen Benutzernamen auf einen Zeitstempel abbildet, der den Zeipunkt der letzten Aktivität angibt. Bei jeder (relevanten) Aktivität musst Du dann den Zeitstempel aktualisieren. Damit kannst Du feststellen, wie lange die letzte Aktivität her war.


----------



## beta20 (16. Dez 2019)

Danke!
Nun... "*onlineUserList*" ist mir noch nicht wirklich klar... Wann und wo passiert das?
Im Moment verwende ich das nur bei *sessionDestroyed *aber irgendwann muss diese Liste doch auch erstellt werden?



```
public class ChatOnlineListener implements HttpSessionListener {

    @Inject
    private LoginChatController loginChatController;

    private final Logger LOGGER = LoggerFactory.getLogger(ChatOnlineListener.class);

    /**
     * Session created
     */
    public void sessionCreated(HttpSessionEvent event) {

        LOGGER.info("START sessionCreated");

        try {
            final HttpSession session = event.getSession();
            InetAddress addr = InetAddress.getLocalHost();

            String ip = addr.getHostAddress().toString();
            session.setAttribute("ip", ip);
            
            HashMap<Long, Date> map = new HashMap<Long, Date>();
            map.put(loginChatController.getChatUser().getId(), new Date());
            session.setAttribute("username", map);

            LOGGER.info("Session created");
        } catch (Exception e) {
            LOGGER.error("Error");
        }
    }

    /**
     * Destroyed
     */
    public void sessionDestroyed(HttpSessionEvent event) {

        LOGGER.info("START sessionDestroyed");

        HttpSession session = event.getSession();

        // Get the login username
        ServletContext application = session.getServletContext();

        // Remove the username from the online list
        String username = (String) session.getAttribute("username");
        List onlineUserList = (List) application.getAttribute("onlineUserList");
        onlineUserList.remove(username);

        LOGGER.info(username + "timeout.");

        LOGGER.info("END sessionDestroyed");
    }
}
```


----------



## mihe7 (16. Dez 2019)

beta20 hat gesagt.:


> aber irgendwann muss diese Liste doch auch erstellt werden?


Ja, beim Login (also an der Stelle, an der Du den Benutzernamen in die onlineUserList einfügst) musst Du prüfen, ob es die Liste schon gibt, wenn nicht legst Du sie an.


----------



## beta20 (16. Dez 2019)

Also wenn ich eine LoginBean habe (SessionScoped), dann sollte das so aussehen?
-> In der Init Methode....


```
@PostConstruct
    [B]public[/B] [B]void[/B] init() {

// mein ChatUser Objekt holen aus DB...
....
               
HashMap<Long, Date> map = new HashMap<Long, Date>();
                    map.put(chatUser.getId(), new Date());
                    FacesContext facesContext = FacesContext.getCurrentInstance();
                    HttpSession session = (HttpSession) facesContext.getExternalContext().getSession(true);
                    session.setAttribute("username", map);
                   ServletContext application = session.getServletContext();
                    List<HashMap<Long, Date>> onlineUserList = (List<HashMap<Long, Date>>) application  .getAttribute("onlineUserList");

                    if (onlineUserList == null || onlineUserList.isEmpty()) {
                        List<HashMap<Long, Date>> list = new ArrayList<HashMap<Long, Date>>();
                        list.add(map);
                        session.setAttribute("onlineUserList", list);

                    }
```


Hier mal meine Online Check Methode (Rückgabe ist gerade noch "test", da onlineUserList immer NULL ist ? Warum?


```
@ViewScoped
@Named
public class ChatOnlineCheck implements Serializable {

    private static final long serialVersionUID = -6695529018699874351L;
    private final Logger LOGGER = LoggerFactory.getLogger(ChatOnlineCheck.class);
    
    @Inject
    private LoginChatController loginChatController;
    
    @PostConstruct
    public void init() {
        checkOnlineStatus(loginChatController.getChatUser());
    }
    
    /**
     * Online Status prüfen
     *
     * @param chatUser
     * @return
     */
    public String checkOnlineStatus(ChatUser chatUser) {

        LOGGER.info("START checkOnlineStatus");
        
        FacesContext facesContext = FacesContext.getCurrentInstance();
        HttpSession session = (HttpSession) facesContext.getExternalContext().getSession(true);
        
        ServletContext application = session.getServletContext();
        
        @SuppressWarnings("unchecked")
        List<HashMap<Long, Date>> onlineUserList = (List<HashMap<Long, Date>>) application
                .getAttribute("onlineUserList");
        
        if (onlineUserList != null && !onlineUserList.isEmpty()) {
            
            for(HashMap<Long, Date> map : onlineUserList) {
                map.containsKey(chatUser.getId());
            }           
        }

        LOGGER.info("END checkOnlineStatus");
        return "test";
    }
}
```

Was ist denn hier falsch?


----------



## mihe7 (17. Dez 2019)

beta20 hat gesagt.:


> Rückgabe ist gerade noch "test", da onlineUserList immer NULL ist ? Warum?


Die Rückgabe ist immer "test", auch wenn onlineUserList nicht null ist. 

Außerdem:


beta20 hat gesagt.:


> session.setAttribute("onlineUserList", list);


Da müsste schon application statt session stehen. 

Außerdem: 


beta20 hat gesagt.:


> List<HashMap<Long, Date>> onlineUserList


Wozu brauchst Du eine Liste von HashMaps?!?


----------



## beta20 (17. Dez 2019)

OK, habe es geändert zu:

```
ServletContext application = session.getServletContext();
application.setAttribute("onlineUserList", list);
```

Was soll ich sonst als HashMaps verwenden? Ich will doch Username und Datum speichern?

Wie sollte das dann aussehen?

```
ServletContext application = session.getServletContext();
                    List<HashMap<Long, Date>> onlineUserList = (List<HashMap<Long, Date>>) application
                            .getAttribute("onlineUserList");
                    if (onlineUserList == null || onlineUserList.isEmpty()) {
                        List<HashMap<Long, Date>> list = new ArrayList<HashMap<Long, Date>>();
                        list.add(map);
                        application.setAttribute("onlineUserList", list);
                    }
```


Ebenfalls dann in der Session Destroyed Methode:

```
/**
     * Destroyed
     */
    public void sessionDestroyed(HttpSessionEvent event) {
....
        List onlineUserList = (List) application.getAttribute("onlineUserList");
        onlineUserList.remove(username);

    }
```

Und in meiner checkOnline Methode:

```
/**
     * Online Status prüfen
     *
     * @param chatUser
     * @return
     */
    public String checkOnlineStatus(ChatUser chatUser) {

        LOGGER.info("START checkOnlineStatus");
        
        FacesContext facesContext = FacesContext.getCurrentInstance();
        HttpSession session = (HttpSession) facesContext.getExternalContext().getSession(true);
        
        ServletContext application = session.getServletContext();
        
        @SuppressWarnings("unchecked")
        List<HashMap<Long, Date>> onlineUserList = (List<HashMap<Long, Date>>) application
                .getAttribute("onlineUserList");
        
        if (onlineUserList != null && !onlineUserList.isEmpty()) {
            
            for(HashMap<Long, Date> map : onlineUserList) {
                map.containsKey(chatUser.getId());
            }           
        }

        LOGGER.info("END checkOnlineStatus");
        return "test";
    }
```


Warum ist return immer "test" ? Ich will dochl ausgeben:
a) ob User ONLINE, AWAY, OFFLINE 
b) Wenn AWAY, dann ebenfalls die Zeit speichern
-> Also könnte mir vorstellen eine Klasse mit zwei Attributen zu erstellen (status, timeAway) zu erstellen und das Objekt der Klasse dann zurückgeben?


----------



## mihe7 (17. Dez 2019)

beta20 hat gesagt.:


> Ich will doch Username und Datum speichern?


Map<String, Date> würde Benutzernamen auf Daten (pl. von Datum) abbilden. 


```
Map<String, Date> onlineUserList = (Map<String, Date>) application
                .getAttribute("onlineUserList");

onlineUserList.put(username, new Date()); // letzten Zugriff des Benutzers speichern
```



beta20 hat gesagt.:


> Warum ist return immer "test" ? Ich will dochl ausgeben:


Naja, wenn Du das willst, musst Du das dem Rechner auch sagen. Aktuell iterierst Du über einfach über eine Liste und gibst dann "test" zurück. 



beta20 hat gesagt.:


> Also könnte mir vorstellen eine Klasse mit zwei Attributen zu erstellen (status, timeAway) zu erstellen und das Objekt der Klasse dann zurückgeben?


Ja, eine Klasse ist eine gute Idee. Wenn Du noch den Benutzernamen mit aufnimmst, dann würde das Objekt alle Informationen enthalten, die Du anzeigen kannst.


----------



## beta20 (17. Dez 2019)

Könnte ich dann auch anstatt String meine Klasse (MyClass) verwenden, wo ich dann den User speichere?

Map<*MyClass*, Date> onlineUserList = (Map<*MyClass*, Date>) application
                .getAttribute("onlineUserList");


----------



## mihe7 (17. Dez 2019)

beta20 hat gesagt.:


> Könnte ich dann auch anstatt String meine Klasse (MyClass) verwenden, wo ich dann den User speichere?


Könntest Du (wenn Du equals und hashCode implementierst und die dort verwendeten Attribute unveränderlich sind), allerdings: warum sollte ein Objekt dieser Klasse ein Schlüssel sein?

Mach es doch einfach andersrum:

```
Map<String, MyClass> onineUserList;
```
Der Schlüssel wäre dann immer noch der Benutzername.


----------

