https://d226lax1qjow5r.cloudfront.net/blog/blogposts/track-santa-sms-java-dr/Tracking-Santa-with-SMS.png

Dem Weihnachtsmann auf der Spur mit SMS und Java

Zuletzt aktualisiert am May 10, 2021

Lesedauer: 5 Minuten

Seit Dezember 2004, Google jedes Jahr eine weihnachtliche Website, die es den Nutzern ermöglicht, den Weihnachtsmann an Heiligabend zu finden. Zusätzlich, NORAD den Weihnachtsmann seit 1955 aufspüren. Obwohl es keine offizielle API gibt, gibt es eine inoffizielle API die verwendet werden kann, um den Aufenthaltsort des Weihnachtsmanns zu verfolgen.

Um die Weihnachtszeit zu feiern, wollte ich eine Spring Boot Anwendung erstellen, mit der man per SMS über den Aufenthaltsort des Weihnachtsmanns informiert werden kann. Der vollständige Code für diese Java-Anwendung ist zu finden auf GitHub.

Zunächst möchte ich erklären, wie die Anwendung auf einer höheren Ebene funktioniert. Dann können wir uns mit einigen der schwierigeren Probleme befassen und Wege finden, sie zu umgehen.

Sehen Sie es in Aktion

Wenn eine SMS auf meiner Nexmo-Nummer eingeht, wird eine Nutzlast an meinen registrierten Webhook gesendet. Informationen wie die Telefonnummer des Absenders und der Inhalt der Nachricht werden dann verwendet, um zu bestimmen, wie auf die Nachricht reagiert werden soll.

Hier sehen Sie, wie es in Aktion aussieht:

The santa tracker in action.The santa tracker in action.

Die erste Nachricht

Beim ersten Empfang einer Nachricht wird dem Benutzer immer der aktuelle Standort des Weihnachtsmanns angezeigt. Er wird auch gefragt, ob er seine Postleitzahl angeben möchte, um Entfernungsangaben zu erhalten.

Standort-Suche

Die Suche nach dem Standort ist keine triviale Aufgabe. Die Postleitzahlen sind weltweit nicht einheitlich, und es ist hilfreich zu wissen, aus welchem Land der Nutzer Kontakt aufnimmt, um die Suche einzugrenzen.

Wenn der Nutzer sich entscheidet, die Postleitzahl anzugeben, verwende ich Nexmo-Nummern-Einblick um das Land zu ermitteln, aus dem der Nutzer seine Nachrichten sendet. Sie werden dann aufgefordert, ihre Postleitzahl anzugeben. Daraufhin verwende ich einen Dienst namens GeoNames um den Breiten- und Längengrad für diese Postleitzahl zu ermitteln.

Diese Informationen werden zusammen mit der Telefonnummer in der Datenbank gespeichert. Anhand des Breiten- und Längengrads wird berechnet, wie weit der Weihnachtsmann von ihnen entfernt ist:

A response with Santa's locationA response with Santa's location

Technischer werden

Oberflächlich betrachtet, scheint die Anwendung nicht allzu kompliziert zu sein. Es gibt jedoch eine Reihe von Herausforderungen, auf die ich während des Entwicklungsprozesses gestoßen bin. Ich möchte einige der eher technischen Aspekte des Codes hervorheben.

Weiterleitung eingehender Nachrichten

Meine Nexmo-Nummer ist so konfiguriert, dass sie POST Anfragen an eine Webhook-URL zu senden. Ich habe eine IncomingMessageController eingerichtet, um die eingehenden Nachrichten zu verarbeiten:

@PostMapping
public void post(@RequestParam("msisdn") String from,
        @RequestParam("to") String nexmoNumber,
        @RequestParam("keyword") String keyword,
        @RequestParam("text") String text
) {
  Phone phone = findOrCreatePhone(from, nexmoNumber);
  findHandler(phone, keyword).handle(phone, text);
}

Die Methode findOrCreatePhone Methode wird verwendet, um eine neue Phone Entität zu persistieren oder eine bestehende Entität nachzuschlagen. So sieht die Telefon-Entität aus:

@Entity
public class Phone {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;
  private String number;
  private String nexmoNumber;
  private String countryCode;

  @Enumerated(EnumType.STRING)
  private Stage stage;

  @OneToOne
  private Location location;

  // Getters and Setters
}

Die Entität enthält die Telefonnummer, die Nexmo-Nummer, einen Ländercode und die aktuelle Phase, in der sich der Nutzer befindet. Ich werde über das Stage enum in einem späteren Abschnitt.

Die Methode findHandler Methode wird verwendet, um eine geeignete KeywordHandler um die Nachricht zu behandeln:

public interface KeywordHandler {
  void handle(Phone phone, String text);
}

