From a631aa803bd3a243f9d6fde8e7cb79028cdf3392 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Tue, 6 Oct 2015 23:06:21 -0700 Subject: [PATCH] GUAC-1193: Implement JDBC ConnectionRecordSet. Add MySQL mapping. --- .../connection/ConnectionRecordMapper.java | 22 ++ .../ConnectionRecordSearchTerm.java | 296 ++++++++++++++++++ .../jdbc/connection/ConnectionRecordSet.java | 123 ++++++++ .../ConnectionRecordSortPredicate.java | 82 +++++ .../guacamole/auth/jdbc/user/UserContext.java | 12 +- .../connection/ConnectionRecordMapper.xml | 58 ++++ 6 files changed, 591 insertions(+), 2 deletions(-) create mode 100644 extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/connection/ConnectionRecordSearchTerm.java create mode 100644 extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/connection/ConnectionRecordSet.java create mode 100644 extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/connection/ConnectionRecordSortPredicate.java diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/connection/ConnectionRecordMapper.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/connection/ConnectionRecordMapper.java index 8816fb69c..eaca812e5 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/connection/ConnectionRecordMapper.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/connection/ConnectionRecordMapper.java @@ -23,6 +23,7 @@ package org.glyptodon.guacamole.auth.jdbc.connection; import java.util.List; +import java.util.Set; import org.apache.ibatis.annotations.Param; /** @@ -56,5 +57,26 @@ public interface ConnectionRecordMapper { * The number of rows inserted. */ int insert(@Param("record") ConnectionRecordModel record); + + /** + * Searches for up to limit connection records that contain + * the given terms, sorted by the given predicates. + * + * @param terms + * The search terms that must match the returned records. + * + * @param sortPredicates + * A list of predicates to sort the returned records by, in order of + * priority. + * + * @param limit + * The maximum number of records that should be returned. + * + * @return + * The results of the search performed with the given parameters. + */ + List search(@Param("terms") Set terms, + @Param("sortPredicates") List sortPredicates, + @Param("limit") int limit); } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/connection/ConnectionRecordSearchTerm.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/connection/ConnectionRecordSearchTerm.java new file mode 100644 index 000000000..e2e62b32e --- /dev/null +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/connection/ConnectionRecordSearchTerm.java @@ -0,0 +1,296 @@ +/* + * Copyright (C) 2015 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.glyptodon.guacamole.auth.jdbc.connection; + +import java.util.Calendar; +import java.util.Date; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A search term for querying historical connection records. This will contain + * a the search term in string form and, if that string appears to be a date. a + * corresponding date range. + * + * @author James Muehlner + */ +public class ConnectionRecordSearchTerm { + + /** + * A pattern that can match a year, year and month, or year and month and + * day. + */ + private static final Pattern DATE_PATTERN = + Pattern.compile("(\\d+)(?:-(\\d+)?(?:-(\\d+)?)?)?"); + + /** + * The index of the group within DATE_PATTERN containing the + * year number. + */ + private static final int YEAR_GROUP = 1; + + /** + * The index of the group within DATE_PATTERN containing the + * month number, if any. + */ + private static final int MONTH_GROUP = 2; + + /** + * The index of the group within DATE_PATTERN containing the + * day number, if any. + */ + private static final int DAY_GROUP = 3; + + /** + * The start of the date range for records that should be retrieved, if the + * provided search term appears to be a date. + */ + private final Date startDate; + + /** + * The end of the date range for records that should be retrieved, if the + * provided search term appears to be a date. + */ + private final Date endDate; + + /** + * The string that should be searched for. + */ + private final String term; + + /** + * Parse the given string as an integer, returning the provided default + * value if the string is null. + * + * @param str + * The string to parse as an integer. + * + * @param defaultValue + * The value to return if str is null. + * + * @return + * The parsed value, or the provided default value if str + * is null. + */ + private static int parseInt(String str, int defaultValue) { + + if (str == null) + return defaultValue; + + return Integer.parseInt(str); + + } + + /** + * Returns a new calendar representing the last millisecond of the same + * year as calendar. + * + * @param calendar + * The calendar defining the year whose end (last millisecond) is to be + * returned. + * + * @return + * A new calendar representing the last millisecond of the same year as + * calendar. + */ + private static Calendar getEndOfYear(Calendar calendar) { + + // Get first day of next year + Calendar endOfYear = Calendar.getInstance(); + endOfYear.clear(); + endOfYear.set(Calendar.YEAR, calendar.get(Calendar.YEAR) + 1); + + // Transform into the last millisecond of the given year + endOfYear.add(Calendar.MILLISECOND, -1); + + return endOfYear; + + } + + /** + * Returns a new calendar representing the last millisecond of the same + * month and year as calendar. + * + * @param calendar + * The calendar defining the month and year whose end (last millisecond) + * is to be returned. + * + * @return + * A new calendar representing the last millisecond of the same month + * and year as calendar. + */ + private static Calendar getEndOfMonth(Calendar calendar) { + + // Copy given calender only up to given month + Calendar endOfMonth = Calendar.getInstance(); + endOfMonth.clear(); + endOfMonth.set(Calendar.YEAR, calendar.get(Calendar.YEAR)); + endOfMonth.set(Calendar.MONTH, calendar.get(Calendar.MONTH)); + + // Advance to the last millisecond of the given month + endOfMonth.add(Calendar.MONTH, 1); + endOfMonth.add(Calendar.MILLISECOND, -1); + + return endOfMonth; + + } + + /** + * Returns a new calendar representing the last millisecond of the same + * year, month, and day as calendar. + * + * @param calendar + * The calendar defining the year, month, and day whose end + * (last millisecond) is to be returned. + * + * @return + * A new calendar representing the last millisecond of the same year, + * month, and day as calendar. + */ + private static Calendar getEndOfDay(Calendar calendar) { + + // Copy given calender only up to given month + Calendar endOfMonth = Calendar.getInstance(); + endOfMonth.clear(); + endOfMonth.set(Calendar.YEAR, calendar.get(Calendar.YEAR)); + endOfMonth.set(Calendar.MONTH, calendar.get(Calendar.MONTH)); + endOfMonth.set(Calendar.DAY_OF_MONTH, calendar.get(Calendar.DAY_OF_MONTH)); + + // Advance to the last millisecond of the given day + endOfMonth.add(Calendar.DAY_OF_MONTH, 1); + endOfMonth.add(Calendar.MILLISECOND, -1); + + return endOfMonth; + + } + + /** + * Creates a new ConnectionRecordSearchTerm representing the given string. + * If the given string appears to be a date, the start and end dates of the + * implied date range will be automatically determined and made available + * via getStartDate() and getEndDate() respectively. + * + * @param term + * The string that should be searched for. + */ + public ConnectionRecordSearchTerm(String term) { + + // Search terms absolutely must not be null + if (term == null) + throw new NullPointerException("Search terms may not be null"); + + this.term = term; + + // Parse start/end of date range if term appears to be a date + Matcher matcher = DATE_PATTERN.matcher(term); + if (matcher.matches()) { + + // Retrieve date components from term + String year = matcher.group(YEAR_GROUP); + String month = matcher.group(MONTH_GROUP); + String day = matcher.group(DAY_GROUP); + + // Parse start date from term + Calendar startCalendar = Calendar.getInstance(); + startCalendar.clear(); + startCalendar.set( + Integer.parseInt(year), + parseInt(month, 0), + parseInt(day, 1) + ); + + Calendar endCalendar; + + // Derive end date from start date + if (month == null) { + endCalendar = getEndOfYear(startCalendar); + } + else if (day == null) { + endCalendar = getEndOfMonth(startCalendar); + } + else { + endCalendar = getEndOfDay(startCalendar); + } + + // Convert results back into dates + this.startDate = startCalendar.getTime(); + this.endDate = endCalendar.getTime(); + + } + + // The search term doesn't look like a date + else { + this.startDate = null; + this.endDate = null; + } + + } + + /** + * Returns the start of the date range for records that should be retrieved, + * if the provided search term appears to be a date. + * + * @return + * The start of the date range. + */ + public Date getStartDate() { + return startDate; + } + + /** + * Returns the end of the date range for records that should be retrieved, + * if the provided search term appears to be a date. + * + * @return + * The end of the date range. + */ + public Date getEndDate() { + return endDate; + } + + /** + * Returns the string that should be searched for. + * + * @return + * The search term. + */ + public String getTerm() { + return term; + } + + @Override + public int hashCode() { + return term.hashCode(); + } + + @Override + public boolean equals(Object obj) { + + if (obj == null || !(obj instanceof ConnectionRecordSearchTerm)) + return false; + + return ((ConnectionRecordSearchTerm) obj).getTerm().equals(getTerm()); + + } + +} diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/connection/ConnectionRecordSet.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/connection/ConnectionRecordSet.java new file mode 100644 index 000000000..355f59fad --- /dev/null +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/connection/ConnectionRecordSet.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2015 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.glyptodon.guacamole.auth.jdbc.connection; + +import com.google.inject.Inject; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.glyptodon.guacamole.GuacamoleException; +import org.glyptodon.guacamole.auth.jdbc.base.RestrictedObject; +import org.glyptodon.guacamole.net.auth.ConnectionRecord; + +/** + * A JDBC implementation of ConnectionRecordSet. Calls to asCollection() will + * query connection history records from the database. Which records are + * returned will be determined by the values passed in earlier. + * + * @author James Muehlner + */ +public class ConnectionRecordSet extends RestrictedObject + implements org.glyptodon.guacamole.net.auth.ConnectionRecordSet { + + /** + * Mapper for accessing connection history. + */ + @Inject + private ConnectionRecordMapper connectionRecordMapper; + + /** + * The set of strings that each must occur somewhere within the returned + * connection records, whether within the associated username, the name of + * the associated connection, or any associated date. If non-empty, any + * connection record not matching each of the strings within the collection + * will be excluded from the results. + */ + private final Set requiredContents = + new HashSet(); + + /** + * The maximum number of connection history records that should be returned + * by a call to asCollection(). + */ + private int limit = Integer.MAX_VALUE; + + /** + * A list of predicates to apply while sorting the resulting connection + * records, describing the properties involved and the sort order for those + * properties. + */ + private final List connectionRecordSortPredicates = + new ArrayList(); + + @Override + public Collection asCollection() + throws GuacamoleException { + + // Perform the search against the database + List searchResults = + connectionRecordMapper.search(requiredContents, + connectionRecordSortPredicates, limit); + + List modeledSearchResults = + new ArrayList(); + + // Convert raw DB records into ConnectionRecords + for(ConnectionRecordModel model : searchResults) { + modeledSearchResults.add(new ModeledConnectionRecord(model)); + } + + return modeledSearchResults; + + } + + @Override + public ConnectionRecordSet contains(String value) + throws GuacamoleException { + requiredContents.add(new ConnectionRecordSearchTerm(value)); + return this; + } + + @Override + public ConnectionRecordSet limit(int limit) throws GuacamoleException { + this.limit = Math.min(this.limit, limit); + return this; + } + + @Override + public ConnectionRecordSet sort(SortableProperty property, boolean desc) + throws GuacamoleException { + + connectionRecordSortPredicates.add(new ConnectionRecordSortPredicate( + property, + desc + )); + + return this; + + } + +} diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/connection/ConnectionRecordSortPredicate.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/connection/ConnectionRecordSortPredicate.java new file mode 100644 index 000000000..279321b44 --- /dev/null +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/connection/ConnectionRecordSortPredicate.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2015 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.glyptodon.guacamole.auth.jdbc.connection; + +import org.glyptodon.guacamole.net.auth.ConnectionRecordSet; + +/** + * A sort predicate which species the property to use when sorting connection + * records, along with the sort order. + * + * @author James Muehlner + */ +public class ConnectionRecordSortPredicate { + + /** + * The property to use when sorting ConnectionRecords. + */ + private final ConnectionRecordSet.SortableProperty property; + + /** + * Whether the sort order is descending (true) or ascending (false). + */ + private final boolean descending; + + /** + * Creates a new ConnectionRecordSortPredicate with the given sort property + * and sort order. + * + * @param property + * The property to use when sorting ConnectionRecords. + * + * @param descending + * Whether the sort order is descending (true) or ascending (false). + */ + public ConnectionRecordSortPredicate(ConnectionRecordSet.SortableProperty property, + boolean descending) { + this.property = property; + this.descending = descending; + } + + /** + * Returns the property that should be used when sorting ConnectionRecords. + * + * @return + * The property that should be used when sorting ConnectionRecords. + */ + public ConnectionRecordSet.SortableProperty getProperty() { + return property; + } + + /** + * Returns whether the sort order is descending. + * + * @return + * true if the sort order is descending, false if the sort order is + * ascending. + */ + public boolean isDescending() { + return descending; + } + +} diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/user/UserContext.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/user/UserContext.java index 06bd5256e..72dea43f4 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/user/UserContext.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/user/UserContext.java @@ -32,6 +32,7 @@ import java.util.Collection; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.auth.jdbc.base.RestrictedObject; import org.glyptodon.guacamole.auth.jdbc.activeconnection.ActiveConnectionDirectory; +import org.glyptodon.guacamole.auth.jdbc.connection.ConnectionRecordSet; import org.glyptodon.guacamole.auth.jdbc.connection.ModeledConnection; import org.glyptodon.guacamole.auth.jdbc.connectiongroup.ModeledConnectionGroup; import org.glyptodon.guacamole.form.Form; @@ -39,7 +40,6 @@ import org.glyptodon.guacamole.net.auth.ActiveConnection; import org.glyptodon.guacamole.net.auth.AuthenticationProvider; import org.glyptodon.guacamole.net.auth.Connection; import org.glyptodon.guacamole.net.auth.ConnectionGroup; -import org.glyptodon.guacamole.net.auth.ConnectionRecordSet; import org.glyptodon.guacamole.net.auth.Directory; import org.glyptodon.guacamole.net.auth.User; import org.glyptodon.guacamole.net.auth.simple.SimpleConnectionRecordSet; @@ -94,6 +94,12 @@ public class UserContext extends RestrictedObject @Inject private Provider rootGroupProvider; + /** + * Provider for creating connection record sets. + */ + @Inject + private Provider connectionRecordSetProvider; + @Override public void init(AuthenticatedUser currentUser) { @@ -141,7 +147,9 @@ public class UserContext extends RestrictedObject @Override public ConnectionRecordSet getConnectionHistory() throws GuacamoleException { - return new SimpleConnectionRecordSet(); + ConnectionRecordSet connectionRecordSet = connectionRecordSetProvider.get(); + connectionRecordSet.init(getCurrentUser()); + return connectionRecordSet; } @Override diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/glyptodon/guacamole/auth/jdbc/connection/ConnectionRecordMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/glyptodon/guacamole/auth/jdbc/connection/ConnectionRecordMapper.xml index b5775f607..fb45aeb2d 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/glyptodon/guacamole/auth/jdbc/connection/ConnectionRecordMapper.xml +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/glyptodon/guacamole/auth/jdbc/connection/ConnectionRecordMapper.xml @@ -72,4 +72,62 @@ + + + \ No newline at end of file