/** * i-net software provides programming examples for illustration only, without warranty * either expressed or implied, including, but not limited to, the implied warranties * of merchantability and/or fitness for a particular purpose. This programming example * assumes that you are familiar with the programming language being demonstrated and * the tools used to create and debug procedures. i-net software support professionals * can help explain the functionality of a particular procedure, but they will not modify * these examples to provide added functionality or construct procedures to meet your * specific needs. * * Copyright © 1999-2026 i-net software GmbH, Berlin, Germany. **/ package com.inet.dashboard.openweathermap; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.net.URLConnection; import java.text.DateFormat; import java.text.DecimalFormat; import java.text.NumberFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ForkJoinPool; import java.util.stream.Collectors; import java.util.stream.Stream; import javax.annotation.Nonnull; import javax.annotation.Nullable; import com.inet.config.structure.model.LocalizedKey; import com.inet.dashboard.api.ClientWidgetKey; import com.inet.dashboard.api.DashboardWidget; import com.inet.dashboard.api.model.DashboardWidgetDefinition; import com.inet.dashboard.openweathermap.ForecastData.Forecast; import com.inet.http.ClientMessageException; import com.inet.http.servlet.ClientLocale; import com.inet.lib.json.Json; import com.inet.lib.ui.UIBuilder; import com.inet.lib.ui.UIGroup; import com.inet.lib.ui.UIProperty; import com.inet.lib.util.EncodingFunctions; import com.inet.lib.util.StringFunctions; import com.inet.thread.timer.DefaultTimer; import com.inet.thread.timer.DefaultTimerTask; /** * A dashboard widget implementation that provides the information about the required settings and serves the weather data to the registered clients */ public class OpenWeatherMapWidget implements DashboardWidget { // The URL to the openweathermap.org API private static final String API_URL = "https://api.openweathermap.org/data/2.5/"; private static final String API_KEY = "apikey"; private static final String LOCATION = "location"; private static final String UNITS = "units"; private final Map refreshTasks = new ConcurrentHashMap<>(); /** * {@inheritDoc} */ @Override public @Nonnull String getExtensionName() { return "openweathermap"; } /** * {@inheritDoc} */ @Override public @Nonnull String getCategory() { return DashboardOpenWeatherMapServerPlugin.MSG.getMsg( "dashboard.openweathermap.category" ); } /** * {@inheritDoc} */ @Override public @Nonnull String getDescription() { return DashboardOpenWeatherMapServerPlugin.MSG.getMsg( "dashboard.openweathermap.description" ); } /** * {@inheritDoc} */ @Override public @Nonnull String getDisplayName() { return DashboardOpenWeatherMapServerPlugin.MSG.getMsg( "dashboard.openweathermap.name" ); } /** * {@inheritDoc} */ @Override public URL getIcon() { return getClass().getResource( "dashboard_openweather.svg" ); } /** * {@inheritDoc} */ @Override public @Nonnull String getRenderer( @Nonnull Map settings ) { return "dashboard-widget-openweathermap"; // a string, containing hyphens, to identify the javascript file and the tag name of the widget } /** * {@inheritDoc} */ @Override public @Nullable UIBuilder getSettingsUI() { UIBuilder ui = new UIBuilder( DashboardOpenWeatherMapServerPlugin.MSG, "dashboard.openweathermap." ); appendBaseSettings( ui ); // append the default widget settings like the title UIGroup group = ui.group( "weathersettings" ); group.input().text( API_KEY ); group.input().text( LOCATION ); UIProperty unitSelect = group.select().optionsFromKeys( UNITS, Stream.of( Units.values() ).map( Enum::name ).collect( Collectors.toList() ) ); ui.defaultValue( unitSelect, Units.METRIC.name() ); ui.validator( ( settings ) -> { if( ((String)settings.getOrDefault( API_KEY, "" )).isEmpty() ) { throw new ClientMessageException( DashboardOpenWeatherMapServerPlugin.MSG.getMsg( "dashboard.openweathermap.error.noapikey" ) ); } } ); ui.validator( ( settings ) -> { if( ((String)settings.getOrDefault( LOCATION, "" )).isEmpty() ) { throw new ClientMessageException( DashboardOpenWeatherMapServerPlugin.MSG.getMsg( "dashboard.openweathermap.error.nolocation" ) ); } } ); return ui; } /** * {@inheritDoc} */ @SuppressWarnings( "null" ) @Override public void register( @Nonnull String clientID, @Nonnull DashboardWidgetDefinition definition ) { Map settings = definition.getSettings(); String apiKey = (String)settings.getOrDefault( API_KEY, "" ); String location = (String)settings.getOrDefault( LOCATION, "" ); Units units = Units.valueOf( (String)settings.getOrDefault( UNITS, Units.METRIC.name() ) ); ClientWidgetKey key = new ClientWidgetKey( clientID, definition.getID() ); try { WidgetSettings widgetSettings = new WidgetSettings( apiKey, location, units, ClientLocale.getThreadLocale().getLanguage() ); RefreshTask refreshTask = refreshTasks.computeIfAbsent( widgetSettings, s -> new RefreshTask( s ) ); refreshTask.registerClient( key ); } catch( Throwable t ) { sendError( key, String.valueOf( t.getMessage() ) ); } } /** * {@inheritDoc} */ @Override public void unregister( @Nonnull String clientID, @Nonnull DashboardWidgetDefinition definition ) { Map settings = definition.getSettings(); String apiKey = (String)settings.getOrDefault( API_KEY, "" ); String location = (String)settings.getOrDefault( LOCATION, "" ); Units units = Units.valueOf( (String)settings.getOrDefault( UNITS, Units.METRIC.name() ) ); ClientWidgetKey key = new ClientWidgetKey( clientID, definition.getID() ); try { WidgetSettings widgetSettings = new WidgetSettings( apiKey, location, units, ClientLocale.getThreadLocale().getLanguage() ); refreshTasks.computeIfPresent( widgetSettings, ( s, refreshTask ) -> { if( refreshTask.unregisterClient( key ) ) { refreshTask.cancel(); return null; } return refreshTask; } ); } catch( Throwable t ) { DashboardOpenWeatherMapServerPlugin.LOGGER.error( t ); } } /** * DefaultTimerTask implementation to refresh the weather data at a fixed interval. */ private final class RefreshTask extends DefaultTimerTask { private WidgetSettings widgetSettings; private Map weatherData; private Set<@Nonnull ClientWidgetKey> clients = ConcurrentHashMap.newKeySet(); /** * Creates the task for the given settings and starts the timer immediately. * @param widgetSettings the settings of the widget */ public RefreshTask( WidgetSettings widgetSettings ) { this.widgetSettings = widgetSettings; DefaultTimer.getInstance().schedule( this, 0, 3600000 ); // Refresh instantly and then each hour } /** * Registers a new client. * @param key the new client */ public void registerClient( @Nonnull ClientWidgetKey key ) { clients.add( key ); Map weatherData = this.weatherData; if( weatherData != null ) { sendData( key, weatherData ); } } /** * Unregisters a client. Returns whether all clients are removed now. * @param key the client to unregister * @return whether all clients are disconnected */ public boolean unregisterClient( ClientWidgetKey key ) { if( clients.remove( key ) ) { if( clients.isEmpty() ) { return true; } } return false; } /** * Loads the weather data for the given user input and returns a formatted map with values for the client * @return the map with values for the client * @throws IOException if the data could not be requested */ private Map loadWeatherData() throws IOException { if( widgetSettings.getApiKey() == null || widgetSettings.getApiKey().isEmpty() ) { throw new ClientMessageException( DashboardOpenWeatherMapServerPlugin.MSG.getMsg( "dashboard.openweathermap.error.noapikey" ) ); } if( widgetSettings.getLocation() == null || widgetSettings.getLocation().isEmpty() ) { throw new ClientMessageException( DashboardOpenWeatherMapServerPlugin.MSG.getMsg( "dashboard.openweathermap.error.nolocation" ) ); } Units units = widgetSettings.getUnits(); String cityAndCountry = "q=" + EncodingFunctions.encodeUrlParameter( widgetSettings.getLocation() ); // Location as City,CountryCode as search query String appID = "appid=" + EncodingFunctions.encodeUrlParameter( widgetSettings.getApiKey() ); // App ID, entered by the user String language = "lang=" + EncodingFunctions.encodeUrlParameter( widgetSettings.getLanguage() ); // Current language settings retrieved from the client locale String unitSettings = "units=" + EncodingFunctions.encodeUrlParameter( units.name().toLowerCase() ); // The units, selected by the user NumberFormat exactTemperatureFormat = DecimalFormat.getInstance(); exactTemperatureFormat.setMinimumFractionDigits( 0 ); exactTemperatureFormat.setMaximumFractionDigits( 2 ); NumberFormat roundedTemperatureFormat = DecimalFormat.getInstance(); roundedTemperatureFormat.setMaximumFractionDigits( 0 ); DateFormat timeFormat = SimpleDateFormat.getTimeInstance( SimpleDateFormat.SHORT ); DateFormat dateFormat = new SimpleDateFormat( "E" ); Map result = new HashMap(); // Create the target URL for the current weather data URL targetCurrentWeather = new URL( API_URL + "weather?" + cityAndCountry + "&" + appID + "&" + language + "&" + unitSettings ); // Open the connection and read the data from the input stream URLConnection connectionCurrentWeather = targetCurrentWeather.openConnection(); try (InputStream inputStream = connectionCurrentWeather.getInputStream()) { // Read the data with the JSON-Parser into a WeatherData instance. // the HashMap for extraFields is set, to allow additional fields int the response data that can be ignored. Values that do not match a field, will be added to this map. WeatherData weatherData = new Json().fromJson( inputStream, WeatherData.class, new HashMap>(), null ); if( weatherData.getCod() != 200 ) { // If the code os not 200 OK, throw exception return null; } // Get all details and format them result.put( "description", weatherData.getWeather().get( 0 ).getDescription() ); result.put( "icon", weatherData.getWeather().get( 0 ).getIcon() ); result.put( "temperature", String.format( "%s", exactTemperatureFormat.format( weatherData.getMain().getTemp() ) ) + " " + units.getTemperatureUnit() ); result.put( "pressure", new LocalizedKey( DashboardOpenWeatherMapServerPlugin.MSG.getMsg( "dashboard.openweathermap.pressure" ), weatherData.getMain().getPressure() + " hPa" ) ); result.put( "humidity", new LocalizedKey( DashboardOpenWeatherMapServerPlugin.MSG.getMsg( "dashboard.openweathermap.humidity" ), weatherData.getMain().getHumidity() + "%" ) ); result.put( "windspeed", new LocalizedKey( DashboardOpenWeatherMapServerPlugin.MSG.getMsg( "dashboard.openweathermap.windspeed" ), weatherData.getWind().getSpeed() + " " + units.getSpeedUnit() ) ); result.put( "sunrise", new LocalizedKey( DashboardOpenWeatherMapServerPlugin.MSG.getMsg( "dashboard.openweathermap.sunrise" ), timeFormat.format( new Date( weatherData.getSys().getSunrise() * 1000 ) ) ) ); result.put( "sunset", new LocalizedKey( DashboardOpenWeatherMapServerPlugin.MSG.getMsg( "dashboard.openweathermap.sunset" ), timeFormat.format( new Date( weatherData.getSys().getSunset() * 1000 ) ) ) ); result.put( "visibility", new LocalizedKey( DashboardOpenWeatherMapServerPlugin.MSG.getMsg( "dashboard.openweathermap.visibility" ), weatherData.getVisibility() + " m" ) ); } catch( Throwable th ) { for( ClientWidgetKey widgetKey : clients ) { sendError( widgetKey, StringFunctions.getUserFriendlyErrorMessage( connectionCurrentWeather ) ); } return null; } // Create the target URL for the daily forecast URL targetForecast = new URL( API_URL + "forecast/daily?" + cityAndCountry + "&" + appID + "&" + language + "&" + unitSettings + "&cnt=7" ); List> forecasts = new ArrayList<>(); // Open the connection and read the data from the input stream URLConnection connectionForecast = targetForecast.openConnection(); try (InputStream inputStream = connectionForecast.getInputStream()) { // Read the data with the JSON-Parser into a WeatherData instance. // the HashMap for extraFields is set, to allow additional fields int the response data that can be ignored. Values that do not match a field, will be added to this map. ForecastData forecastData = new Json().fromJson( inputStream, ForecastData.class, new HashMap>(), null ); if( forecastData.getCod() != 200 ) { // If the code os not 200 OK, throw exception return null; } for( Forecast forecast : forecastData.getForecasts() ) { Map forecastResult = new HashMap(); forecastResult.put( "date", dateFormat.format( new Date( forecast.getDt() * 1000 ) ) ); forecastResult.put( "description", forecast.getWeather().getDescription() ); forecastResult.put( "icon", forecast.getWeather().getIcon() ); forecastResult.put( "temperaturemin", String.format( "%s", roundedTemperatureFormat.format( forecast.getTemp().getMin() ) + " " + units.getTemperatureUnit() ) ); forecastResult.put( "temperaturemax", String.format( "%s", roundedTemperatureFormat.format( forecast.getTemp().getMax() ) + " " + units.getTemperatureUnit() ) ); forecasts.add( forecastResult ); } } catch( Throwable th ) { for( ClientWidgetKey widgetKey : clients ) { sendError( widgetKey, StringFunctions.getUserFriendlyErrorMessage( connectionCurrentWeather ) ); } return null; } result.put( "forecasts", forecasts ); return result; } /** * {@inheritDoc} */ @Override public void runImpl() throws Throwable { ForkJoinPool.commonPool().execute( () -> { try { Map weatherData = this.weatherData = loadWeatherData(); if( weatherData == null ) { return; } for( ClientWidgetKey widgetKey : clients ) { sendData( widgetKey, weatherData ); } } catch( Throwable t ) { if( t instanceof ClientMessageException ) { ClientMessageException cme = (ClientMessageException)t; for( ClientWidgetKey widgetKey : clients ) { sendError( widgetKey, cme.getMessage() ); } return; } DashboardOpenWeatherMapServerPlugin.LOGGER.error( t ); for( ClientWidgetKey widgetKey : clients ) { sendError( widgetKey, DashboardOpenWeatherMapServerPlugin.MSG.getMsg( "dashboard.openweathermap.error.loading" ) ); } } } ); } } }