/* * Copyright 2014 michael-simons.eu. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package ac.simons.bikingFX.bikes; import ac.simons.bikingFX.api.JsonRetrievalTask; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URL; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.ZoneOffset; import java.util.Base64; import java.util.Optional; import java.util.ResourceBundle; import java.util.function.Consumer; import java.util.function.Supplier; import java.util.logging.Level; import java.util.logging.Logger; import javafx.beans.property.Property; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.concurrent.Task; import javafx.concurrent.WorkerStateEvent; import javafx.event.EventHandler; import javax.json.Json; import javax.json.JsonObject; import javax.json.JsonReader; import javax.json.JsonWriter; import static java.time.Instant.ofEpochMilli; import static java.time.ZoneId.systemDefault; /** * @author Michael J. Simons, 2014-10-20 */ public class MilageChangeListener implements ChangeListener<Integer> { private static final Logger logger = Logger.getLogger(MilageChangeListener.class.getName()); /** * Used to retrieve a password */ private final Supplier<String> passwordSupplier; /** * Used to store a password */ private final Consumer<Optional<String>> passwordConsumer; /** * Used for error messages */ private final ResourceBundle resources; /** * Optionally used for handling failed events on the actual task */ private EventHandler<WorkerStateEvent> taskFailedHandler; public MilageChangeListener(Supplier<String> passwordSupplier, Consumer<Optional<String>> passwordConsumer, ResourceBundle resources) { this.passwordSupplier = passwordSupplier; this.passwordConsumer = passwordConsumer; this.resources = resources; } public void setOnFailed(EventHandler<WorkerStateEvent> taskFailedHandler) { this.taskFailedHandler = taskFailedHandler; } @Override public void changed(ObservableValue<? extends Integer> observable, Integer oldValue, Integer newValue) { final Property<Integer> milageProperty = (Property<Integer>) observable; final Bike bike = (Bike) milageProperty.getBean(); final LocalDate recordedOn; final LocalDate hlp = LocalDate.now(); // If it's the first half of the month i assume i want to add last months milage if(hlp.getDayOfMonth() <= 15) { recordedOn = hlp.withDayOfMonth(1); } else { recordedOn = hlp.withDayOfMonth(1).plusMonths(1); } // Password is retrieved on the FX thread as it propably opens a dialog final String password = passwordSupplier.get(); logger.log(Level.FINE, "Updating {0}s mileage from {1} to {2}, recorded on {3}...", new Object[]{bike.getName(), oldValue, newValue, recordedOn}); final Task<Integer> updateMilageTask = new Task<Integer>() { @Override protected Integer call() throws Exception { final URL apiEndpoint = new URL(String.format("%s/bikes/%d/milages", JsonRetrievalTask.BASE_URL, bike.getId())); logger.log(Level.FINE, "Calling {0}...", new Object[]{apiEndpoint.toExternalForm()}); // Prepare connection final HttpURLConnection connection = (HttpURLConnection) apiEndpoint.openConnection(); // It's a post request connection.setRequestMethod("POST"); // With json as content connection.setRequestProperty("Content-Type", "application/json; charset=utf-8"); // that needs to be authorized connection.setRequestProperty("Authorization", String.format("Basic %s", Base64.getEncoder().encodeToString(String.format("%s:%s", "biking2", password).getBytes()))); connection.setDoInput(true); connection.setDoOutput(true); // Write the actuall request body try (final JsonWriter jsonWriter = Json.createWriter(connection.getOutputStream())) { jsonWriter.write(Json.createObjectBuilder() .add("recordedOn", recordedOn.atStartOfDay().toEpochSecond(ZoneOffset.UTC) * 1000) .add("amount", newValue) .build() ); } final int code = connection.getResponseCode(); Integer rv = null; // Wrong password if (code == HttpURLConnection.HTTP_UNAUTHORIZED) { // Delete the wrong password passwordConsumer.accept(Optional.empty()); logger.log(Level.WARNING, "Unauthorized request, maybe wrong password?"); throw new RuntimeException(resources.getString("common.wrongPassword")); } // Bad request (invalid milage, date etc.) else if (code == HttpURLConnection.HTTP_BAD_REQUEST) { final StringBuilder errorMessage = new StringBuilder(); try (final InputStreamReader reader = new InputStreamReader(connection.getErrorStream())) { final char[] buffer = new char[2048]; int len; while ((len = reader.read(buffer, 0, buffer.length)) > 0) { errorMessage.append(buffer, 0, len); } } logger.log(Level.WARNING, "Bad request: {0}", new Object[]{errorMessage}); throw new RuntimeException(errorMessage.toString()); } // Dont check the other http states, assume everything else must be ok (like in the angular app ;) ) else { // At this point we can be safe that the password is correct passwordConsumer.accept(Optional.of(password)); try (final JsonReader reader = Json.createReader(connection.getInputStream())) { final JsonObject jsonObject = reader.readObject(); final LocalDate recordedOn = LocalDateTime.ofInstant(ofEpochMilli(jsonObject.getJsonNumber("recordedOn").longValue()), systemDefault()).toLocalDate(); rv = jsonObject.getInt("amount"); logger.log(Level.INFO, "Stored new mileage {0} recored on {1} for {2}", new Object[]{rv, recordedOn, bike.getName()}); } } return rv; } }; if (this.taskFailedHandler != null) { updateMilageTask.setOnFailed(this.taskFailedHandler); } new Thread(updateMilageTask).start(); } }