Jeder Handler ist für die Bearbeitung eines bestimmten Schlüsselworts zuständig. Die Schlüsselwörter, die die Anwendung kennt, sind:

  • HILFE die kontextbezogene Informationen zur Unterstützung des Benutzers liefert.

  • ABBRECHEN bricht die aktuelle Reihe von Fragen ab.

  • REMOVE wodurch der Benutzer aus der Datenbank entfernt wird.

  • JA in dem der Benutzer die Frage bejaht.

  • NEIN in dem der Benutzer die Frage verneint.

  • WHERE der den aktuellen Standort des Weihnachtsmanns angibt.

  • STANDORT die es dem Nutzer ermöglicht, seinen Standort zu aktualisieren.

Jede KeywordHandler ist als Spring Managed Bean registriert. Die HelpKeywordHandlersieht zum Beispiel wie folgt aus:

@Component
public class HelpKeywordHandler implements KeywordHandler {
  @Override
  public void handle(Phone phone, String text) {
    // Logic here
  }
}

Alle KeywordHandler Implementierungen werden in eine keywordHandler Karte auf der IncomingMessageController:

private final Map<String, KeywordHandler> keywordHandlers;

@Autowired
public IncomingMessageController(Map<String, KeywordHandler> keywordHandlers) {
  this.keywordHandlers = keywordHandlers;
}

Beim Injizieren in eine Map verwendet Spring camelCase Klassennamen als Schlüssel und die instanziierte Klasse als Wert. Zum Beispiel wird die HelpKeywordHandler wird mit dem Schlüssel helpKeywordHandler:

HelpKeywordHandler handler = keywordHandlers.get("helpKeywordHandler");

Die entsprechende KeywordHandler wird dann mit einer Variante des Strategie-Musters. Wenn kein gültiges KeywordHandler gefunden wird, dann wird die DefaultKeywordHandler verwendet, um zu antworten.

private KeywordHandler findHandler(Phone phone, String keyword) {
  // New users should always go to the default handler
  if (phone.getStage() == null) {
    return keywordHandlers.get("defaultKeywordHandler");
  }

  KeywordHandler handler = keywordHandlers.get(keywordToHandlerName(keyword));
  return (handler != null) ? handler : keywordHandlers.get("defaultKeywordHandler");
}

private String keywordToHandlerName(String keyword) {
  return keyword.toLowerCase() + "KeywordHandler";
}

Die Weiterleitung von Nachrichten auf diese Weise ermöglicht Flexibilität, wenn neue Schlüsselwörter hinzugefügt werden müssen.

Umgang mit den verschiedenen Stadien

Jeder Handler ist für Nachrichten zuständig, die mit seinem Schlüsselwort beginnen. Aber woher weiß der YesKeywordHandler wissen, auf welche Frage der Benutzer antwortet? Hier kommt die Stage enum innerhalb der Phone Klasse ins Spiel kommt.

Die Phone kann in den folgenden Zwischenstadien existieren:

  • Keine Stufe (null) für Benutzer, die gerade erst angelegt wurden und denen noch keine Frage gestellt wurde.

  • INITIAL für Nutzer, denen mit der ersten Nachricht geantwortet wurde, in der der Standort des Weihnachtsmannes angegeben ist und in der sie gefragt werden, ob sie eine Postleitzahl angeben möchten oder nicht.

  • COUNTRY_PROMPT für Benutzer, die gefragt wurden, ob ihr Ländercode korrekt ist.

  • POSTAL_PROMPT für Nutzer, die um die Angabe einer Postleitzahl gebeten wurden.

Nachdem die Fragen gestellt wurden, werden sie in die folgenden Endstufen eingeordnet:

  • REGISTERED für Nutzer, die eine Postleitzahl angegeben haben und genauere Informationen erhalten.

  • GUEST für Benutzer, die keine Postleitzahl angeben möchten und nur den aktuellen Standort des Weihnachtsmannes erhalten, ohne dass die Entfernung berechnet wird.

So wird die YesKeywordHandler Stufen verwendet:

@Override
public void handle(Phone phone, String text) {
  if (phone.getStage() == Phone.Stage.INITIAL) {
    handlePromptForCountryCode(phone);
  } else if (phone.getStage() == Phone.Stage.COUNTRY_PROMPT) {
    handlePromptForPostalCode(phone);
  } else {
    outgoingMessageService.sendUnknown(phone);
  }
}

Ein Benutzer, der in der INITIAL Phase die Frage "Möchten Sie Angaben zur Postleitzahl machen?" beantworten.

Ein Benutzer, der in der COUNTRY_PROMPT Phase antwortet, muss die Frage "I see you're messaging from the US. Antworten Sie mit JA, einem zweistelligen Ländercode oder CANCEL, wenn Sie Ihre Meinung geändert haben.

Abrufen des Ländercodes

