diff --git a/src/main/java/com/rohitawate/restaurant/homewindow/BodyTabController.java b/src/main/java/com/rohitawate/restaurant/homewindow/BodyTabController.java index 70002a3..e82e5c5 100644 --- a/src/main/java/com/rohitawate/restaurant/homewindow/BodyTabController.java +++ b/src/main/java/com/rohitawate/restaurant/homewindow/BodyTabController.java @@ -79,6 +79,8 @@ public class BodyTabController implements Initializable { public DataDispatchRequest getBasicRequest(String requestType) { DataDispatchRequest request = new DataDispatchRequest(requestType); + // Raw and binary types get saved in Body. + // Form and URL encoded types use tuple objects if (rawTab.isSelected()) { String contentType; switch (rawInputTypeBox.getValue()) { diff --git a/src/main/java/com/rohitawate/restaurant/homewindow/DashboardController.java b/src/main/java/com/rohitawate/restaurant/homewindow/DashboardController.java index 2917184..43964e1 100644 --- a/src/main/java/com/rohitawate/restaurant/homewindow/DashboardController.java +++ b/src/main/java/com/rohitawate/restaurant/homewindow/DashboardController.java @@ -27,6 +27,7 @@ import com.rohitawate.restaurant.requestsmanager.DELETERequestManager; import com.rohitawate.restaurant.requestsmanager.DataDispatchRequestManager; import com.rohitawate.restaurant.requestsmanager.GETRequestManager; import com.rohitawate.restaurant.requestsmanager.RequestManager; +import com.rohitawate.restaurant.util.Services; import com.rohitawate.restaurant.util.settings.Settings; import com.rohitawate.restaurant.util.themes.ThemeManager; import javafx.beans.binding.Bindings; @@ -64,7 +65,7 @@ public class DashboardController implements Initializable { @FXML private Label statusCode, statusCodeDescription, responseTime, responseSize, errorTitle, errorDetails; @FXML - private JFXButton cancelButton; + private JFXButton sendButton, cancelButton; @FXML private TabPane requestOptionsTab; @FXML @@ -291,6 +292,7 @@ public class DashboardController implements Initializable { default: loadingLayer.setVisible(false); } + Services.historyManager.saveHistory(getState()); } catch (MalformedURLException MURLE) { promptLayer.setVisible(true); snackBar.show("Invalid address. Please verify and try again.", 3000); @@ -394,7 +396,7 @@ public class DashboardController implements Initializable { * @return DashboardState - Current state of the Dashboard */ public DashboardState getState() { - DashboardState dashboardState = null; + DashboardState dashboardState; switch (httpMethodBox.getValue()) { case "POST": case "PUT": diff --git a/src/main/java/com/rohitawate/restaurant/homewindow/HistoryItemController.java b/src/main/java/com/rohitawate/restaurant/homewindow/HistoryItemController.java new file mode 100644 index 0000000..f890037 --- /dev/null +++ b/src/main/java/com/rohitawate/restaurant/homewindow/HistoryItemController.java @@ -0,0 +1,30 @@ +package com.rohitawate.restaurant.homewindow; + +import javafx.fxml.FXML; +import javafx.fxml.Initializable; +import javafx.scene.control.Label; +import javafx.scene.layout.HBox; + +import java.net.URL; +import java.util.ResourceBundle; + +public class HistoryItemController { + @FXML + private Label requestType, address; + + public void setRequestType(String requestType) { + this.requestType.setText(requestType); + } + + public void setAddress(String address) { + this.address.setText(address); + } + + public String getRequestType() { + return requestType.getText(); + } + + public String getAddress() { + return address.getText(); + } +} diff --git a/src/main/java/com/rohitawate/restaurant/homewindow/HomeWindowController.java b/src/main/java/com/rohitawate/restaurant/homewindow/HomeWindowController.java index 7a1c51e..eb3b3d0 100644 --- a/src/main/java/com/rohitawate/restaurant/homewindow/HomeWindowController.java +++ b/src/main/java/com/rohitawate/restaurant/homewindow/HomeWindowController.java @@ -17,45 +17,91 @@ package com.rohitawate.restaurant.homewindow; import com.rohitawate.restaurant.models.DashboardState; +import com.rohitawate.restaurant.models.requests.DataDispatchRequest; +import com.rohitawate.restaurant.models.requests.GETRequest; +import com.rohitawate.restaurant.models.requests.RestaurantRequest; +import com.rohitawate.restaurant.util.Services; import javafx.application.Platform; import javafx.beans.binding.Bindings; +import javafx.concurrent.Task; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; import javafx.fxml.Initializable; import javafx.scene.Parent; import javafx.scene.Scene; +import javafx.scene.control.ScrollPane; import javafx.scene.control.Tab; import javafx.scene.control.TabPane; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCodeCombination; import javafx.scene.input.KeyCombination; +import javafx.scene.input.MouseButton; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; import javafx.stage.Stage; import java.io.*; +import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.List; import java.util.ResourceBundle; +import java.util.concurrent.ExecutionException; public class HomeWindowController implements Initializable { @FXML private TabPane homeWindowTabPane; + @FXML + private VBox historyTab; + @FXML + private StackPane historyPromptLayer; private KeyCombination newTab = new KeyCodeCombination(KeyCode.T, KeyCombination.CONTROL_DOWN); - private List controllers; + private List dashboardControllers; + private List historyItemControllers; @Override public void initialize(URL location, ResourceBundle resources) { - controllers = new ArrayList<>(); + dashboardControllers = new ArrayList<>(); + historyItemControllers = new ArrayList<>(); recoverState(); Platform.runLater(() -> { + // Adds a new tab if the last tab is closed Scene thisScene = homeWindowTabPane.getScene(); thisScene.setOnKeyPressed(e -> { if (newTab.match(e)) addTab(); }); + + // Saves the state of the application before closing Stage thisStage = (Stage) thisScene.getWindow(); thisStage.setOnCloseRequest(e -> saveState()); + + // Loads the history + Task> historyLoader = new Task>() { + @Override + protected List call() throws Exception { + return Services.historyManager.getHistory(); + } + }; + + // Appends the history items to the HistoryTab + historyLoader.setOnSucceeded(e -> { + try { + List history = historyLoader.get(); + if (history.size() == 0) { + historyPromptLayer.setVisible(true); + return; + } + + for (DashboardState state : history) + addHistoryItem(state); + } catch (InterruptedException | ExecutionException E) { + E.printStackTrace(); + } + }); + historyLoader.setOnFailed(e -> historyLoader.getException().printStackTrace()); + new Thread(historyLoader).start(); }); } @@ -79,10 +125,11 @@ public class HomeWindowController implements Initializable { newTab.setOnCloseRequest(e -> { if (homeWindowTabPane.getTabs().size() == 1) addTab(); - controllers.remove(controller); + dashboardControllers.remove(controller); }); homeWindowTabPane.getTabs().add(newTab); - controllers.add(controller); + homeWindowTabPane.getSelectionModel().select(newTab); + dashboardControllers.add(controller); } catch (IOException e) { e.printStackTrace(); } @@ -92,7 +139,7 @@ public class HomeWindowController implements Initializable { List dashboardStates = new ArrayList<>(); // Get the states of all the tabs - for (DashboardController controller : controllers) + for (DashboardController controller : dashboardControllers) dashboardStates.add(controller.getState()); try { @@ -139,4 +186,27 @@ public class HomeWindowController implements Initializable { System.out.println("Application loaded."); } } + + public void addHistoryItem(DashboardState state) { + try { + FXMLLoader loader = new FXMLLoader(getClass().getResource("/fxml/homewindow/HistoryItem.fxml")); + Parent historyItem = loader.load(); + HistoryItemController controller = loader.getController(); + + controller.setRequestType(state.getHttpMethod()); + + controller.setAddress(state.getTarget().toString()); + + historyTab.getChildren().add(0, historyItem); + historyItemControllers.add(controller); + + // Clicking on HistoryItem opens it up in a new tab + historyItem.setOnMouseClicked(mouseEvent -> { + if (mouseEvent.getButton() == MouseButton.PRIMARY) + addTab(state); + }); + } catch (IOException IOE) { + IOE.printStackTrace(); + } + } } diff --git a/src/main/java/com/rohitawate/restaurant/main/Main.java b/src/main/java/com/rohitawate/restaurant/main/Main.java index 7829fde..a8c0d05 100644 --- a/src/main/java/com/rohitawate/restaurant/main/Main.java +++ b/src/main/java/com/rohitawate/restaurant/main/Main.java @@ -33,7 +33,9 @@ public class Main extends Application { new Services(); - Parent dashboard = FXMLLoader.load(getClass().getResource("/fxml/homewindow/HomeWindow.fxml")); + FXMLLoader loader = new FXMLLoader(getClass().getResource("/fxml/homewindow/HomeWindow.fxml")); + Parent dashboard = loader.load(); + Services.homeWindowController = loader.getController(); Stage dashboardStage = new Stage(); ThemeManager.setTheme(dashboard); diff --git a/src/main/java/com/rohitawate/restaurant/requestsmanager/DELETERequestManager.java b/src/main/java/com/rohitawate/restaurant/requestsmanager/DELETERequestManager.java index 34c06a4..bee3eda 100644 --- a/src/main/java/com/rohitawate/restaurant/requestsmanager/DELETERequestManager.java +++ b/src/main/java/com/rohitawate/restaurant/requestsmanager/DELETERequestManager.java @@ -39,8 +39,6 @@ public class DELETERequestManager extends RequestManager { protected RestaurantResponse call() throws Exception { DELETERequest deleteRequest = (DELETERequest) request; - Services.historyManager.saveHistory(deleteRequest); - RestaurantResponse response = new RestaurantResponse(); WebTarget target = client.target(deleteRequest.getTarget().toString()); Map.Entry mapEntry; diff --git a/src/main/java/com/rohitawate/restaurant/requestsmanager/DataDispatchRequestManager.java b/src/main/java/com/rohitawate/restaurant/requestsmanager/DataDispatchRequestManager.java index a4d3ee2..436d1d3 100644 --- a/src/main/java/com/rohitawate/restaurant/requestsmanager/DataDispatchRequestManager.java +++ b/src/main/java/com/rohitawate/restaurant/requestsmanager/DataDispatchRequestManager.java @@ -54,8 +54,6 @@ public class DataDispatchRequestManager extends RequestManager { DataDispatchRequest dataDispatchRequest = (DataDispatchRequest) request; String requestType = dataDispatchRequest.getRequestType(); - Services.historyManager.saveHistory(dataDispatchRequest); - RestaurantResponse response = new RestaurantResponse(); WebTarget target = client.target(dataDispatchRequest.getTarget().toString()); Map.Entry mapEntry; diff --git a/src/main/java/com/rohitawate/restaurant/requestsmanager/GETRequestManager.java b/src/main/java/com/rohitawate/restaurant/requestsmanager/GETRequestManager.java index 18cce8d..7f4dc11 100644 --- a/src/main/java/com/rohitawate/restaurant/requestsmanager/GETRequestManager.java +++ b/src/main/java/com/rohitawate/restaurant/requestsmanager/GETRequestManager.java @@ -39,8 +39,6 @@ public class GETRequestManager extends RequestManager { RestaurantResponse response = new RestaurantResponse(); WebTarget target = client.target(request.getTarget().toString()); - Services.historyManager.saveHistory(request); - Builder requestBuilder = target.request(); HashMap headers = request.getHeaders(); diff --git a/src/main/java/com/rohitawate/restaurant/util/Services.java b/src/main/java/com/rohitawate/restaurant/util/Services.java index e123c3d..0706ed2 100644 --- a/src/main/java/com/rohitawate/restaurant/util/Services.java +++ b/src/main/java/com/rohitawate/restaurant/util/Services.java @@ -16,10 +16,12 @@ package com.rohitawate.restaurant.util; +import com.rohitawate.restaurant.homewindow.HomeWindowController; import com.rohitawate.restaurant.util.history.HistoryManager; public class Services { public static HistoryManager historyManager; + public static HomeWindowController homeWindowController; static { historyManager = new HistoryManager(); diff --git a/src/main/java/com/rohitawate/restaurant/util/history/HistoryManager.java b/src/main/java/com/rohitawate/restaurant/util/history/HistoryManager.java index c631e21..005c7f7 100644 --- a/src/main/java/com/rohitawate/restaurant/util/history/HistoryManager.java +++ b/src/main/java/com/rohitawate/restaurant/util/history/HistoryManager.java @@ -18,16 +18,25 @@ package com.rohitawate.restaurant.util.history; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.rohitawate.restaurant.models.DashboardState; import com.rohitawate.restaurant.models.requests.DELETERequest; import com.rohitawate.restaurant.models.requests.DataDispatchRequest; import com.rohitawate.restaurant.models.requests.GETRequest; import com.rohitawate.restaurant.models.requests.RestaurantRequest; +import com.rohitawate.restaurant.util.Services; import com.rohitawate.restaurant.util.json.JSONUtils; +import com.rohitawate.restaurant.util.settings.Settings; +import javafx.util.Pair; +import javax.ws.rs.core.MediaType; import java.io.File; import java.io.InputStream; +import java.net.MalformedURLException; import java.sql.*; import java.time.LocalDate; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; import java.util.Map; public class HistoryManager { @@ -55,6 +64,18 @@ public class HistoryManager { statement = conn.prepareStatement(JSONUtils.trimString(queries.get("createHeadersTable").toString())); statement.execute(); + + statement = + conn.prepareStatement(JSONUtils.trimString(queries.get("createRequestContentMapTable").toString())); + statement.execute(); + + statement = + conn.prepareStatement(JSONUtils.trimString(queries.get("createBodiesTable").toString())); + statement.execute(); + + statement = + conn.prepareStatement(JSONUtils.trimString(queries.get("createTuplesTable").toString())); + statement.execute(); } catch (Exception E) { E.printStackTrace(); } finally { @@ -63,47 +84,225 @@ public class HistoryManager { } // Method is made synchronized to allow only one database transaction at a time. - public synchronized void saveHistory(RestaurantRequest request) { - try { - statement = - conn.prepareStatement(JSONUtils.trimString(queries.get("saveRequest").toString())); + public synchronized void saveHistory(DashboardState state) { + new Thread(() -> { + try { + statement = + conn.prepareStatement(JSONUtils.trimString(queries.get("saveRequest").toString())); - // Determines the request type - if (request.getClass() == GETRequest.class) - statement.setString(1, "GET"); - else if (request.getClass() == DataDispatchRequest.class) { - if (((DataDispatchRequest) request).getRequestType().equals("POST")) - statement.setString(1, "POST"); - else - statement.setString(1, "PUT"); - } else if (request.getClass() == DELETERequest.class) - statement.setString(1, "DELETE"); + statement.setString(1, state.getHttpMethod()); + statement.setString(2, String.valueOf(state.getTarget())); + statement.setString(3, LocalDate.now().toString()); - statement.setString(2, String.valueOf(request.getTarget())); - statement.setString(3, LocalDate.now().toString()); + statement.executeUpdate(); - statement.executeUpdate(); + if (state.getHeaders().size() > 0) { + // Get latest RequestID to insert into Headers table + statement = conn.prepareStatement("SELECT MAX(ID) AS MaxID FROM Requests"); - if (request.getHeaders().size() > 0) { - // Get latest RequestID to insert into Headers table - statement = conn.prepareStatement("SELECT MAX(ID) AS MaxID FROM Requests"); + ResultSet RS = statement.executeQuery(); + int requestID = -1; + if (RS.next()) + requestID = RS.getInt("MaxID"); - ResultSet RS = statement.executeQuery(); - int requestID = -1; - if (RS.next()) - requestID = RS.getInt("MaxID"); + // Saves request headers + statement = conn.prepareStatement(JSONUtils.trimString(queries.get("saveHeader").toString())); + for (Map.Entry entry : state.getHeaders().entrySet()) { + statement.setInt(1, requestID); + statement.setString(2, entry.getKey().toString()); + statement.setString(3, entry.getValue().toString()); - statement = conn.prepareStatement(JSONUtils.trimString(queries.get("saveHeader").toString())); - for (Map.Entry entry : request.getHeaders().entrySet()) { - statement.setInt(1, requestID); - statement.setString(2, entry.getKey().toString()); - statement.setString(3, entry.getValue().toString()); + statement.executeUpdate(); + } - statement.executeUpdate(); + if (state.getHttpMethod().equals("POST") || state.getHttpMethod().equals("PUT")) { + // Maps the request to its ContentType for faster recovery + statement = conn.prepareStatement(JSONUtils.trimString(queries.get("saveRequestContentPair").toString())); + statement.setInt(1, requestID); + statement.setString(2, state.getContentType()); + + statement.executeUpdate(); + + // Determines where to fetch the body from, based on the ContentType + switch (state.getContentType()) { + case MediaType.TEXT_PLAIN: + case MediaType.APPLICATION_JSON: + case MediaType.APPLICATION_XML: + case MediaType.TEXT_HTML: + case MediaType.APPLICATION_OCTET_STREAM: + // Saves the body in case of raw content, or the file location in case of binary + statement = conn.prepareStatement(JSONUtils.trimString(queries.get("saveBody").toString())); + statement.setInt(1, requestID); + statement.setString(2, state.getBody()); + statement.executeUpdate(); + break; + case MediaType.APPLICATION_FORM_URLENCODED: + for (Map.Entry entry : state.getStringTuples().entrySet()) { + // Saves the string tuples + statement = conn.prepareStatement(JSONUtils.trimString(queries.get("saveTuple").toString())); + statement.setInt(1, requestID); + statement.setString(2, "String"); + statement.setString(3, entry.getKey()); + statement.setString(4, entry.getValue()); + + statement.executeUpdate(); + } + break; + case MediaType.MULTIPART_FORM_DATA: + for (Map.Entry entry : state.getStringTuples().entrySet()) { + // Saves the string tuples + statement = conn.prepareStatement(JSONUtils.trimString(queries.get("saveTuple").toString())); + statement.setInt(1, requestID); + statement.setString(2, "String"); + statement.setString(3, entry.getKey()); + statement.setString(4, entry.getValue()); + + statement.executeUpdate(); + } + + for (Map.Entry entry : state.getFileTuples().entrySet()) { + // Saves the file tuples + statement = conn.prepareStatement(JSONUtils.trimString(queries.get("saveTuple").toString())); + statement.setInt(1, requestID); + statement.setString(2, "File"); + statement.setString(3, entry.getKey()); + statement.setString(4, entry.getValue()); + + statement.executeUpdate(); + } + break; + } + } } + } catch (SQLException e) { + e.printStackTrace(); + } + }).start(); + // Appends this history item to the HistoryTab + Services.homeWindowController.addHistoryItem(state); + } + + public synchronized List getHistory() { + List history = new ArrayList<>(); + try { + // Loads the requests from the last x number of days, x being stored in Settings.showHistoryRange + statement = conn.prepareStatement(JSONUtils.trimString(queries.get("selectRecentRequests").toString())); + String historyStartDate = LocalDate.now().minusDays(Settings.showHistoryRange).toString(); + statement.setString(1, historyStartDate); + + ResultSet resultSet = statement.executeQuery(); + + DashboardState state; + while (resultSet.next()) { + state = new DashboardState(); + + try { + state.setTarget(resultSet.getString("Target")); + } catch (MalformedURLException e) { + e.printStackTrace(); + } + + int requestID = resultSet.getInt("ID"); + state.setHeaders(getRequestHeaders(requestID)); + state.setHttpMethod(resultSet.getString("Type")); + + if (state.getHttpMethod().equals("POST") || state.getHttpMethod().equals("PUT")) { + // Retrieves request body ContentType for querying corresponding table + statement = conn.prepareStatement(JSONUtils.trimString(queries.get("selectRequestContentType").toString())); + statement.setInt(1, requestID); + + ResultSet RS = statement.executeQuery(); + + String contentType = ""; + if (RS.next()) + contentType = RS.getString("ContentType"); + + state.setContentType(contentType); + + // Retrieves body from corresponding table + switch (contentType) { + case MediaType.TEXT_PLAIN: + case MediaType.APPLICATION_JSON: + case MediaType.APPLICATION_XML: + case MediaType.TEXT_HTML: + case MediaType.APPLICATION_OCTET_STREAM: + statement = conn.prepareStatement(JSONUtils.trimString(queries.get("selectRequestBody").toString())); + statement.setInt(1, requestID); + + RS = statement.executeQuery(); + + if (RS.next()) + state.setBody(resultSet.getString("Body")); + break; + case MediaType.APPLICATION_FORM_URLENCODED: + state.setStringTuples(getTuples(requestID, "String")); + break; + case MediaType.MULTIPART_FORM_DATA: + state.setStringTuples(getTuples(requestID, "String")); + state.setFileTuples(getTuples(requestID, "Files")); + break; + } + } + + history.add(state); } } catch (SQLException e) { e.printStackTrace(); } + return history; + } + + private HashMap getRequestHeaders(int requestID) { + HashMap headers = new HashMap<>(); + + try { + PreparedStatement statement = + conn.prepareStatement(JSONUtils.trimString(queries.get("selectRequestHeaders").toString())); + statement.setInt(1, requestID); + + ResultSet RS = statement.executeQuery(); + + String key, value; + while (RS.next()) { + key = RS.getString("Key"); + value = RS.getString("Value"); + headers.put(key, value); + } + } catch (SQLException e) { + e.printStackTrace(); + } + return headers; + } + + /** + * + * @param requestID Database ID of the request whose tuples are needed. + * @param type Type of tuples needed ('String' or 'File') + * @return tuples - Map of tuples of corresponding type + */ + private HashMap getTuples(int requestID, String type) { + if (!type.equals("String") || !type.equals("File")) + return null; + + HashMap tuples = new HashMap<>(); + + try { + PreparedStatement statement = + conn.prepareStatement(JSONUtils.trimString(queries.get("selectTuples").toString())); + statement.setInt(1, requestID); + statement.setString(2, type); + + ResultSet RS = statement.executeQuery(); + + String key, value; + while (RS.next()) { + key = RS.getString("Key"); + value = RS.getString("Value"); + tuples.put(key, value); + } + } catch (SQLException e) { + e.printStackTrace(); + } + return tuples; } } diff --git a/src/main/java/com/rohitawate/restaurant/util/settings/Settings.java b/src/main/java/com/rohitawate/restaurant/util/settings/Settings.java index 704d989..880a736 100644 --- a/src/main/java/com/rohitawate/restaurant/util/settings/Settings.java +++ b/src/main/java/com/rohitawate/restaurant/util/settings/Settings.java @@ -31,4 +31,5 @@ public class Settings { public static int connectionReadTimeOut = 30000; public static String theme = "Adreana"; + public static long showHistoryRange = 7; } diff --git a/src/main/resources/css/Adreana.css b/src/main/resources/css/Adreana.css index ae046a0..4d3fca7 100644 --- a/src/main/resources/css/Adreana.css +++ b/src/main/resources/css/Adreana.css @@ -98,6 +98,7 @@ .scroll-pane .viewport, .scroll-pane .scroll-bar:vertical { -fx-background-color: #3d3d3d; + -fx-background-insets: 0px; } /* Tab attributes */ @@ -141,11 +142,19 @@ -fx-background-color: #808080; } -.split-pane, +.split-pane { + -fx-background-color: #505050; + -fx-padding: 0px; +} + .split-pane .split-pane-divider { -fx-background-color: #505050; } +.split-pane:horizontal .split-pane-divider { + -fx-padding: 0px; +} + #keyField, #valueField, #filePathField { -fx-prompt-text-fill: #919191; -fx-background-color: #303030; @@ -170,6 +179,22 @@ -fx-background-color: #a2a2a2; } +/* History tab */ +#historyPane, #historyTab { + -fx-background-color: #404040; +} + +#historySearchField { + -fx-background-color: #505050; + -fx-text-fill: white; +} + +/* History item */ +#historyItemBox { + -fx-background-color: #353535; +} + +/* SnackBar */ .jfx-snackbar-content { -fx-background-color: black; } diff --git a/src/main/resources/fxml/homewindow/Dashboard.fxml b/src/main/resources/fxml/homewindow/Dashboard.fxml index a3f7fe0..642c0fd 100644 --- a/src/main/resources/fxml/homewindow/Dashboard.fxml +++ b/src/main/resources/fxml/homewindow/Dashboard.fxml @@ -16,26 +16,37 @@ ~ limitations under the License. --> - - - - - - - + + + + + + + + + + + + + + + + + + + + - + - + - + @@ -46,13 +57,12 @@ - + - + @@ -65,7 +75,7 @@ - + @@ -76,210 +86,177 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - - + + + - + - + - - - + -