Merge branch 'master' into aaron-branch-2

This commit is contained in:
Aaron Sun 2020-11-15 17:36:31 -08:00 committed by GitHub
commit 6e3402a8e5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 580 additions and 92 deletions

View File

@ -2,6 +2,7 @@ import java.sql.Connection;
import java.sql.PreparedStatement; import java.sql.PreparedStatement;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
@ -9,6 +10,7 @@ public class ChainGetter implements CallHandler {
private final Connection connection; private final Connection connection;
private final String GET_CHAIN = "SELECT * FROM Chain WHERE chainID = ?;"; private final String GET_CHAIN = "SELECT * FROM Chain WHERE chainID = ?;";
private final String GET_CHAINS = "SELECT chainID FROM Chain;";
public ChainGetter(Connection connection, String cognitoID) { public ChainGetter(Connection connection, String cognitoID) {
this.connection = connection; this.connection = connection;
@ -16,8 +18,21 @@ public class ChainGetter implements CallHandler {
@Override @Override
public Object conductAction(Map<String, Object> bodyMap, HashMap<String, String> queryMap, String cognitoID) throws SQLException { public Object conductAction(Map<String, Object> bodyMap, HashMap<String, String> queryMap, String cognitoID) throws SQLException {
Integer id = Integer.parseInt(queryMap.get("id"));
if (id == -1) {
PreparedStatement getChains = connection.prepareStatement(GET_CHAINS);
System.out.println(getChains);
ResultSet getChainsResults = getChains.executeQuery();
System.out.println(getChainsResults);
ArrayList<Integer> chainIDs = new ArrayList<>();
while (getChainsResults.next()) {
chainIDs.add(getChainsResults.getInt("chainID"));
}
return chainIDs;
}
PreparedStatement statement = connection.prepareStatement(GET_CHAIN); PreparedStatement statement = connection.prepareStatement(GET_CHAIN);
statement.setInt(1, Integer.parseInt(queryMap.get("id"))); statement.setInt(1, id);
System.out.println(statement); System.out.println(statement);
ResultSet queryResults = statement.executeQuery(); ResultSet queryResults = statement.executeQuery();
queryResults.first(); queryResults.first();

View File

@ -1,3 +1,6 @@
import com.amazonaws.services.lambda.AWSLambdaClientBuilder;
import com.amazonaws.services.lambda.model.InvokeRequest;
import java.sql.Connection; import java.sql.Connection;
import java.sql.PreparedStatement; import java.sql.PreparedStatement;
import java.sql.ResultSet; import java.sql.ResultSet;
@ -18,13 +21,30 @@ public class ItemSearcher implements CallHandler {
} }
@Override @Override
public Object conductAction(Map<String, Object> body, HashMap<String, String> queryParams, String s) throws SQLException { public Object conductAction(Map<String, Object> body, HashMap<String, String> queryParams, String cognitoID) throws SQLException {
PreparedStatement getItemMatches = connection.prepareStatement(GET_ITEM_MATCHES); PreparedStatement getItemMatches = connection.prepareStatement(GET_ITEM_MATCHES);
getItemMatches.setString(1, "%" + queryParams.get("id") + "%"); getItemMatches.setString(1, "%" + queryParams.get("id") + "%");
System.out.println(getItemMatches); System.out.println(getItemMatches);
ResultSet searchResults = getItemMatches.executeQuery(); ResultSet searchResults = getItemMatches.executeQuery();
ItemSearch searchResultsObject = new ItemSearch(searchResults); ItemSearch searchResultsObject = new ItemSearch(searchResults);
System.out.println(searchResultsObject); System.out.println(searchResultsObject);
InvokeRequest invokeRequest = new InvokeRequest();
invokeRequest.setFunctionName("SearchHistoryPOST");
invokeRequest.setPayload("{" +
" \"body\": {" +
" \"searchTerm\": \"" + queryParams.get("id") + "\"" +
" }," +
" \"params\": {" +
" \"querystring\": {" +
" }" +
" }," +
" \"context\": {" +
" \"sub\": \"" + cognitoID + "\"" +
" }" +
"}");
invokeRequest.setInvocationType("Event");
System.out.println(invokeRequest);
AWSLambdaClientBuilder.defaultClient().invoke(invokeRequest);
return searchResultsObject; return searchResultsObject;
} }
} }

View File

@ -9,7 +9,7 @@ public class ListAdder implements CallHandler {
private String cognitoID; private String cognitoID;
private final String LIST_CREATE = "INSERT INTO List (name, owner, lastUpdated) VALUES (?, ?, ?);"; private final String LIST_CREATE = "INSERT INTO List (name, owner, lastUpdated) VALUES (?, ?, ?);";
private final String LIST_ACCESS_GRANT = "INSERT INTO ListSharee(listID, userID) VALUES(?, ?);"; private final String LIST_ACCESS_GRANT = "INSERT INTO ListSharee(listID, userID, permissionLevel) VALUES(?, ?, ?);";
public ListAdder(Connection connection, String cognitoID) { public ListAdder(Connection connection, String cognitoID) {
this.connection = connection; this.connection = connection;
@ -31,6 +31,7 @@ public class ListAdder implements CallHandler {
PreparedStatement accessGrant = connection.prepareStatement(LIST_ACCESS_GRANT); PreparedStatement accessGrant = connection.prepareStatement(LIST_ACCESS_GRANT);
accessGrant.setInt(1, newID); accessGrant.setInt(1, newID);
accessGrant.setString(2, cognitoID); accessGrant.setString(2, cognitoID);
accessGrant.setInt(3, ListPermissions.getAll());
System.out.println(accessGrant); System.out.println(accessGrant);
accessGrant.executeUpdate(); accessGrant.executeUpdate();
connection.commit(); connection.commit();

View File

@ -10,7 +10,7 @@ public class ListDeleter implements CallHandler {
private final Connection connection; private final Connection connection;
private final String cognitoID; private final String cognitoID;
private final String GET_LISTS = "SELECT * FROM List WHERE (owner = ? AND listID = ?);"; private final String ACCESS_CHECK = "SELECT * from ListSharee WHERE userID = ? and listID = ?;";
private final String DELETE_LIST = "DELETE FROM List WHERE listID = ?;"; private final String DELETE_LIST = "DELETE FROM List WHERE listID = ?;";
private final String DELETE_REQUESTOR_ACCESS = "DELETE FROM ListSharee where listID = ? AND userID = ?;"; private final String DELETE_REQUESTOR_ACCESS = "DELETE FROM ListSharee where listID = ? AND userID = ?;";
private final String DELETE_LIST_ACCESS = "DELETE FROM ListSharee where listID = ?;"; private final String DELETE_LIST_ACCESS = "DELETE FROM ListSharee where listID = ?;";
@ -24,19 +24,24 @@ public class ListDeleter implements CallHandler {
@Override @Override
public Object conductAction(Map<String, Object> bodyMap, HashMap<String, String> queryMap, String cognitoID) throws SQLException { public Object conductAction(Map<String, Object> bodyMap, HashMap<String, String> queryMap, String cognitoID) throws SQLException {
Integer listID = Integer.parseInt(queryMap.get("id")); Integer listID = Integer.parseInt(queryMap.get("id"));
PreparedStatement cleanRequestorAccess = connection.prepareStatement(DELETE_REQUESTOR_ACCESS);
cleanRequestorAccess.setInt(1, listID);
cleanRequestorAccess.setString(2, cognitoID);
System.out.println(cleanRequestorAccess);
cleanRequestorAccess.executeUpdate();
PreparedStatement accessCheck = connection.prepareStatement(GET_LISTS); PreparedStatement accessCheck = connection.prepareStatement(ACCESS_CHECK);
accessCheck.setString(1, cognitoID); accessCheck.setString(1, cognitoID);
accessCheck.setInt(2, listID); accessCheck.setInt(2, listID);
System.out.println(accessCheck); System.out.println(accessCheck);
ResultSet userLists = accessCheck.executeQuery(); ResultSet userLists = accessCheck.executeQuery();
if (!userLists.next()) { if (!userLists.next()) {
throw new AccessControlException("User does not have access to list"); throw new AccessControlException("User does not have access to list");
} else {
Integer permissionLevel = userLists.getInt("permissionLevel");
if (!ListPermissions.hasPermission(permissionLevel, "Delete")) {
PreparedStatement cleanRequestorAccess = connection.prepareStatement(DELETE_REQUESTOR_ACCESS);
cleanRequestorAccess.setInt(1, listID);
cleanRequestorAccess.setString(2, cognitoID);
System.out.println(cleanRequestorAccess);
cleanRequestorAccess.executeUpdate();
return null;
}
} }
PreparedStatement cleanAccess = connection.prepareStatement(DELETE_LIST_ACCESS); PreparedStatement cleanAccess = connection.prepareStatement(DELETE_LIST_ACCESS);
cleanAccess.setInt(1, listID); cleanAccess.setInt(1, listID);

View File

@ -1,3 +1,4 @@
import java.security.AccessControlException;
import java.sql.Connection; import java.sql.Connection;
import java.sql.PreparedStatement; import java.sql.PreparedStatement;
import java.sql.ResultSet; import java.sql.ResultSet;
@ -40,9 +41,19 @@ public class ListGetter implements CallHandler{
System.out.println(checkAccess); System.out.println(checkAccess);
ResultSet accessResults = checkAccess.executeQuery(); ResultSet accessResults = checkAccess.executeQuery();
int sharees = 0; int sharees = 0;
while (sharees < 2 && accessResults.next()) { boolean verifiedAccess = false;
while ((sharees < 2 && accessResults.next()) || !verifiedAccess) {
int permissionLevel = accessResults.getInt("permissionLevel");
if (accessResults.getString("userID").equals(cognitoID)) {
verifiedAccess = true;
if (!ListPermissions.hasPermission(permissionLevel, "Read")) {
throw new AccessControlException("User " + cognitoID + " does not have permission to read list " + id);
}
}
if (permissionLevel > 0) {
sharees++; sharees++;
} }
}
boolean shared = false; boolean shared = false;
if (sharees > 1) { if (sharees > 1) {
shared = true; shared = true;

View File

@ -0,0 +1,41 @@
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
public class ListPermissions {
private static final Map<Integer, String> keysToPerms;
static {
//All keys should be a prime number > 1
//All keys need to be maintained here and in ListShare object in data on the client side
HashMap<Integer, String> keysToPermsTemp = new HashMap<>();
keysToPermsTemp.put(2, "read");
keysToPermsTemp.put(3, "write");
keysToPermsTemp.put(5, "delete");
keysToPermsTemp.put(7, "share");
keysToPerms = Collections.unmodifiableMap(keysToPermsTemp);
}
public static Integer getAll() {
Integer toReturn = 1;
for (Integer key : keysToPerms.keySet()) {
toReturn *= key;
}
return toReturn;
}
public static boolean hasPermission(Integer level, String permission) {
return level % getKeyForPermission(permission) == 0;
}
public static Integer getKeyForPermission(String permissionRaw) {
String permission = permissionRaw.toLowerCase();
for (Map.Entry<Integer, String> entry : keysToPerms.entrySet()) {
if (entry.getValue().equals(permission)) {
return entry.getKey();
}
}
System.out.println("Tried to get key for invalid permission: " + permission);
return -1;
}
}

View File

@ -1,3 +1,4 @@
import java.security.AccessControlException;
import java.sql.*; import java.sql.*;
import java.time.Instant; import java.time.Instant;
import java.util.HashMap; import java.util.HashMap;
@ -8,7 +9,7 @@ public class ListEntryAdder implements CallHandler {
private Connection connection; private Connection connection;
private String cognitoID; private String cognitoID;
private final String ACCESS_CHECK = "SELECT * from ListSharee WHERE userID = ? and listID = ?;";
private final String CHECK_ITEM_IN_LIST = "SELECT quantity from ListProduct WHERE productID = ? AND listID = ?;"; private final String CHECK_ITEM_IN_LIST = "SELECT quantity from ListProduct WHERE productID = ? AND listID = ?;";
private final String CLEAR_PAIRING = "DELETE from ListProduct WHERE productID = ? AND listID = ?;"; private final String CLEAR_PAIRING = "DELETE from ListProduct WHERE productID = ? AND listID = ?;";
private final String ITEM_TO_LIST = "INSERT INTO ListProduct (productID, listID, quantity, addedDate, purchased) VALUES (?, ?, ?, ?, ?)"; private final String ITEM_TO_LIST = "INSERT INTO ListProduct (productID, listID, quantity, addedDate, purchased) VALUES (?, ?, ?, ?, ?)";
@ -19,12 +20,24 @@ public class ListEntryAdder implements CallHandler {
} }
public Object conductAction(Map<String, Object> bodyMap, HashMap<String, String> queryString, String cognitoID) throws SQLException { public Object conductAction(Map<String, Object> bodyMap, HashMap<String, String> queryString, String cognitoID) throws SQLException {
PreparedStatement quantitiyStatement = connection.prepareStatement(CHECK_ITEM_IN_LIST);
Integer productID = (Integer) bodyMap.get("productID"); Integer productID = (Integer) bodyMap.get("productID");
Integer listID = (Integer) bodyMap.get("listID"); Integer listID = (Integer) bodyMap.get("listID");
quantitiyStatement.setInt(1, productID); PreparedStatement accessCheck = connection.prepareStatement(ACCESS_CHECK);
quantitiyStatement.setInt(2, listID); accessCheck.setString(1, cognitoID);
ResultSet quanitityRS = quantitiyStatement.executeQuery(); accessCheck.setInt(2, listID);
ResultSet access = accessCheck.executeQuery();
if (access.next()) {
if (!ListPermissions.hasPermission(access.getInt("permissionLevel"), "Write")) {
throw new AccessControlException("User " + cognitoID + " does not have write permissions for list " + listID);
}
} else {
throw new AccessControlException("User " + cognitoID + " does not have any permissions to access list " + listID);
}
PreparedStatement quantityStatement = connection.prepareStatement(CHECK_ITEM_IN_LIST);
quantityStatement.setInt(1, productID);
quantityStatement.setInt(2, listID);
ResultSet quanitityRS = quantityStatement.executeQuery();
int priorQuanity = 0; int priorQuanity = 0;
if (quanitityRS.next()) { if (quanitityRS.next()) {
priorQuanity = quanitityRS.getInt(1); priorQuanity = quanitityRS.getInt(1);

View File

@ -1,5 +1,7 @@
import java.security.AccessControlException;
import java.sql.Connection; import java.sql.Connection;
import java.sql.PreparedStatement; import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
@ -10,6 +12,7 @@ public class ListEntryDeleter implements CallHandler {
private String cognitoID; private String cognitoID;
private final String REMOVE_FROM_LIST = "DELETE FROM ListProduct WHERE (ProductID = ? AND ListID = ?);"; private final String REMOVE_FROM_LIST = "DELETE FROM ListProduct WHERE (ProductID = ? AND ListID = ?);";
private final String ACCESS_CHECK = "SELECT * from ListSharee WHERE userID = ? and listID = ?;";
public ListEntryDeleter(Connection connection, String cognitoID) { public ListEntryDeleter(Connection connection, String cognitoID) {
this.connection = connection; this.connection = connection;
@ -17,9 +20,26 @@ public class ListEntryDeleter implements CallHandler {
} }
public Object conductAction(Map<String, Object> bodyMap, HashMap<String, String> queryString, String cognitoID) throws SQLException { public Object conductAction(Map<String, Object> bodyMap, HashMap<String, String> queryString, String cognitoID) throws SQLException {
Integer productID = (Integer) bodyMap.get("productID");
Integer listID = (Integer) bodyMap.get("listID");
PreparedStatement accessCheck = connection.prepareStatement(ACCESS_CHECK);
accessCheck.setString(1, cognitoID);
accessCheck.setInt(2, listID);
ResultSet access = accessCheck.executeQuery();
if (access.next()) {
if (!ListPermissions.hasPermission(access.getInt("permissionLevel"), "Write")) {
throw new AccessControlException("User " + cognitoID + " does not have write permissions for list " + listID);
}
} else {
throw new AccessControlException("User " + cognitoID + " does not have any permissions to access list " + listID);
}
PreparedStatement statement = connection.prepareStatement(REMOVE_FROM_LIST); PreparedStatement statement = connection.prepareStatement(REMOVE_FROM_LIST);
statement.setInt(1, (Integer) bodyMap.get("productID")); statement.setInt(1, productID);
statement.setInt(2, (Integer) bodyMap.get("listID")); statement.setInt(2, listID);
System.out.println(statement); System.out.println(statement);
statement.executeUpdate(); statement.executeUpdate();
connection.commit(); connection.commit();

View File

@ -3,7 +3,7 @@ import com.amazonaws.services.lambda.runtime.RequestHandler;
import java.util.Map; import java.util.Map;
public class ListSharePOST implements RequestHandler<Map<String,Object>, Object> { public class ListSharePUT implements RequestHandler<Map<String,Object>, Object> {
public Object handleRequest(Map<String, Object> inputMap, Context unfilled) { public Object handleRequest(Map<String, Object> inputMap, Context unfilled) {
return BasicHandler.handleRequest(inputMap, unfilled, ListSharer.class); return BasicHandler.handleRequest(inputMap, unfilled, ListSharer.class);

View File

@ -22,7 +22,8 @@ public class ListSharer implements CallHandler {
} }
final private String CHECK_ACCESS = "SELECT * from ListSharee WHERE listID = ? AND userID = ?;"; final private String CHECK_ACCESS = "SELECT * from ListSharee WHERE listID = ? AND userID = ?;";
final private String SHARE_LIST = "INSERT INTO ListSharee(listID, userID) VALUES(?, ?);"; final private String SHARE_LIST = "INSERT INTO ListSharee(listID, userID, permissionLevel, uiPosition) VALUES(?, ?, ?, ?) ON DUPLICATE KEY UPDATE permissionLevel = ?;";
public Object conductAction(Map<String, Object> bodyMap, HashMap<String, String> queryString, String cognitoID) throws SQLException { public Object conductAction(Map<String, Object> bodyMap, HashMap<String, String> queryString, String cognitoID) throws SQLException {
PreparedStatement checkAccess = connection.prepareStatement(CHECK_ACCESS); PreparedStatement checkAccess = connection.prepareStatement(CHECK_ACCESS);
@ -30,8 +31,12 @@ public class ListSharer implements CallHandler {
checkAccess.setInt(1, listID); checkAccess.setInt(1, listID);
checkAccess.setString(2, cognitoID); checkAccess.setString(2, cognitoID);
ResultSet checkAccessRS = checkAccess.executeQuery(); ResultSet checkAccessRS = checkAccess.executeQuery();
if (!checkAccessRS.next()) { if (checkAccessRS.next()) {
throw new AccessControlException("The requesting user does not have access to the requested list"); if (!ListPermissions.hasPermission(checkAccessRS.getInt("permissionLevel"), "Share")) {
throw new AccessControlException("User " + cognitoID + " does not have share permissions for list " + listID);
}
} else {
throw new AccessControlException("User " + cognitoID + " does not have any permissions to access list " + listID);
} }
InvokeRequest invokeRequest = new InvokeRequest(); InvokeRequest invokeRequest = new InvokeRequest();
invokeRequest.setFunctionName("UserGET"); invokeRequest.setFunctionName("UserGET");
@ -52,15 +57,18 @@ public class ListSharer implements CallHandler {
throw new InputMismatchException("Could not find specified user to share with"); throw new InputMismatchException("Could not find specified user to share with");
} }
String shareWithSub = new String(invokeResult.getPayload().array()).replace("\"", ""); String shareWithSub = new String(invokeResult.getPayload().array()).replace("\"", "");
checkAccess.setString(2, shareWithSub); // checkAccess.setString(2, shareWithSub);
checkAccessRS = checkAccess.executeQuery(); // checkAccessRS = checkAccess.executeQuery();
if (checkAccessRS.next()) { // if (checkAccessRS.next()) {
throw new InputMismatchException("The specified user already has access"); // throw new InputMismatchException("The specified user already has access");
} // }
PreparedStatement shareList = connection.prepareStatement(SHARE_LIST); PreparedStatement shareList = connection.prepareStatement(SHARE_LIST);
shareList.setInt(1, listID); shareList.setInt(1, listID);
shareList.setString(2, shareWithSub); shareList.setString(2, shareWithSub);
Integer permissionLevel = Integer.parseInt(bodyMap.get("permissionLevel").toString());
shareList.setInt(3, permissionLevel);
shareList.setInt(4, permissionLevel);
shareList.executeUpdate(); shareList.executeUpdate();
connection.commit(); connection.commit();
return null; return null;

View File

@ -0,0 +1,43 @@
import java.io.Serializable;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
public class SearchHistory implements Serializable {
ArrayList<String> searches;
public SearchHistory(ArrayList<String> searches) {
this.searches = searches;
}
public SearchHistory() {
this.searches = new ArrayList<>();
}
public SearchHistory(ResultSet row) throws SQLException {
this.searches = new ArrayList<>();
row.beforeFirst();
while (row.next()) {
this.searches.add(row.getString("search"));
}
}
public ArrayList<String> getSearches() {
return searches;
}
public void setSearches(ArrayList<String> searches) {
this.searches = searches;
}
public void addSearch(String newSearch) {
searches.add(newSearch);
}
@Override
public String toString() {
return "SearchHistory{" +
"searches=" + searches +
'}';
}
}

View File

@ -0,0 +1,11 @@
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import java.util.Map;
public class SearchHistoryGET implements RequestHandler<Map<String,Object>, Object> {
public Object handleRequest(Map<String, Object> inputMap, Context unfilled) {
return BasicHandler.handleRequest(inputMap, unfilled, SearchHistoryGetter.class);
}
}

View File

@ -0,0 +1,31 @@
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
public class SearchHistoryGetter implements CallHandler {
private Connection connection;
private String cognitoID;
public SearchHistoryGetter(Connection connection, String cognitoID) {
this.connection = connection;
this.cognitoID = cognitoID;
}
final private String SELECT_HISTORY = "SELECT * from SearchHistory WHERE userID = ?;";
public Object conductAction(Map<String, Object> bodyMap, HashMap<String, String> queryString, String cognitoID) throws SQLException {
PreparedStatement select_history = connection.prepareStatement(SELECT_HISTORY);
select_history.setString(1, cognitoID);
System.out.println(select_history);
ResultSet searchHistory = select_history.executeQuery();
if (!searchHistory.first()) {
return new SearchHistory();
}
System.out.println(new SearchHistory(searchHistory));
return new SearchHistory(searchHistory);
}
}

View File

@ -0,0 +1,11 @@
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import java.util.Map;
public class SearchHistoryPOST implements RequestHandler<Map<String,Object>, Object> {
public Object handleRequest(Map<String, Object> inputMap, Context unfilled) {
return BasicHandler.handleRequest(inputMap, unfilled, SearchHistoryUpdater.class);
}
}

View File

@ -0,0 +1,28 @@
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
public class SearchHistoryUpdater implements CallHandler {
private Connection connection;
private String cognitoID;
public SearchHistoryUpdater(Connection connection, String cognitoID) {
this.connection = connection;
this.cognitoID = cognitoID;
}
final private String UPDATE_HISTORY = "INSERT INTO SearchHistory(userID, search) VALUES(?, ?);";
public Object conductAction(Map<String, Object> bodyMap, HashMap<String, String> queryString, String cognitoID) throws SQLException {
PreparedStatement store_history = connection.prepareStatement(UPDATE_HISTORY);
store_history.setString(1, cognitoID);
store_history.setObject(2, bodyMap.get("searchTerm"));
System.out.println(store_history);
store_history.executeUpdate();
connection.commit();
return null;
}
}

View File

@ -28,12 +28,23 @@ public class UserGetter implements CallHandler {
System.out.println(userPoolId); System.out.println(userPoolId);
ListUsersRequest checkRequest = new ListUsersRequest().withUserPoolId(userPoolId); ListUsersRequest checkRequest = new ListUsersRequest().withUserPoolId(userPoolId);
Object emailObject = bodyMap.get("emailToCheck"); Object emailObject = bodyMap.get("emailToCheck");
String attributeToGet = "sub";
if (emailObject != null) { if (emailObject != null) {
checkRequest.setFilter("email=\"" + emailObject.toString() +"\""); checkRequest.setFilter("email=\"" + emailObject.toString() +"\"");
} else { } else {
// checkRequest.setFilter("sub=\"" + cognitoID + "\""); try {
String id = queryMap.get("id");
if ((id != null) && (!id.equals(""))) {
attributeToGet = "email";
checkRequest.setFilter("sub=\"" + cognitoID + "\"");
} else {
return cognitoID; return cognitoID;
} }
} catch (Exception e) {
System.out.println(e);
return cognitoID;
}
}
System.out.println(checkRequest); System.out.println(checkRequest);
AWSCognitoIdentityProvider awsCognitoIdentityProvider = AWSCognitoIdentityProviderClientBuilder.defaultClient(); AWSCognitoIdentityProvider awsCognitoIdentityProvider = AWSCognitoIdentityProviderClientBuilder.defaultClient();
ListUsersResult foundUsersResult = awsCognitoIdentityProvider.listUsers(checkRequest); ListUsersResult foundUsersResult = awsCognitoIdentityProvider.listUsers(checkRequest);
@ -47,14 +58,14 @@ public class UserGetter implements CallHandler {
} }
UserType foundUser = foundUsers.get(0); UserType foundUser = foundUsers.get(0);
System.out.println(foundUser.getAttributes()); System.out.println(foundUser.getAttributes());
String sub = ""; String attributeToReturn = "";
for (AttributeType attribute : foundUser.getAttributes()) { for (AttributeType attribute : foundUser.getAttributes()) {
if (attribute.getName().equals("sub")) { if (attribute.getName().equals(attributeToGet)) {
sub = attribute.getValue(); attributeToReturn = attribute.getValue();
break; break;
} }
System.out.println(attribute.getName() + ": " + attribute.getValue()); System.out.println(attribute.getName() + ": " + attribute.getValue());
} }
return sub; return attributeToReturn;
} }
} }

View File

@ -94,6 +94,11 @@
<artifactId>jackson-dataformat-xml</artifactId> <artifactId>jackson-dataformat-xml</artifactId>
<version>2.8.5</version> <version>2.8.5</version>
</dependency> </dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.11.3</version>
</dependency>
<dependency> <dependency>
<groupId>com.fasterxml.jackson.core</groupId> <groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId> <artifactId>jackson-databind</artifactId>

View File

@ -3,25 +3,16 @@ package com.example.listify;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log; import android.util.Log;
import android.view.LayoutInflater; import android.view.*;
import android.view.View;
import android.view.ViewGroup;
import android.widget.*; import android.widget.*;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import com.bumptech.glide.Glide; import com.bumptech.glide.Glide;
import com.example.listify.data.*;
import com.example.listify.data.Chain; import org.json.JSONException;
import com.example.listify.data.Item;
import com.example.listify.data.List;
import com.example.listify.data.ListEntry;
import com.example.listify.data.ListShare;
import com.example.listify.ui.SignupPage;
import java.io.IOException; import java.io.IOException;
import java.text.DecimalFormat; import java.text.DecimalFormat;
@ -30,8 +21,6 @@ import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Properties; import java.util.Properties;
import org.json.JSONException;
import static com.example.listify.MainActivity.am; import static com.example.listify.MainActivity.am;
public class ListPage extends AppCompatActivity implements Requestor.Receiver { public class ListPage extends AppCompatActivity implements Requestor.Receiver {
@ -125,9 +114,9 @@ public class ListPage extends AppCompatActivity implements Requestor.Receiver {
public void onClick(DialogInterface dialog, int which) { public void onClick(DialogInterface dialog, int which) {
EditText sharedEmailText = (EditText) codeView.findViewById(R.id.editTextTextSharedEmail); EditText sharedEmailText = (EditText) codeView.findViewById(R.id.editTextTextSharedEmail);
String sharedEmail = sharedEmailText.getText().toString(); String sharedEmail = sharedEmailText.getText().toString();
ListShare listShare = new ListShare(listID, sharedEmail); ListShare listShare = new ListShare(listID, sharedEmail, "Read, Write, Delete, Share");
try { try {
requestor.postObject(listShare); requestor.putObject(listShare);
} }
catch(Exception e) { catch(Exception e) {
e.printStackTrace(); e.printStackTrace();
@ -144,6 +133,51 @@ public class ListPage extends AppCompatActivity implements Requestor.Receiver {
}); });
} }
@Override
public boolean onCreateOptionsMenu(Menu menu) {
//Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.list, menu);
// return super.onCreateOptionsMenu(menu);
MenuItem renameItem = menu.findItem(R.id.action_rename_list);
renameItem.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem item) {
Toast.makeText(ListPage.this, "Rename List", Toast.LENGTH_SHORT).show();
return false;
}
});
MenuItem shareItem = menu.findItem(R.id.action_share_list);
shareItem.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem item) {
Toast.makeText(ListPage.this, "Share List", Toast.LENGTH_SHORT).show();
return false;
}
});
MenuItem duplicateItem = menu.findItem(R.id.action_duplicate_list);
duplicateItem.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem item) {
Toast.makeText(ListPage.this, "Duplicate List", Toast.LENGTH_SHORT).show();
return false;
}
});
MenuItem exportItem = menu.findItem(R.id.action_export_list);
exportItem.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem item) {
Toast.makeText(ListPage.this, "Export List", Toast.LENGTH_SHORT).show();
return false;
}
});
return true;
}
@Override @Override
public void acceptDelivery(Object delivered) { public void acceptDelivery(Object delivered) {
List list = (List) delivered; List list = (List) delivered;

View File

@ -16,19 +16,15 @@ import androidx.navigation.Navigation;
import androidx.navigation.ui.AppBarConfiguration; import androidx.navigation.ui.AppBarConfiguration;
import androidx.navigation.ui.NavigationUI; import androidx.navigation.ui.NavigationUI;
import com.amplifyframework.auth.AuthException; import com.amplifyframework.auth.AuthException;
import com.example.listify.data.Item;
import com.example.listify.data.ItemSearch;
import com.example.listify.data.List; import com.example.listify.data.List;
import com.example.listify.data.ListEntry; import com.example.listify.data.SearchHistory;
import com.example.listify.ui.LoginPage; import com.example.listify.ui.LoginPage;
import com.google.android.material.navigation.NavigationView; import com.google.android.material.navigation.NavigationView;
import org.json.JSONException; import org.json.JSONException;
import java.io.IOException; import java.io.IOException;
import java.time.Instant; import java.time.Instant;
import java.util.Arrays;
import java.util.Properties; import java.util.Properties;
import java.util.Random;
import static com.example.listify.SplashActivity.showSplash; import static com.example.listify.SplashActivity.showSplash;
@ -108,7 +104,14 @@ public class MainActivity extends AppCompatActivity implements CreateListDialogF
} }
Requestor requestor = new Requestor(authManager, configs.getProperty("apiKey")); Requestor requestor = new Requestor(authManager, configs.getProperty("apiKey"));
SynchronousReceiver<SearchHistory> historyReceiver = new SynchronousReceiver<>();
requestor.getObject("N/A", SearchHistory.class, historyReceiver, historyReceiver);
try {
System.out.println(historyReceiver.await());
} catch (Exception e) {
e.printStackTrace();
}
/*
List testList = new List(-1, "New List", "user filled by lambda", Instant.now().toEpochMilli()); List testList = new List(-1, "New List", "user filled by lambda", Instant.now().toEpochMilli());
ListEntry entry = new ListEntry(1, 4, Math.abs(new Random().nextInt()), Instant.now().toEpochMilli(),false); ListEntry entry = new ListEntry(1, 4, Math.abs(new Random().nextInt()), Instant.now().toEpochMilli(),false);
@ -139,6 +142,7 @@ public class MainActivity extends AppCompatActivity implements CreateListDialogF
} catch (Exception receiverError) { } catch (Exception receiverError) {
receiverError.printStackTrace(); receiverError.printStackTrace();
} }
*/
} }
//------------------------------------------------------------------------------------------// //------------------------------------------------------------------------------------------//

View File

@ -60,6 +60,25 @@ public class Requestor {
launchCall(deleteRequest, null, classType, failureHandler); launchCall(deleteRequest, null, classType, failureHandler);
} }
public void putObject(Object toPost) throws JSONException {
putObject(toPost, (RequestErrorHandler) null);
}
public void putObject(Object toPost, RequestErrorHandler failureHandler) throws JSONException {
putObject(toPost, null, failureHandler);
}
public void putObject(Object toPost, Receiver<Integer> idReceiver) throws JSONException {
putObject(toPost, idReceiver, null);
}
public void putObject(Object toPut, Receiver<Integer> idReceiver, RequestErrorHandler failureHandler) throws JSONException {
String putURL = DEV_BASEURL + "/" + toPut.getClass().getSimpleName();
Request putRequest = buildBaseRequest(putURL, "PUT", new Gson().toJson(toPut));
launchCall(putRequest, idReceiver, Integer.class, failureHandler);
}
public void postObject(Object toPost) throws JSONException { public void postObject(Object toPost) throws JSONException {
postObject(toPost, (RequestErrorHandler) null); postObject(toPost, (RequestErrorHandler) null);
} }
@ -85,7 +104,7 @@ public class Requestor {
String responseString = response.body().string(); String responseString = response.body().string();
if (receiver != null) { if (receiver != null) {
if (classType == null) { if (classType == null) {
Log.e("Requestor Contract Error", "classType while receiver populated"); Log.e("Requestor Contract Error", "no/null classType while receiver populated");
} }
try { try {
receiver.acceptDelivery(new Gson().fromJson(responseString, classType)); receiver.acceptDelivery(new Gson().fromJson(responseString, classType));

View File

@ -129,9 +129,9 @@ public class ShoppingListsSwipeableAdapter extends BaseAdapter {
public void onClick(DialogInterface dialog, int which) { public void onClick(DialogInterface dialog, int which) {
EditText sharedEmailText = (EditText) codeView.findViewById(R.id.editTextTextSharedEmail); EditText sharedEmailText = (EditText) codeView.findViewById(R.id.editTextTextSharedEmail);
String sharedEmail = sharedEmailText.getText().toString(); String sharedEmail = sharedEmailText.getText().toString();
ListShare listShare = new ListShare(curList.getItemID(), sharedEmail); ListShare listShare = new ListShare(curList.getItemID(), sharedEmail, "Read, Write, Delete, Share");
try { try {
requestor.postObject(listShare); requestor.putObject(listShare);
} }
catch(Exception e) { catch(Exception e) {
e.printStackTrace(); e.printStackTrace();

View File

@ -1,20 +1,71 @@
package com.example.listify.data; package com.example.listify.data;
import com.example.listify.BuildConfig;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
public class ListShare { public class ListShare {
Integer listID; Integer listID;
String shareWithEmail; String shareWithEmail;
final ListShare[] other; final ListShare[] other;
Integer permissionLevel;
private static final Map<Integer, String> keysToPerms;
public ListShare(Integer listID, String shareWithEmail, ListShare[] other) { static {
this.listID = listID; //All keys should be a prime number > 1
this.shareWithEmail = shareWithEmail; //All keys need to be maintained here and in List module->ListPermissions class on the Lambda side
this.other = other; HashMap<Integer, String> keysToPermsTemp = new HashMap<>();
keysToPermsTemp.put(2, "read");
keysToPermsTemp.put(3, "write");
keysToPermsTemp.put(5, "delete");
keysToPermsTemp.put(7, "share");
keysToPerms = Collections.unmodifiableMap(keysToPermsTemp);
} }
public ListShare(Integer listID, String shareWithEmail) { public ListShare(Integer listID, String shareWithEmail, Integer permissionLevel, ListShare[] other) {
this.listID = listID; this.listID = listID;
this.shareWithEmail = shareWithEmail; this.shareWithEmail = shareWithEmail;
this.other = null; this.permissionLevel = permissionLevel;
this.other = other
}
public ListShare(Integer listID, String shareWithEmail, String permissionsRaw, ListShare[] other) {
String permissions = permissionsRaw.toLowerCase();
this.listID = listID;
this.shareWithEmail = shareWithEmail;
permissionLevel = 1;
this.other = other;
for (Map.Entry<Integer, String> keytoPermEntry: keysToPerms.entrySet()) {
if (permissions.contains(keytoPermEntry.getValue())) {
permissionLevel *= keytoPermEntry.getKey();
}
}
}
@Override
public String toString() {
StringBuilder toReturn = new StringBuilder("ListShare{" +
"listID=" + listID +
", shareWithEmail='" + shareWithEmail + '\'' +
", permissionLevel=" + permissionLevel +
" [Permissions: ");
int permissionLevelCopy = permissionLevel;
for (Integer permissionObject : keysToPerms.keySet()) {
Integer permissionInteger = permissionObject;
if (permissionLevelCopy % permissionInteger == 0) {
permissionLevelCopy /= permissionInteger;
toReturn.append(keysToPerms.get(permissionInteger)).append(",");
}
}
if (BuildConfig.DEBUG && permissionLevelCopy != 1) {
throw new AssertionError("Assertion failed");
}
toReturn.append("]}");
return toReturn.toString();
} }
public Integer getListID() { public Integer getListID() {
@ -36,4 +87,12 @@ public class ListShare {
public ListShare[] getEntries() { public ListShare[] getEntries() {
return other; return other;
} }
public Integer getPermissionLevel() {
return permissionLevel;
}
public void setPermissionLevel(Integer permissionLevel) {
this.permissionLevel = permissionLevel;
}
} }

View File

@ -0,0 +1,30 @@
package com.example.listify.data;
import java.util.ArrayList;
public class SearchHistory {
ArrayList<String> searches;
public SearchHistory(ArrayList<String> searches) {
this.searches = searches;
}
public ArrayList<String> getSearches() {
return searches;
}
public void setSearches(ArrayList<String> searches) {
this.searches = searches;
}
public void addSearch(String newSearch) {
searches.add(newSearch);
}
@Override
public String toString() {
return "SearchHistory{" +
"searches=" + searches +
'}';
}
}

View File

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/>
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#000000">
<path
android:fillColor="@android:color/white"
android:pathData="M16,1L4,1c-1.1,0 -2,0.9 -2,2v14h2L4,3h12L16,1zM15,5l6,6v10c0,1.1 -0.9,2 -2,2L7.99,23C6.89,23 6,22.1 6,21l0.01,-14c0,-1.1 0.89,-2 1.99,-2h7zM14,12h5.5L14,6.5L14,12z"/>
</vector>

View File

@ -1,9 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="28dp" android:width="24dp"
android:height="28dp" android:height="24dp"
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24" android:viewportHeight="24"
android:tint="?attr/colorControlNormal"> android:tint="#000000">
<path <path
android:fillColor="@android:color/white" android:fillColor="@android:color/white"
android:pathData="M10,18h4v-2h-4v2zM3,6v2h18L21,6L3,6zM6,13h12v-2L6,11v2z"/> android:pathData="M10,18h4v-2h-4v2zM3,6v2h18L21,6L3,6zM6,13h12v-2L6,11v2z"/>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#000000">
<path
android:fillColor="@android:color/white"
android:pathData="M9,3L5,6.99h3L8,14h2L10,6.99h3L9,3zM16,17.01L16,10h-2v7.01h-3L15,21l4,-3.99h-3z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M12,8c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,16c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z"/>
</vector>

View File

@ -1,4 +1,4 @@
<vector android:height="24dp" android:tint="#FFFFFF" <vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24" android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z"/> <path android:fillColor="@android:color/white" android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z"/>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#000000">
<path
android:fillColor="@android:color/white"
android:pathData="M3,18h6v-2L3,16v2zM3,6v2h18L21,6L3,6zM3,13h12v-2L3,11v2z"/>
</vector>

View File

@ -1,5 +0,0 @@
<vector android:height="28dp" android:tint="?attr/colorControlNormal"
android:viewportHeight="24" android:viewportWidth="24"
android:width="28dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M3,18h6v-2L3,16v2zM3,6v2h18L21,6L3,6zM3,13h12v-2L3,11v2z"/>
</vector>

View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_more"
android:icon="@drawable/ic_baseline_more_vert_24"
android:title=""
app:showAsAction="always">
<menu>
<item
android:id="@+id/action_rename_list"
android:icon="@drawable/ic_baseline_edit_24"
android:title="Rename List"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_share_list"
android:icon="@drawable/ic_baseline_share_24"
android:title="Share List"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_duplicate_list"
android:icon="@drawable/ic_baseline_file_copy_24"
android:title="Duplicate"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_export_list"
android:icon="@drawable/ic_baseline_import_export_24"
android:title="Export"
app:showAsAction="ifRoom" />
</menu>
</item>
</menu>

View File

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_create_list"
android:orderInCategory="101"
android:title="Create List"
app:showAsAction="never" />
</menu>

View File

@ -1,14 +1,23 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" <menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_more"
android:icon="@drawable/ic_baseline_more_vert_24"
android:title=""
app:showAsAction="always">
<menu>
<item <item
android:id="@+id/action_sort" android:id="@+id/action_sort"
android:orderInCategory="101" android:icon="@drawable/ic_baseline_sort_24"
android:title="Sort" android:title="Sort"
app:showAsAction="never" /> app:showAsAction="never" />
<item <item
android:id="@+id/action_filter" android:id="@+id/action_filter"
android:orderInCategory="101" android:icon="@drawable/ic_baseline_filter_list_24"
android:title="Filter" android:title="Filter"
app:showAsAction="never" /> app:showAsAction="never" />
</menu>
</item>
</menu> </menu>