Um die Postleitzahl genauer nachschlagen zu können, ist es hilfreich, das Land zu kennen, aus dem der Benutzer eine Nachricht sendet. Ich habe ein PhoneLocationLookupService die Nexmo Basic Number Insight verwendet, um die Länderinformationen für die Telefonnummer zu ermitteln:

@Service
public class PhoneLocationLookupService {
  private final InsightClient insightClient;

  @Autowired
  public PhoneLocationLookupService(NexmoClient nexmoClient) {
    this.insightClient = nexmoClient.getInsightClient();
  }

  public String lookupCountryCode(String number) {
    try {
      return this.insightClient.getBasicNumberInsight(number).getCountryCode();
    } catch (IOException | NexmoClientException e) {
      return null;
    }
  }
}

Die Anwendung fordert den Nutzer auf, sein Land zu bestätigen, falls er auf Reisen ist und sich in ein anderes Land begeben möchte.

Suche nach Postleitzahlen

Es stellt sich heraus, dass die Suche nach Längen- und Breitengraden für Postleitzahlen keine triviale Aufgabe ist. Ich habe mich für GeoNames entschieden, weil der Dienst kostenlos ist.

Ich habe eine PostCodeLookupService für die Suche nach der Postleitzahl. Die getLocation Methode prüft zunächst, ob die Postleitzahl in der Datenbank bereits bekannt ist. Dies ist eine gute Praxis, da sie dazu beiträgt, die Anzahl der Anrufe bei einem Drittanbieterdienst zu begrenzen.

Wenn keine Entität vorhanden ist Location Entität vorhanden ist, wird der Dienst des Drittanbieters aufgerufen und eine neue Entität wird gespeichert. Wenn wir keinen Ort finden konnten, der dieser Postleitzahl entspricht, geben wir einen leeren Optional zurück, damit die Anwendung weiß, dass sie den Benutzer bitten muss, es erneut zu versuchen:

private Optional<Location> getLocation(String country, String postalCode) {
  Optional<Location> locationOptional = locationRepository.findByPostalCode(postalCode);
  if (locationOptional.isPresent()) {
    return locationOptional;
  }

  LocationResponse response = getLocationResponse(country, postalCode);
  if (response.getPostalCodes().isEmpty()) {
    return Optional.empty();
  }

  Location newLocation = buildLocation(response, postalCode);
  return Optional.of(locationRepository.save(newLocation));
}

Berechnung der Entfernung

Sobald wir den Breiten- und Längengrad des Benutzers haben, können wir den aktuellen Standort des Weihnachtsmanns nachschlagen und mit einer Formel bestimmen, wie weit der Benutzer vom Weihnachtsmann entfernt ist. Dies geschieht in der DistanceCalculationService:

public double getDistanceInMiles(double lat1, double lng1, double lat2, double lng2) {
  double theta = lng1 - lng2;
  double dist = Math.sin(Math.toRadians(lat1))
          * Math.sin(Math.toRadians(lat2))
          + Math.cos(Math.toRadians(lat1))
          * Math.cos(Math.toRadians(lat2))
          * Math.cos(Math.toRadians(theta));

  dist = Math.acos(dist);
  dist = Math.toDegrees(dist);
  dist = dist * 60 * 1.1515;

  return dist;
}

Schlussfolgerung

Wir haben uns angeschaut, wie man eine Anwendung erstellen kann, mit der man den Standort des Weihnachtsmanns per SMS abrufen kann. Wir haben die Weiterleitung von Nachrichten auf der Grundlage von Schlüsselwörtern, die Verwendung mehrerer Stufen zur Bestimmung der Fragen, auf die ein Benutzer antwortet, und die Verwendung von Nexmo Number Insight zur Ermittlung der Landesvorwahl für eine Telefonnummer behandelt.

Für detailliertere Informationen empfehle ich einen Blick auf den Code auf GitHub. Die README Datei enthält alle Informationen, die Sie benötigen, um die Anwendung selbst zum Laufen zu bringen.

Schauen Sie sich unbedingt Googles Weihnachtsmann-Tracker oder NORAD Weihnachtsmann-Tracker um den Heiligabend herum als zusätzliche Möglichkeit, den Weihnachtsmann im Auge zu behalten.

Teilen Sie:

https://a.storyblok.com/f/270183/150x150/a3d03a85fd/placeholder.svg
Steve CrowVonage Ehemalige

Steve ist ein selbsternannter Mathlet und König des Scharfsinns. Außerdem ist er ein Liebhaber von Windhunden, kniffligen Puzzles und europäischen Brettspielen. Wenn er nicht gerade mit Nicht-Mathematikern über Mathe und mit Nicht-Javaleuten über Java spricht, kann man ihn beim Kaffeetrinken und beim Hacken von Code antreffen.