From 257d342c054fcd2f592e90c4b519c62a98d3ed2c Mon Sep 17 00:00:00 2001 From: Alessandro Ricchiuti <1532479+axl8713@users.noreply.github.com> Date: Thu, 6 Feb 2025 11:07:55 +0100 Subject: [PATCH] Support for user favorites. (#402) * Implemented CRUD operation for tags. * Implemented tag association to the resource. * Added test for tag management. * Added log messages. * Added DB creation scripts for tag tables. * Added javadoc to tag REST service. * Refactoring. * Added count in get all tags operation result. * Added filtering by tag for the extJS get all operation. * Exposed get all tags operation to anonymous users. * Refactored nameLike path variable handling. * Introduced AssociatedEntityFilter abstraction. * Set distinct in resource count query. * Added null check to nameLike path variable handling. * Refactored nameLike query variable handling. * Added DB migration scripts. * Handled on delete cascade actions on tags. * Implementation for user favorites functionality. * Added favorite only resource search to ExtJS resource list operation. * Renamed deprecated functions in ResourceService. * Added DB migration scripts. * Added favorite information to extJS resource get operations. * Fixed hash calculation for StoredData to avoid stack overflow. * Fixed bug that was removing tag association after updates. * Signalled failing assertion in rest client test. * Fixed tag list serialization when null. * Handled names in filters that contain commas. * Fixes after merging master. --- doc/sql/002_create_schema_oracle.sql | 17 ++ doc/sql/002_create_schema_postgres.sql | 19 +- .../h2-migration-from-v.2.1.0-to-v2.3.0.sql | 23 +- ...racle-migration-from-v.2.1.0-to-v2.3.0.sql | 22 +- ...resql-migration-from-v.2.1.0-to-v2.3.0.sql | 22 +- .../geostore/core/model/Resource.java | 12 + .../geostore/core/model/User.java | 45 ++-- .../geostore/core/dao/ResourceDAO.java | 18 +- .../core/dao/impl/ResourceDAOImpl.java | 13 + .../geostore/services/FavoriteService.java | 38 +++ .../geostore/services/ResourceService.java | 24 +- .../geostore/services/UserService.java | 17 +- .../dto/ResourceSearchParameters.java | 14 ++ .../services/FavoriteServiceImpl.java | 79 ++++++ .../services/ResourceServiceImpl.java | 42 ++-- .../geostore/services/UserServiceImpl.java | 27 +- .../src/main/resources/applicationContext.xml | 18 +- .../services/FavoriteServiceImplTest.java | 89 +++++++ .../geostore/services/ServiceTestBase.java | 3 + .../services/rest/RESTFavoriteService.java | 72 ++++++ .../geostore/services/model/ExtResource.java | 23 +- .../services/model/ExtShortResource.java | 20 +- .../services/rest/RESTExtJsService.java | 2 + .../rest/impl/RESTExtJsServiceImpl.java | 60 +++-- .../rest/impl/RESTExtJsServiceImplTest.java | 235 ++++++++++++++++-- .../services/rest/impl/ServiceTestBase.java | 23 +- .../rest/impl/RESTFavoriteServiceImpl.java | 60 +++++ .../src/main/resources/applicationContext.xml | 7 + 28 files changed, 908 insertions(+), 136 deletions(-) create mode 100644 src/core/services-api/src/main/java/it/geosolutions/geostore/services/FavoriteService.java create mode 100644 src/core/services-impl/src/main/java/it/geosolutions/geostore/services/FavoriteServiceImpl.java create mode 100644 src/core/services-impl/src/test/java/it/geosolutions/geostore/services/FavoriteServiceImplTest.java create mode 100644 src/modules/rest/api/src/main/java/it/geosolutions/geostore/services/rest/RESTFavoriteService.java create mode 100644 src/modules/rest/impl/src/main/java/it/geosolutions/geostore/services/rest/impl/RESTFavoriteServiceImpl.java diff --git a/doc/sql/002_create_schema_oracle.sql b/doc/sql/002_create_schema_oracle.sql index e6a297ef..28930ba8 100644 --- a/doc/sql/002_create_schema_oracle.sql +++ b/doc/sql/002_create_schema_oracle.sql @@ -133,6 +133,23 @@ foreign key (tag_id) references gs_tag(id); + create table gs_user_favorites ( + user_id number(19,0) not null, + resource_id number(19,0) not null, + primary key (user_id, resource_id) + ); + + alter table gs_user_favorites + add constraint fk_user_favorites_resource + foreign key (resource_id) + references gs_resource(id) + on delete cascade; + + alter table gs_user_favorites + add constraint fk_user_favorites_user + foreign key (user_id) + references gs_user(id); + create index idx_attribute_name on gs_attribute (name); create index idx_attribute_resource on gs_attribute (resource_id); diff --git a/doc/sql/002_create_schema_postgres.sql b/doc/sql/002_create_schema_postgres.sql index 46829095..c515a187 100644 --- a/doc/sql/002_create_schema_postgres.sql +++ b/doc/sql/002_create_schema_postgres.sql @@ -149,7 +149,24 @@ SET search_path TO geostore; add constraint fk_resource_tags_tag foreign key (tag_id) references gs_tag(id); - + + create table gs_user_favorites ( + user_id int8 not null, + resource_id int8 not null, + constraint gs_user_favorites_pkey primary key (user_id, resource_id) + ); + + alter table gs_user_favorites + add constraint fk_user_favorites_resource + foreign key (resource_id) + references gs_resource(id) + on delete cascade; + + alter table gs_user_favorites + add constraint fk_user_favorites_user + foreign key (user_id) + references gs_user(id); + create index idx_attribute_name on gs_attribute (name); create index idx_attribute_resource on gs_attribute (resource_id); diff --git a/doc/sql/migration/h2/h2-migration-from-v.2.1.0-to-v2.3.0.sql b/doc/sql/migration/h2/h2-migration-from-v.2.1.0-to-v2.3.0.sql index 2d9ceec5..3a38c6ee 100644 --- a/doc/sql/migration/h2/h2-migration-from-v.2.1.0-to-v2.3.0.sql +++ b/doc/sql/migration/h2/h2-migration-from-v.2.1.0-to-v2.3.0.sql @@ -1,3 +1,4 @@ +-- Tags CREATE TABLE gs_tag ( id BIGINT NOT NULL, color VARCHAR(255) NOT NULL, @@ -12,14 +13,26 @@ CREATE TABLE gs_resource_tags ( CONSTRAINT gs_resource_tags_pkey PRIMARY KEY (tag_id, resource_id) ); --- Add foreign key constraints to gs_resource_tags ALTER TABLE gs_resource_tags ADD CONSTRAINT fk_resource_tags_resource FOREIGN KEY (resource_id) REFERENCES gs_resource(id) ON DELETE CASCADE; -ALTER TABLE gs_resource_tags - ADD CONSTRAINT fk_resource_tags_tag - FOREIGN KEY (tag_id) - REFERENCES gs_tag(id); \ No newline at end of file +-- Favorites +CREATE TABLE gs_user_favorites ( + user_id BIGINT NOT NULL, + resource_id BIGINT NOT NULL, + CONSTRAINT gs_user_favorites_pkey PRIMARY KEY (user_id, resource_id) +); + +ALTER TABLE gs_user_favorites + ADD CONSTRAINT fk_user_favorites_resource + FOREIGN KEY (resource_id) + REFERENCES gs_resource(id) + ON DELETE CASCADE; + +ALTER TABLE gs_user_favorites + ADD CONSTRAINT fk_user_favorites_user + FOREIGN KEY (user_id) + REFERENCES gs_user(id); \ No newline at end of file diff --git a/doc/sql/migration/oracle/oracle-migration-from-v.2.1.0-to-v2.3.0.sql b/doc/sql/migration/oracle/oracle-migration-from-v.2.1.0-to-v2.3.0.sql index ff4601a4..66065f27 100644 --- a/doc/sql/migration/oracle/oracle-migration-from-v.2.1.0-to-v2.3.0.sql +++ b/doc/sql/migration/oracle/oracle-migration-from-v.2.1.0-to-v2.3.0.sql @@ -1,3 +1,4 @@ +-- Tags create table gs_tag ( id number(19,0) not null, color varchar2(255 char) not null, @@ -12,7 +13,6 @@ create table gs_resource_tags ( primary key (tag_id, resource_id) ); --- Add foreign key constraints to gs_resource_tags alter table gs_resource_tags add constraint fk_resource_tags_resource foreign key (resource_id) @@ -22,4 +22,22 @@ alter table gs_resource_tags alter table gs_resource_tags add constraint fk_resource_tags_tag foreign key (tag_id) - references gs_tag(id); \ No newline at end of file + references gs_tag(id); + +-- Favorites +create table gs_user_favorites ( + user_id number(19,0) not null, + resource_id number(19,0) not null, + primary key (user_id, resource_id) +); + +alter table gs_user_favorites + add constraint fk_user_favorites_resource + foreign key (resource_id) + references gs_resource(id) + on delete cascade; + +alter table gs_user_favorites + add constraint fk_user_favorites_user + foreign key (user_id) + references gs_user(id); \ No newline at end of file diff --git a/doc/sql/migration/postgresql/postgresql-migration-from-v.2.1.0-to-v2.3.0.sql b/doc/sql/migration/postgresql/postgresql-migration-from-v.2.1.0-to-v2.3.0.sql index 955ee088..04e719e3 100644 --- a/doc/sql/migration/postgresql/postgresql-migration-from-v.2.1.0-to-v2.3.0.sql +++ b/doc/sql/migration/postgresql/postgresql-migration-from-v.2.1.0-to-v2.3.0.sql @@ -1,3 +1,4 @@ +-- Tags create table gs_tag ( id int8 not null, color varchar(255) not null, @@ -12,7 +13,6 @@ create table gs_resource_tags ( constraint gs_resource_tags_pkey primary key (tag_id, resource_id) ); --- Add foreign key constraints to gs_resource_tags alter table gs_resource_tags add constraint fk_resource_tags_resource foreign key (resource_id) @@ -22,4 +22,22 @@ alter table gs_resource_tags alter table gs_resource_tags add constraint fk_resource_tags_tag foreign key (tag_id) - references gs_tag(id); \ No newline at end of file + references gs_tag(id); + +-- Favorites +create table gs_user_favorites ( + user_id int8 not null, + resource_id int8 not null, + constraint gs_user_favorites_pkey primary key (user_id, resource_id) +); + +alter table gs_user_favorites + add constraint fk_user_favorites_resource + foreign key (resource_id) + references gs_resource(id) + on delete cascade; + +alter table gs_user_favorites + add constraint fk_user_favorites_user + foreign key (user_id) + references gs_user(id); \ No newline at end of file diff --git a/src/core/model/src/main/java/it/geosolutions/geostore/core/model/Resource.java b/src/core/model/src/main/java/it/geosolutions/geostore/core/model/Resource.java index 40bd41c2..99f5df81 100644 --- a/src/core/model/src/main/java/it/geosolutions/geostore/core/model/Resource.java +++ b/src/core/model/src/main/java/it/geosolutions/geostore/core/model/Resource.java @@ -127,6 +127,10 @@ public class Resource implements Serializable, CycleRecoverable { @OnDelete(action = OnDeleteAction.CASCADE) private Set tags; + @ManyToMany(mappedBy = "favorites", fetch = FetchType.LAZY) + @OnDelete(action = OnDeleteAction.CASCADE) + private Set favoritedBy; + /** @return the id */ public Long getId() { return id; @@ -268,6 +272,14 @@ public void setTags(Set tags) { this.tags = tags; } + public Set getFavoritedBy() { + return favoritedBy; + } + + public void setFavoritedBy(Set favoritedBy) { + this.favoritedBy = favoritedBy; + } + /* * (non-Javadoc) @see java.lang.Object#toString() */ diff --git a/src/core/model/src/main/java/it/geosolutions/geostore/core/model/User.java b/src/core/model/src/main/java/it/geosolutions/geostore/core/model/User.java index 80fa0e10..bbce0df3 100644 --- a/src/core/model/src/main/java/it/geosolutions/geostore/core/model/User.java +++ b/src/core/model/src/main/java/it/geosolutions/geostore/core/model/User.java @@ -74,10 +74,8 @@ @XmlRootElement(name = "User") public class User implements Serializable { - /** The Constant serialVersionUID. */ private static final long serialVersionUID = -138056245004697133L; - /** The id. */ @Id @GeneratedValue private Long id; @Column(nullable = false, updatable = false, length = 255) @@ -90,20 +88,12 @@ public class User implements Serializable { @Enumerated(EnumType.STRING) private Role role; - public User() {}; - - public User(User user) { - this.id = user.id; - this.name = user.name; - this.password = user.password; - this.role = user.role; - this.newPassword = user.newPassword; - this.trusted = user.trusted; - this.attribute = user.attribute; - this.security = user.security; - this.groups = user.groups; - this.enabled = user.enabled; - } + @ManyToMany + @JoinTable( + name = "gs_user_favorites", + joinColumns = @JoinColumn(name = "user_id"), + inverseJoinColumns = @JoinColumn(name = "resource_id")) + private Set favorites; /* * NOT to be saved on DB @@ -134,6 +124,21 @@ public User(User user) { @Column(nullable = false, updatable = true) private boolean enabled = true; + public User() {}; + + public User(User user) { + this.id = user.id; + this.name = user.name; + this.password = user.password; + this.role = user.role; + this.newPassword = user.newPassword; + this.trusted = user.trusted; + this.attribute = user.attribute; + this.security = user.security; + this.groups = user.groups; + this.enabled = user.enabled; + } + /** @return the id */ // @XmlTransient public Long getId() { @@ -275,6 +280,14 @@ public void setTrusted(boolean trusted) { this.trusted = trusted; } + public Set getFavorites() { + return favorites; + } + + public void setFavorites(Set favorites) { + this.favorites = favorites; + } + /* * (non-Javadoc) @see java.lang.Object#toString() */ diff --git a/src/core/persistence/src/main/java/it/geosolutions/geostore/core/dao/ResourceDAO.java b/src/core/persistence/src/main/java/it/geosolutions/geostore/core/dao/ResourceDAO.java index 99896638..101a6e67 100644 --- a/src/core/persistence/src/main/java/it/geosolutions/geostore/core/dao/ResourceDAO.java +++ b/src/core/persistence/src/main/java/it/geosolutions/geostore/core/dao/ResourceDAO.java @@ -44,13 +44,13 @@ public interface ResourceDAO extends RestrictedGenericDAO { * @param resourceId * @return List */ - public List findAttributes(long resourceId); + List findAttributes(long resourceId); /** @param search */ - public void removeResources(ISearch search); + void removeResources(ISearch search); - /** @param resourcesIDs A list of resources Ids to search */ - public List findResources(List resourcesIds); + /** @param resourcesIds A list of resources Ids to search */ + List findResources(List resourcesIds); /** * Gets a resource by name. @@ -58,7 +58,7 @@ public interface ResourceDAO extends RestrictedGenericDAO { * @return the resource with the specified name, or null if none was found * @throws NonUniqueResultException if more than one result */ - public Resource findByName(String resourceName); + Resource findByName(String resourceName); /** * Returns a list of resource names matching the specified pattern @@ -67,4 +67,12 @@ public interface ResourceDAO extends RestrictedGenericDAO { * @return a list of resource names */ public List findResourceNamesMatchingPattern(String pattern); + + /** + * Returns a list of resources that are the favorites of the user + * + * @param userId user identifier + * @return a list of resources favorites by the user + */ + List findUserFavorites(Long userId); } diff --git a/src/core/persistence/src/main/java/it/geosolutions/geostore/core/dao/impl/ResourceDAOImpl.java b/src/core/persistence/src/main/java/it/geosolutions/geostore/core/dao/impl/ResourceDAOImpl.java index 85c3f2a4..36a45639 100644 --- a/src/core/persistence/src/main/java/it/geosolutions/geostore/core/dao/impl/ResourceDAOImpl.java +++ b/src/core/persistence/src/main/java/it/geosolutions/geostore/core/dao/impl/ResourceDAOImpl.java @@ -215,4 +215,17 @@ public List findResourceNamesMatchingPattern(String pattern) { return resourceNames; } + + /** + * @param userId + * @return List the user's favorite resources + */ + @Override + public List findUserFavorites(Long userId) { + Search searchCriteria = new Search(Resource.class); + + searchCriteria.addFilter(Filter.some("favoritedBy", Filter.equal("id", userId))); + + return super.search(searchCriteria); + } } diff --git a/src/core/services-api/src/main/java/it/geosolutions/geostore/services/FavoriteService.java b/src/core/services-api/src/main/java/it/geosolutions/geostore/services/FavoriteService.java new file mode 100644 index 00000000..d6822fcd --- /dev/null +++ b/src/core/services-api/src/main/java/it/geosolutions/geostore/services/FavoriteService.java @@ -0,0 +1,38 @@ +/* + * ==================================================================== + * + * Copyright (C) 2025 GeoSolutions S.A.S. + * http://www.geo-solutions.it + * + * GPLv3 + Classpath exception + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. + * + * ==================================================================== + * + * This software consists of voluntary contributions made by developers + * of GeoSolutions. For more information on GeoSolutions, please see + * . + * + */ +package it.geosolutions.geostore.services; + +import it.geosolutions.geostore.services.exception.NotFoundServiceEx; + +public interface FavoriteService { + + void addFavorite(long userId, long resourceId) throws NotFoundServiceEx; + + void removeFavorite(long userId, long resourceId) throws NotFoundServiceEx; +} diff --git a/src/core/services-api/src/main/java/it/geosolutions/geostore/services/ResourceService.java b/src/core/services-api/src/main/java/it/geosolutions/geostore/services/ResourceService.java index 1a0662a5..32e4a8ca 100644 --- a/src/core/services-api/src/main/java/it/geosolutions/geostore/services/ResourceService.java +++ b/src/core/services-api/src/main/java/it/geosolutions/geostore/services/ResourceService.java @@ -218,30 +218,38 @@ void updateSecurityRules(long id, List rules) throws BadRequestServiceEx, InternalErrorServiceEx, NotFoundServiceEx; /** - * Get filter count by filter and user + * Count resources by filter and user * * @param filter * @param user * @return resources' count that the user has access * @throws InternalErrorServiceEx * @throws BadRequestServiceEx - * @deprecated rename into count() */ - @Deprecated - long getCountByFilterAndUser(SearchFilter filter, User user) + long count(SearchFilter filter, User user) throws BadRequestServiceEx, InternalErrorServiceEx; + + /** + * Count resources by filter and user, eventually limited to user's favorites + * + * @param filter + * @param user + * @param favoritesOnly + * @return resources' count that the user has access + * @throws InternalErrorServiceEx + * @throws BadRequestServiceEx + */ + long count(SearchFilter filter, User user, boolean favoritesOnly) throws BadRequestServiceEx, InternalErrorServiceEx; /** - * Get filter count by nameLike and user + * Count resources by nameLike and user * * @param nameLike * @param user * @return resources' count that the user has access * @throws BadRequestServiceEx - * @deprecated rename into count() */ - @Deprecated - long getCountByFilterAndUser(String nameLike, User user) throws BadRequestServiceEx; + long count(String nameLike, User user) throws BadRequestServiceEx; long insertAttribute(long id, String name, String value, DataType type) throws InternalErrorServiceEx; diff --git a/src/core/services-api/src/main/java/it/geosolutions/geostore/services/UserService.java b/src/core/services-api/src/main/java/it/geosolutions/geostore/services/UserService.java index b05caede..45f2eda0 100644 --- a/src/core/services-api/src/main/java/it/geosolutions/geostore/services/UserService.java +++ b/src/core/services-api/src/main/java/it/geosolutions/geostore/services/UserService.java @@ -76,7 +76,7 @@ public interface UserService { * @return User * @throws NotFoundServiceEx */ - public User get(String name) throws NotFoundServiceEx; + User get(String name) throws NotFoundServiceEx; /** * @param page @@ -117,7 +117,7 @@ List getAll(Integer page, Integer entries, String nameLike, boolean includ * * @return true if the persist operation finish with success, false otherwise */ - public boolean insertSpecialUsers(); + boolean insertSpecialUsers(); /** * Returns all user with the specified attribute (name / value). @@ -125,9 +125,9 @@ List getAll(Integer page, Integer entries, String nameLike, boolean includ * @param attribute * @return */ - public Collection getByAttribute(UserAttribute attribute); + Collection getByAttribute(UserAttribute attribute); - public Collection getByGroup(UserGroup group); + Collection getByGroup(UserGroup group); /** * Update the user entity by fetching its security rules and group security rules from the @@ -138,4 +138,13 @@ List getAll(Integer page, Integer entries, String nameLike, boolean includ default void fetchSecurityRules(User user) { /* no-op */ } + + /** + * Update the user entity by fetching its favorites resources from the database. + * + * @param user + */ + default void fetchFavorites(User user) { + /* no-op */ + } } diff --git a/src/core/services-api/src/main/java/it/geosolutions/geostore/services/dto/ResourceSearchParameters.java b/src/core/services-api/src/main/java/it/geosolutions/geostore/services/dto/ResourceSearchParameters.java index 142b5f49..36a45fda 100644 --- a/src/core/services-api/src/main/java/it/geosolutions/geostore/services/dto/ResourceSearchParameters.java +++ b/src/core/services-api/src/main/java/it/geosolutions/geostore/services/dto/ResourceSearchParameters.java @@ -13,6 +13,7 @@ public class ResourceSearchParameters { private final boolean includeAttributes; private final boolean includeData; private final boolean includeTags; + private final boolean favoritesOnly; private final User authUser; private ResourceSearchParameters( @@ -25,6 +26,7 @@ private ResourceSearchParameters( boolean includeAttributes, boolean includeData, boolean includeTags, + boolean favoritesOnly, User authUser) { this.filter = filter; this.page = page; @@ -35,6 +37,7 @@ private ResourceSearchParameters( this.includeAttributes = includeAttributes; this.includeData = includeData; this.includeTags = includeTags; + this.favoritesOnly = favoritesOnly; this.authUser = authUser; } @@ -74,6 +77,10 @@ public boolean isIncludeTags() { return includeTags; } + public boolean isFavoritesOnly() { + return favoritesOnly; + } + public User getAuthUser() { return authUser; } @@ -92,6 +99,7 @@ public static class Builder { private boolean includeAttributes; private boolean includeData; private boolean includeTags; + private boolean favoritesOnly; private User authUser; private Builder() {} @@ -141,6 +149,11 @@ public Builder includeTags(boolean includeTags) { return this; } + public Builder favoritesOnly(boolean favoritesOnly) { + this.favoritesOnly = favoritesOnly; + return this; + } + public Builder authUser(User authUser) { this.authUser = authUser; return this; @@ -157,6 +170,7 @@ public ResourceSearchParameters build() { includeAttributes, includeData, includeTags, + favoritesOnly, authUser); } } diff --git a/src/core/services-impl/src/main/java/it/geosolutions/geostore/services/FavoriteServiceImpl.java b/src/core/services-impl/src/main/java/it/geosolutions/geostore/services/FavoriteServiceImpl.java new file mode 100644 index 00000000..2ca7d2e2 --- /dev/null +++ b/src/core/services-impl/src/main/java/it/geosolutions/geostore/services/FavoriteServiceImpl.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2025 GeoSolutions S.A.S. + * http://www.geo-solutions.it + * + * GPLv3 + Classpath exception + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package it.geosolutions.geostore.services; + +import it.geosolutions.geostore.core.dao.ResourceDAO; +import it.geosolutions.geostore.core.dao.UserDAO; +import it.geosolutions.geostore.core.model.Resource; +import it.geosolutions.geostore.core.model.User; +import it.geosolutions.geostore.services.exception.NotFoundServiceEx; +import org.springframework.transaction.annotation.Transactional; + +public class FavoriteServiceImpl implements FavoriteService { + + private UserDAO userDAO; + private ResourceDAO resourceDAO; + + public void setUserDAO(UserDAO userDAO) { + this.userDAO = userDAO; + } + + public void setResourceDAO(ResourceDAO resourceDAO) { + this.resourceDAO = resourceDAO; + } + + @Override + @Transactional(value = "geostoreTransactionManager") + public void addFavorite(long userId, long resourceId) throws NotFoundServiceEx { + + User user = userDAO.find(userId); + if (user == null) { + throw new NotFoundServiceEx("User not found"); + } + + Resource resource = resourceDAO.find(resourceId); + if (resource == null) { + throw new NotFoundServiceEx("Resource not found"); + } + + user.getFavorites().add(resource); + + userDAO.persist(user); + } + + @Override + @Transactional(value = "geostoreTransactionManager") + public void removeFavorite(long userId, long resourceId) throws NotFoundServiceEx { + + User user = userDAO.find(userId); + if (user == null) { + throw new NotFoundServiceEx("User not found"); + } + + Resource resource = resourceDAO.find(resourceId); + if (resource == null) { + throw new NotFoundServiceEx("Resource not found"); + } + + user.getFavorites().remove(resource); + + userDAO.persist(user); + } +} diff --git a/src/core/services-impl/src/main/java/it/geosolutions/geostore/services/ResourceServiceImpl.java b/src/core/services-impl/src/main/java/it/geosolutions/geostore/services/ResourceServiceImpl.java index 161ec2f3..1d820553 100644 --- a/src/core/services-impl/src/main/java/it/geosolutions/geostore/services/ResourceServiceImpl.java +++ b/src/core/services-impl/src/main/java/it/geosolutions/geostore/services/ResourceServiceImpl.java @@ -27,6 +27,7 @@ */ package it.geosolutions.geostore.services; +import com.googlecode.genericdao.search.Filter; import com.googlecode.genericdao.search.Search; import com.googlecode.genericdao.search.Sort; import it.geosolutions.geostore.core.dao.AttributeDAO; @@ -685,6 +686,11 @@ private List searchResources(ResourceSearchParameters parameters) searchCriteria.addFilterILike("name", parameters.getNameLike()); } + if (parameters.isFavoritesOnly()) { + Long userId = parameters.getAuthUser().getId(); + searchCriteria.addFilterSome("favoritedBy", Filter.equal("id", userId)); + } + searchCriteria.addFetch("security"); searchCriteria.setDistinct(true); @@ -804,31 +810,29 @@ public void updateSecurityRules(long id, List rules) } } - /** - * Get filter count by filter and user - * - * @param filter - * @param user - * @return resources' count that the user has access - * @throws InternalErrorServiceEx - * @throws BadRequestServiceEx - */ - public long getCountByFilterAndUser(SearchFilter filter, User user) + @Override + public long count(SearchFilter filter, User user) + throws BadRequestServiceEx, InternalErrorServiceEx { + return count(filter, user, false); + } + + @Override + public long count(SearchFilter filter, User user, boolean favoritesOnly) throws BadRequestServiceEx, InternalErrorServiceEx { + Search searchCriteria = SearchConverter.convert(filter); - securityDAO.addAdvertisedSecurityConstraints(searchCriteria, user); searchCriteria.setDistinct(true); + + if (favoritesOnly) { + searchCriteria.addFilterSome("favoritedBy", Filter.equal("id", user.getId())); + } + + securityDAO.addAdvertisedSecurityConstraints(searchCriteria, user); return resourceDAO.count(searchCriteria); } - /** - * Get filter count by nameLike and user - * - * @param nameLike - * @param user - * @return resources' count that the user has access - */ - public long getCountByFilterAndUser(String nameLike, User user) { + @Override + public long count(String nameLike, User user) { Search searchCriteria = new Search(Resource.class); diff --git a/src/core/services-impl/src/main/java/it/geosolutions/geostore/services/UserServiceImpl.java b/src/core/services-impl/src/main/java/it/geosolutions/geostore/services/UserServiceImpl.java index 742c5a6a..7bcc2964 100644 --- a/src/core/services-impl/src/main/java/it/geosolutions/geostore/services/UserServiceImpl.java +++ b/src/core/services-impl/src/main/java/it/geosolutions/geostore/services/UserServiceImpl.java @@ -21,6 +21,7 @@ import com.googlecode.genericdao.search.Filter; import com.googlecode.genericdao.search.Search; +import it.geosolutions.geostore.core.dao.ResourceDAO; import it.geosolutions.geostore.core.dao.SecurityDAO; import it.geosolutions.geostore.core.dao.UserAttributeDAO; import it.geosolutions.geostore.core.dao.UserDAO; @@ -60,25 +61,28 @@ public class UserServiceImpl implements UserService { private SecurityDAO securityDAO; - /** @param userGroupDAO the userGroupDAO to set */ - public void setUserGroupDAO(UserGroupDAO userGroupDAO) { - this.userGroupDAO = userGroupDAO; + private ResourceDAO resourceDAO; + + public void setUserDAO(UserDAO userDAO) { + this.userDAO = userDAO; } - /** @param userAttributeDAO the userAttributeDAO to set */ public void setUserAttributeDAO(UserAttributeDAO userAttributeDAO) { this.userAttributeDAO = userAttributeDAO; } - /** @param userDAO the userDAO to set */ - public void setUserDAO(UserDAO userDAO) { - this.userDAO = userDAO; + public void setUserGroupDAO(UserGroupDAO userGroupDAO) { + this.userGroupDAO = userGroupDAO; } public void setSecurityDAO(SecurityDAO securityDAO) { this.securityDAO = securityDAO; } + public void setResourceDAO(ResourceDAO resourceDAO) { + this.resourceDAO = resourceDAO; + } + /* * (non-Javadoc) * @@ -481,4 +485,13 @@ public void fetchSecurityRules(User user) { user.setSecurity(securityDAO.findUserSecurityRules(user.getId())); } + + @Override + public void fetchFavorites(User user) { + if (user == null || user.getId() == null) { + return; + } + + user.setFavorites(new HashSet<>(resourceDAO.findUserFavorites(user.getId()))); + } } diff --git a/src/core/services-impl/src/main/resources/applicationContext.xml b/src/core/services-impl/src/main/resources/applicationContext.xml index 66430b29..bd20a1b8 100644 --- a/src/core/services-impl/src/main/resources/applicationContext.xml +++ b/src/core/services-impl/src/main/resources/applicationContext.xml @@ -5,20 +5,22 @@ http://www.springframework.org/schema/beans/spring-beans-2.5.xsd" default-autowire="byName"> - + - + - + - + - + - + - + - + + + diff --git a/src/core/services-impl/src/test/java/it/geosolutions/geostore/services/FavoriteServiceImplTest.java b/src/core/services-impl/src/test/java/it/geosolutions/geostore/services/FavoriteServiceImplTest.java new file mode 100644 index 00000000..043accf0 --- /dev/null +++ b/src/core/services-impl/src/test/java/it/geosolutions/geostore/services/FavoriteServiceImplTest.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2025 GeoSolutions S.A.S. + * http://www.geo-solutions.it + * + * GPLv3 + Classpath exception + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package it.geosolutions.geostore.services; + +import static org.junit.Assert.assertThrows; + +import it.geosolutions.geostore.core.model.Resource; +import it.geosolutions.geostore.core.model.User; +import it.geosolutions.geostore.core.model.enums.Role; +import it.geosolutions.geostore.services.exception.NotFoundServiceEx; +import java.util.Collections; +import java.util.Set; + +public class FavoriteServiceImplTest extends ServiceTestBase { + + public FavoriteServiceImplTest() {} + + public void testAddFavorite() throws Exception { + + long resourceId = createResource("resource", "description", "category"); + long userId = createUser("user", Role.USER, "password"); + + favoriteService.addFavorite(userId, resourceId); + + User user = userService.get(userId); + userService.fetchFavorites(user); + + Set resourceFavorites = user.getFavorites(); + assertEquals(1, resourceFavorites.size()); + Resource resourceFavorite = resourceFavorites.stream().findFirst().orElseThrow(); + assertEquals(resourceId, resourceFavorite.getId().longValue()); + } + + public void testAddFavoriteNotFoundUser() throws Exception { + long resourceId = createResource("resource", "description", "category"); + assertThrows(NotFoundServiceEx.class, () -> favoriteService.addFavorite(0L, resourceId)); + } + + public void testAddFavoriteNotFoundResource() throws Exception { + long userId = createUser("user", Role.USER, "password"); + assertThrows(NotFoundServiceEx.class, () -> favoriteService.addFavorite(userId, 0L)); + } + + public void testRemoveFavorite() throws Exception { + + long resourceId = createResource("resource", "description", "category"); + long userId = createUser("user", Role.USER, "password"); + + User user = userService.get(userId); + Resource resource = resourceService.get(resourceId); + user.setFavorites(Collections.singleton(resource)); + userService.update(user); + + userService.fetchFavorites(user); + assertFalse(user.getFavorites().isEmpty()); + + favoriteService.removeFavorite(userId, resourceId); + userService.fetchFavorites(user); + + assertTrue(user.getFavorites().isEmpty()); + } + + public void testRemoveFromResourceNotFoundUser() throws Exception { + long resourceId = createResource("resource", "description", "category"); + assertThrows(NotFoundServiceEx.class, () -> favoriteService.removeFavorite(0L, resourceId)); + } + + public void testRemoveFromResourceNotFoundResource() throws Exception { + long userId = createUser("user", Role.USER, "password"); + assertThrows(NotFoundServiceEx.class, () -> favoriteService.removeFavorite(userId, 0L)); + } +} diff --git a/src/core/services-impl/src/test/java/it/geosolutions/geostore/services/ServiceTestBase.java b/src/core/services-impl/src/test/java/it/geosolutions/geostore/services/ServiceTestBase.java index e4ef8009..aab25134 100644 --- a/src/core/services-impl/src/test/java/it/geosolutions/geostore/services/ServiceTestBase.java +++ b/src/core/services-impl/src/test/java/it/geosolutions/geostore/services/ServiceTestBase.java @@ -62,6 +62,8 @@ public class ServiceTestBase extends TestCase { protected static TagService tagService; + protected static FavoriteService favoriteService; + protected static ResourceDAO resourceDAO; protected static TagDAO tagDAO; @@ -85,6 +87,7 @@ public ServiceTestBase() { userService = (UserService) ctx.getBean("userService"); userGroupService = (UserGroupService) ctx.getBean("userGroupService"); tagService = (TagService) ctx.getBean("tagService"); + favoriteService = (FavoriteService) ctx.getBean("favoriteService"); resourceDAO = (ResourceDAO) ctx.getBean("resourceDAO"); tagDAO = (TagDAO) ctx.getBean("tagDAO"); } diff --git a/src/modules/rest/api/src/main/java/it/geosolutions/geostore/services/rest/RESTFavoriteService.java b/src/modules/rest/api/src/main/java/it/geosolutions/geostore/services/rest/RESTFavoriteService.java new file mode 100644 index 00000000..cfc441ff --- /dev/null +++ b/src/modules/rest/api/src/main/java/it/geosolutions/geostore/services/rest/RESTFavoriteService.java @@ -0,0 +1,72 @@ +/* ==================================================================== + * + * Copyright (C) 2025 GeoSolutions S.A.S. + * http://www.geo-solutions.it + * + * GPLv3 + Classpath exception + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. + * + * ==================================================================== + * + * This software consists of voluntary contributions made by developers + * of GeoSolutions. For more information on GeoSolutions, please see + * . + * + */ +package it.geosolutions.geostore.services.rest; + +import it.geosolutions.geostore.services.rest.exception.NotFoundWebEx; +import javax.ws.rs.DELETE; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.SecurityContext; +import org.springframework.security.access.annotation.Secured; + +/** + * REST service mapped under the /users path. For example, to assign a favorite to the + * user, use the endpoint: POST /rest/users/user/{userId}/favorite/{resourceId}. + */ +public interface RESTFavoriteService { + + /** + * @param id user identifier + * @param resourceId resource identifier + * @throws NotFoundWebEx + */ + @POST + @Path("user/{id}/favorite/{resourceId}") + @Secured({"ROLE_ADMIN", "ROLE_USER"}) + void addFavorite( + @Context SecurityContext sc, + @PathParam("id") long id, + @PathParam("resourceId") long resourceId) + throws NotFoundWebEx; + + /** + * @param id user identifier + * @param resourceId resource identifier + * @throws NotFoundWebEx + */ + @DELETE + @Path("user/{id}/favorite/{resourceId}") + @Secured({"ROLE_ADMIN", "ROLE_USER"}) + void removeFavorite( + @Context SecurityContext sc, + @PathParam("id") long id, + @PathParam("resourceId") long resourceId) + throws NotFoundWebEx; +} diff --git a/src/modules/rest/extjs/src/main/java/it/geosolutions/geostore/services/model/ExtResource.java b/src/modules/rest/extjs/src/main/java/it/geosolutions/geostore/services/model/ExtResource.java index fc469ffb..5ace5c73 100644 --- a/src/modules/rest/extjs/src/main/java/it/geosolutions/geostore/services/model/ExtResource.java +++ b/src/modules/rest/extjs/src/main/java/it/geosolutions/geostore/services/model/ExtResource.java @@ -6,8 +6,8 @@ import javax.xml.bind.annotation.XmlRootElement; /** - * An extended version of the {@link Resource} class that includes additional permission flags to - * indicate whether the resource can be edited, deleted, or copied. + * An extended version of the {@link Resource} class that includes additional flags to indicate + * whether the resource can be edited, deleted, or copied and if the resource is a user favorite. */ @XmlRootElement(name = "Resource") public class ExtResource extends Resource { @@ -15,6 +15,7 @@ public class ExtResource extends Resource { @XmlElement private boolean canEdit; @XmlElement private boolean canDelete; @XmlElement private boolean canCopy; + @XmlElement private boolean isFavorite; public ExtResource() {} @@ -37,6 +38,7 @@ private ExtResource(Builder builder) { this.canEdit = builder.canEdit; this.canDelete = builder.canDelete; this.canCopy = builder.canCopy; + this.isFavorite = builder.isFavorite; } public boolean isCanEdit() { @@ -51,6 +53,10 @@ public boolean isCanCopy() { return canCopy; } + public boolean isFavorite() { + return isFavorite; + } + public static Builder builder(Resource resource) { return new Builder(resource); } @@ -60,6 +66,7 @@ public static class Builder { private boolean canEdit; private boolean canDelete; private boolean canCopy; + private boolean isFavorite; private Builder(Resource resource) { this.resource = resource; @@ -80,6 +87,11 @@ public Builder withCanCopy(boolean canCopy) { return this; } + public Builder withIsFavorite(boolean isFavorite) { + this.isFavorite = isFavorite; + return this; + } + public ExtResource build() { return new ExtResource(this); } @@ -91,11 +103,14 @@ public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) return false; if (!super.equals(o)) return false; ExtResource that = (ExtResource) o; - return canEdit == that.canEdit && canDelete == that.canDelete && canCopy == that.canCopy; + return canEdit == that.canEdit + && canDelete == that.canDelete + && canCopy == that.canCopy + && isFavorite == that.isFavorite; } @Override public int hashCode() { - return Objects.hash(super.hashCode(), canEdit, canDelete, canCopy); + return Objects.hash(super.hashCode(), canEdit, canDelete, canCopy, isFavorite); } } diff --git a/src/modules/rest/extjs/src/main/java/it/geosolutions/geostore/services/model/ExtShortResource.java b/src/modules/rest/extjs/src/main/java/it/geosolutions/geostore/services/model/ExtShortResource.java index 1262b00a..5c332db6 100644 --- a/src/modules/rest/extjs/src/main/java/it/geosolutions/geostore/services/model/ExtShortResource.java +++ b/src/modules/rest/extjs/src/main/java/it/geosolutions/geostore/services/model/ExtShortResource.java @@ -9,7 +9,7 @@ /** * An extended version of the {@link ShortResource} class that includes {@link ShortAttributeList} - * and {@link SecurityRuleList} for the resource. + * and {@link SecurityRuleList} for the resource along its favourite status. */ @XmlRootElement(name = "ShortResource") public class ExtShortResource extends ShortResource { @@ -17,6 +17,7 @@ public class ExtShortResource extends ShortResource { @XmlElement private ShortAttributeList attributeList; @XmlElement private SecurityRuleList securityRuleList; @XmlElement private TagList tagList; + @XmlElement private boolean isFavorite; public ExtShortResource() {} @@ -35,6 +36,7 @@ private ExtShortResource(Builder builder) { this.attributeList = builder.attributeList; this.securityRuleList = builder.securityRuleList; this.tagList = builder.tagList; + this.isFavorite = builder.isFavorite; } public ShortAttributeList getAttributeList() { @@ -49,15 +51,20 @@ public TagList getTagList() { return tagList; } + public boolean isFavorite() { + return isFavorite; + } + public static Builder builder(ShortResource resource) { return new Builder(resource); } public static class Builder { private final ShortResource shortResource; - public ShortAttributeList attributeList; - public SecurityRuleList securityRuleList; - public TagList tagList; + private ShortAttributeList attributeList; + private SecurityRuleList securityRuleList; + private TagList tagList; + private boolean isFavorite; private Builder(ShortResource shortResource) { this.shortResource = shortResource; @@ -78,6 +85,11 @@ public Builder withTagList(TagList tagList) { return this; } + public Builder withIsFavorite(boolean isFavorite) { + this.isFavorite = isFavorite; + return this; + } + public ExtShortResource build() { return new ExtShortResource(this); } diff --git a/src/modules/rest/extjs/src/main/java/it/geosolutions/geostore/services/rest/RESTExtJsService.java b/src/modules/rest/extjs/src/main/java/it/geosolutions/geostore/services/rest/RESTExtJsService.java index 05ab5f75..327272b8 100644 --- a/src/modules/rest/extjs/src/main/java/it/geosolutions/geostore/services/rest/RESTExtJsService.java +++ b/src/modules/rest/extjs/src/main/java/it/geosolutions/geostore/services/rest/RESTExtJsService.java @@ -130,6 +130,7 @@ String getResourcesByCategory( * @param includeAttributes whether to include attributes in the returned results * @param includeData whether to include data in the returned results * @param includeTags whether to include tags in the returned results + * @param favoritesOnly whether to return only user favorite resources * @param filter the multipart filter object to apply for resource filtering * @return * @throws BadRequestWebEx @@ -149,6 +150,7 @@ ExtResourceList getExtResourcesList( @QueryParam("includeAttributes") @DefaultValue("false") boolean includeAttributes, @QueryParam("includeData") @DefaultValue("false") boolean includeData, @QueryParam("includeTags") @DefaultValue("false") boolean includeTags, + @QueryParam("favoritesOnly") @DefaultValue("false") boolean favoritesOnly, @Multipart("filter") SearchFilter filter) throws BadRequestWebEx, InternalErrorWebEx; diff --git a/src/modules/rest/extjs/src/main/java/it/geosolutions/geostore/services/rest/impl/RESTExtJsServiceImpl.java b/src/modules/rest/extjs/src/main/java/it/geosolutions/geostore/services/rest/impl/RESTExtJsServiceImpl.java index c077f081..ce821b7f 100644 --- a/src/modules/rest/extjs/src/main/java/it/geosolutions/geostore/services/rest/impl/RESTExtJsServiceImpl.java +++ b/src/modules/rest/extjs/src/main/java/it/geosolutions/geostore/services/rest/impl/RESTExtJsServiceImpl.java @@ -91,7 +91,6 @@ public class RESTExtJsServiceImpl extends RESTServiceImpl implements RESTExtJsSe private ResourcePermissionService resourcePermissionService; - /** @param resourceService */ public void setResourceService(ResourceService resourceService) { this.resourceService = resourceService; } @@ -157,7 +156,7 @@ public String getAllResources(SecurityContext sc, String nameLike, Integer start long count = 0; if (resources != null && !resources.isEmpty()) { - count = resourceService.getCountByFilterAndUser(sqlNameLike, authUser); + count = resourceService.count(sqlNameLike, authUser); } JSONObject result = makeJSONResult(true, count, resources, authUser); @@ -295,7 +294,7 @@ public String getResourcesByCategory( long count = 0; if (!resources.isEmpty()) { - count = resourceService.getCountByFilterAndUser(filter, authUser); + count = resourceService.count(filter, authUser); } JSONObject result = @@ -316,12 +315,6 @@ public String getResourcesByCategory( } } - /* - * (non-Javadoc) - * - * @see it.geosolutions.geostore.services.rest.RESTExtJsService#getResourcesList(javax.ws.rs.core.SecurityContext, java.lang.Integer, - * java.lang.Integer, java.lang.String, java.lang.String, boolean, boolean, it.geosolutions.geostore.services.dto.search.SearchFilter) - */ @Override public ExtResourceList getExtResourcesList( SecurityContext sc, @@ -331,6 +324,7 @@ public ExtResourceList getExtResourcesList( boolean includeAttributes, boolean includeData, boolean includeTags, + boolean favoritesOnly, SearchFilter filter) throws BadRequestWebEx { @@ -340,12 +334,13 @@ public ExtResourceList getExtResourcesList( if (LOGGER.isDebugEnabled()) { LOGGER.debug( - "getResourcesList(start={}, limit={}, includeAttributes={}, includeData={}, includeTags={}", + "getResourcesList(start={}, limit={}, includeAttributes={}, includeData={}, includeTags={}, favoritesOnly={}", start, limit, includeAttributes, includeData, - includeTags); + includeTags, + favoritesOnly); } User authUser = null; @@ -364,23 +359,25 @@ public ExtResourceList getExtResourcesList( } try { - List resources = - resourceService.getResources( - ResourceSearchParameters.builder() - .filter(filter) - .page(page) - .entries(limit) - .sortBy(sort.getSortBy()) - .sortOrder(sort.getSortOrder()) - .includeAttributes(includeAttributes) - .includeData(includeData) - .includeTags(includeTags) - .authUser(authUser) - .build()); + ResourceSearchParameters searchParameters = + ResourceSearchParameters.builder() + .filter(filter) + .page(page) + .entries(limit) + .sortBy(sort.getSortBy()) + .sortOrder(sort.getSortOrder()) + .includeAttributes(includeAttributes) + .includeData(includeData) + .includeTags(includeTags) + .favoritesOnly(favoritesOnly) + .authUser(authUser) + .build(); + + List resources = resourceService.getResources(searchParameters); long count = 0; if (!resources.isEmpty()) { - count = resourceService.getCountByFilterAndUser(filter, authUser); + count = resourceService.count(filter, authUser, favoritesOnly); } return new ExtResourceList(count, convertToExtResources(resources, authUser)); @@ -402,6 +399,7 @@ public ExtResourceList getExtResourcesList( private List convertToExtResources(List foundResources, User user) { userService.fetchSecurityRules(user); + userService.fetchFavorites(user); return foundResources.stream() .map(r -> convertToExtResource(r, user)) @@ -419,9 +417,19 @@ private ExtResource convertToExtResource(Resource resource, User user) { extResourceBuilder.withCanEdit(true).withCanDelete(true); } + if (user != null) { + extResourceBuilder.withIsFavorite(isResourceUserFavorite(resource, user)); + } + return extResourceBuilder.build(); } + private boolean isResourceUserFavorite(Resource resource, User user) { + return user.getFavorites().stream() + .map(Resource::getId) + .anyMatch(id -> id.equals(resource.getId())); + } + /** * @param success * @param count @@ -681,6 +689,7 @@ public ExtShortResource getExtResource( User authUser = extractAuthUser(sc); userService.fetchSecurityRules(authUser); + userService.fetchFavorites(authUser); if (!resourcePermissionService.canUserReadResource(authUser, id)) { throw new ForbiddenErrorWebEx("Resource is protected"); @@ -696,6 +705,7 @@ public ExtShortResource getExtResource( .withAttributes(createShortAttributeList(resource.getAttribute())) .withSecurityRules(new SecurityRuleList(resource.getSecurity())) .withTagList(createTagList(resource.getTags())) + .withIsFavorite(isResourceUserFavorite(resource, authUser)) .build(); } diff --git a/src/modules/rest/extjs/src/test/java/it/geosolutions/geostore/services/rest/impl/RESTExtJsServiceImplTest.java b/src/modules/rest/extjs/src/test/java/it/geosolutions/geostore/services/rest/impl/RESTExtJsServiceImplTest.java index e080c165..63de001a 100644 --- a/src/modules/rest/extjs/src/test/java/it/geosolutions/geostore/services/rest/impl/RESTExtJsServiceImplTest.java +++ b/src/modules/rest/extjs/src/test/java/it/geosolutions/geostore/services/rest/impl/RESTExtJsServiceImplTest.java @@ -460,6 +460,7 @@ public void testExtResourcesList_sorted() throws Exception { false, false, false, + false, new AndFilter()); List resources = response.getList(); @@ -481,6 +482,7 @@ public void testExtResourcesList_sorted() throws Exception { false, false, false, + false, new AndFilter()); List resources = response.getList(); @@ -501,6 +503,7 @@ public void testExtResourcesList_sorted() throws Exception { false, false, false, + false, new AndFilter()); List resources = response.getList(); @@ -521,6 +524,7 @@ public void testExtResourcesList_sorted() throws Exception { false, false, false, + false, new AndFilter()); assertNull(response); @@ -555,7 +559,15 @@ public void testExtResourcesList_creatorFiltered() throws Exception { ExtResourceList response = restExtJsService.getExtResourcesList( - sc, 0, 100, new Sort("", ""), false, false, false, editorFieldFilter); + sc, + 0, + 100, + new Sort("", ""), + false, + false, + false, + false, + editorFieldFilter); List resources = response.getList(); assertEquals(1, resources.size()); @@ -569,7 +581,15 @@ public void testExtResourcesList_creatorFiltered() throws Exception { ExtResourceList response = restExtJsService.getExtResourcesList( - sc, 0, 100, new Sort("", ""), false, false, false, editorFieldFilter); + sc, + 0, + 100, + new Sort("", ""), + false, + false, + false, + false, + editorFieldFilter); List resources = response.getList(); assertEquals(2, resources.size()); @@ -581,7 +601,15 @@ public void testExtResourcesList_creatorFiltered() throws Exception { ExtResourceList response = restExtJsService.getExtResourcesList( - sc, 0, 100, new Sort("", ""), false, false, false, editorFieldFilter); + sc, + 0, + 100, + new Sort("", ""), + false, + false, + false, + false, + editorFieldFilter); assertTrue(response.isEmpty()); } @@ -615,7 +643,15 @@ public void testExtResourcesList_editorFiltered() throws Exception { ExtResourceList response = restExtJsService.getExtResourcesList( - sc, 0, 100, new Sort("", ""), false, false, false, editorFieldFilter); + sc, + 0, + 100, + new Sort("", ""), + false, + false, + false, + false, + editorFieldFilter); List resources = response.getList(); assertEquals(1, resources.size()); @@ -629,7 +665,15 @@ public void testExtResourcesList_editorFiltered() throws Exception { ExtResourceList response = restExtJsService.getExtResourcesList( - sc, 0, 100, new Sort("", ""), false, false, false, editorFieldFilter); + sc, + 0, + 100, + new Sort("", ""), + false, + false, + false, + false, + editorFieldFilter); List resources = response.getList(); assertEquals(2, resources.size()); @@ -641,7 +685,15 @@ public void testExtResourcesList_editorFiltered() throws Exception { ExtResourceList response = restExtJsService.getExtResourcesList( - sc, 0, 100, new Sort("", ""), false, false, false, editorFieldFilter); + sc, + 0, + 100, + new Sort("", ""), + false, + false, + false, + false, + editorFieldFilter); assertTrue(response.isEmpty()); } @@ -689,7 +741,7 @@ public void testExtResourcesList_groupFiltered() throws Exception { ExtResourceList response = restExtJsService.getExtResourcesList( - sc, 0, 100, new Sort("", ""), false, false, false, groupFilter); + sc, 0, 100, new Sort("", ""), false, false, false, false, groupFilter); List resources = response.getList(); assertEquals(1, resources.size()); @@ -703,7 +755,7 @@ public void testExtResourcesList_groupFiltered() throws Exception { ExtResourceList response = restExtJsService.getExtResourcesList( - sc, 0, 100, new Sort("", ""), false, false, false, groupFilter); + sc, 0, 100, new Sort("", ""), false, false, false, false, groupFilter); List resources = response.getList(); assertEquals(2, resources.size()); @@ -715,7 +767,7 @@ public void testExtResourcesList_groupFiltered() throws Exception { ExtResourceList response = restExtJsService.getExtResourcesList( - sc, 0, 100, new Sort("", ""), false, false, false, groupFilter); + sc, 0, 100, new Sort("", ""), false, false, false, false, groupFilter); List resources = response.getList(); assertEquals(2, resources.size()); @@ -727,7 +779,7 @@ public void testExtResourcesList_groupFiltered() throws Exception { ExtResourceList response = restExtJsService.getExtResourcesList( - sc, 0, 100, new Sort("", ""), false, false, false, groupFilter); + sc, 0, 100, new Sort("", ""), false, false, false, false, groupFilter); assertTrue(response.getList().isEmpty()); } @@ -737,7 +789,7 @@ public void testExtResourcesList_groupFiltered() throws Exception { ExtResourceList response = restExtJsService.getExtResourcesList( - sc, 0, 100, new Sort("", ""), false, false, false, groupFilter); + sc, 0, 100, new Sort("", ""), false, false, false, false, groupFilter); assertTrue(response.getList().isEmpty()); } @@ -789,7 +841,7 @@ public void testExtResourcesList_filteredForGroupNameWithCommas() throws Excepti ExtResourceList response = restExtJsService.getExtResourcesList( - sc, 0, 100, new Sort("", ""), false, false, false, groupFilter); + sc, 0, 100, new Sort("", ""), false, false, false, false, groupFilter); List resources = response.getList(); assertEquals(1, resources.size()); @@ -803,7 +855,7 @@ public void testExtResourcesList_filteredForGroupNameWithCommas() throws Excepti ExtResourceList response = restExtJsService.getExtResourcesList( - sc, 0, 100, new Sort("", ""), false, false, false, groupFilter); + sc, 0, 100, new Sort("", ""), false, false, false, false, groupFilter); List resources = response.getList(); assertEquals(1, resources.size()); @@ -817,7 +869,7 @@ public void testExtResourcesList_filteredForGroupNameWithCommas() throws Excepti ExtResourceList response = restExtJsService.getExtResourcesList( - sc, 0, 100, new Sort("", ""), false, false, false, groupFilter); + sc, 0, 100, new Sort("", ""), false, false, false, false, groupFilter); List resources = response.getList(); assertEquals(2, resources.size()); @@ -856,7 +908,7 @@ public void testExtResourcesList_tagFiltered() throws Exception { ExtResourceList response = restExtJsService.getExtResourcesList( - sc, 0, 100, new Sort("", ""), false, false, false, tagFilter); + sc, 0, 100, new Sort("", ""), false, false, false, false, tagFilter); List resources = response.getList(); assertEquals(1, resources.size()); @@ -870,7 +922,7 @@ public void testExtResourcesList_tagFiltered() throws Exception { ExtResourceList response = restExtJsService.getExtResourcesList( - sc, 0, 100, new Sort("", ""), false, false, false, tagFilter); + sc, 0, 100, new Sort("", ""), false, false, false, false, tagFilter); List resources = response.getList(); assertEquals(2, resources.size()); @@ -882,7 +934,7 @@ public void testExtResourcesList_tagFiltered() throws Exception { ExtResourceList response = restExtJsService.getExtResourcesList( - sc, 0, 100, new Sort("", ""), false, false, false, tagFilter); + sc, 0, 100, new Sort("", ""), false, false, false, false, tagFilter); List resources = response.getList(); assertEquals(2, resources.size()); @@ -894,7 +946,7 @@ public void testExtResourcesList_tagFiltered() throws Exception { ExtResourceList response = restExtJsService.getExtResourcesList( - sc, 0, 100, new Sort("", ""), false, false, false, tagFilter); + sc, 0, 100, new Sort("", ""), false, false, false, false, tagFilter); assertTrue(response.getList().isEmpty()); } @@ -904,7 +956,7 @@ public void testExtResourcesList_tagFiltered() throws Exception { ExtResourceList response = restExtJsService.getExtResourcesList( - sc, 0, 100, new Sort("", ""), false, false, false, tagFilter); + sc, 0, 100, new Sort("", ""), false, false, false, false, tagFilter); assertTrue(response.getList().isEmpty()); } @@ -944,7 +996,7 @@ public void testExtResourcesList_filteredForTagNameWithCommas() throws Exception ExtResourceList response = restExtJsService.getExtResourcesList( - sc, 0, 100, new Sort("", ""), false, false, false, tagFilter); + sc, 0, 100, new Sort("", ""), false, false, false, false, tagFilter); List resources = response.getList(); assertEquals(1, resources.size()); @@ -958,7 +1010,7 @@ public void testExtResourcesList_filteredForTagNameWithCommas() throws Exception ExtResourceList response = restExtJsService.getExtResourcesList( - sc, 0, 100, new Sort("", ""), false, false, false, tagFilter); + sc, 0, 100, new Sort("", ""), false, false, false, false, tagFilter); List resources = response.getList(); assertEquals(1, resources.size()); @@ -972,7 +1024,7 @@ public void testExtResourcesList_filteredForTagNameWithCommas() throws Exception ExtResourceList response = restExtJsService.getExtResourcesList( - sc, 0, 100, new Sort("", ""), false, false, false, tagFilter); + sc, 0, 100, new Sort("", ""), false, false, false, false, tagFilter); List resources = response.getList(); assertEquals(2, resources.size()); @@ -1008,7 +1060,7 @@ public void testExtResourcesList_timeAttributesFiltered() throws Exception { ExtResourceList response = restExtJsService.getExtResourcesList( - sc, 0, 100, new Sort("", ""), false, false, false, ltDateFilter); + sc, 0, 100, new Sort("", ""), false, false, false, false, ltDateFilter); List resources = response.getList(); assertEquals(1, resources.size()); @@ -1040,6 +1092,7 @@ public void testExtResourcesList_timeAttributesFiltered() throws Exception { false, false, false, + false, betweenDatesFieldFilter); List resources = response.getList(); @@ -1100,6 +1153,7 @@ public void testExtResourcesList_userOwnedWithPermissionsInformation() throws Ex false, false, false, + false, new AndFilter()); List resources = response.getList(); assertEquals(5, resources.size()); @@ -1118,6 +1172,7 @@ public void testExtResourcesList_userOwnedWithPermissionsInformation() throws Ex false, false, false, + false, new AndFilter()); List resources = response.getList(); assertEquals(2, resources.size()); @@ -1212,6 +1267,7 @@ public void testExtResourcesList_groupOwnedResourceWithPermissionsInformation() false, false, false, + false, new AndFilter()); List resources = response.getList(); assertEquals(3, resources.size()); @@ -1230,6 +1286,7 @@ public void testExtResourcesList_groupOwnedResourceWithPermissionsInformation() false, false, false, + false, new AndFilter()); List resources = response.getList(); assertEquals(2, resources.size()); @@ -1282,6 +1339,7 @@ public void testExtResourcesList_withTags() throws Exception { false, false, true, + false, new AndFilter()); List resources = response.getList(); assertEquals(1, resources.size()); @@ -1300,6 +1358,7 @@ public void testExtResourcesList_withTags() throws Exception { false, false, false, + false, new AndFilter()); List resources = response.getList(); assertEquals(1, resources.size()); @@ -1308,6 +1367,105 @@ public void testExtResourcesList_withTags() throws Exception { } } + @Test + public void testExtResourcesList_favoritesOnly() throws Exception { + final String CAT0_NAME = "CAT000"; + + long userId = restCreateUser("u0", Role.USER, null, "p0"); + SecurityContext sc = new SimpleSecurityContext(userId); + + createCategory(CAT0_NAME); + + long favoriteResourceId = + restCreateResource("favourite_resource", "", CAT0_NAME, userId, true); + restCreateResource("other_resource", "", CAT0_NAME, userId, true); + + favoriteService.addFavorite(userId, favoriteResourceId); + + { + ExtResourceList response = + restExtJsService.getExtResourcesList( + sc, + 0, + 100, + new Sort("", ""), + false, + false, + false, + true, + new AndFilter()); + + List resources = response.getList(); + assertEquals(1, resources.size()); + ExtResource resource = resources.get(0); + assertEquals(favoriteResourceId, resource.getId().longValue()); + assertEquals(1, response.getCount()); + } + + { + ExtResourceList response = + restExtJsService.getExtResourcesList( + sc, + 0, + 100, + new Sort("", ""), + false, + false, + false, + false, + new AndFilter()); + + List resources = response.getList(); + assertEquals(2, resources.size()); + assertEquals(2, response.getCount()); + } + } + + @Test + public void testExtResourcesList_withFavoriteInformation() throws Exception { + final String CAT0_NAME = "CAT000"; + + long userId = restCreateUser("u0", Role.USER, null, "p0"); + SecurityContext sc = new SimpleSecurityContext(userId); + + createCategory(CAT0_NAME); + + long favoriteResourceId = + restCreateResource("favourite_resource", "", CAT0_NAME, userId, true); + long nonFavoriteResourceId = + restCreateResource("other_resource", "", CAT0_NAME, userId, true); + + favoriteService.addFavorite(userId, favoriteResourceId); + + { + ExtResourceList response = + restExtJsService.getExtResourcesList( + sc, + 0, + 100, + new Sort("", ""), + false, + false, + false, + false, + new AndFilter()); + + List resources = response.getList(); + ExtResource favoriteResource = + resources.stream() + .filter(r -> r.getId().equals(favoriteResourceId)) + .findFirst() + .orElseThrow(); + assertTrue(favoriteResource.isFavorite()); + ExtResource nonFavoriteResource = + resources.stream() + .filter(r -> r.getId().equals(nonFavoriteResourceId)) + .findFirst() + .orElseThrow(); + assertFalse(nonFavoriteResource.isFavorite()); + } + } + @Test public void testGetExtResource_userOwnedWithAttributesInformation() throws Exception { final String CAT0_NAME = "CAT000"; @@ -1815,6 +1973,37 @@ public void testGetExtResource_withTags() throws Exception { } } + @Test + public void testGetExtResource_withFavoriteInformation() throws Exception { + final String CAT0_NAME = "CAT000"; + + long userId = restCreateUser("u0", Role.USER, null, "p0"); + SecurityContext userSecurityContext = new SimpleSecurityContext(userId); + + createCategory(CAT0_NAME); + + long favoriteResourceId = + restCreateResource("favourite_resource", "", CAT0_NAME, userId, true); + long nonFavoriteResourceId = + restCreateResource("other_resource", "", CAT0_NAME, userId, true); + + favoriteService.addFavorite(userId, favoriteResourceId); + + { + ExtShortResource response = + restExtJsService.getExtResource( + userSecurityContext, favoriteResourceId, false, false, true); + assertTrue(response.isFavorite()); + } + + { + ExtShortResource response = + restExtJsService.getExtResource( + userSecurityContext, nonFavoriteResourceId, false, false, true); + assertFalse(response.isFavorite()); + } + } + @Test public void testGetGroupsList() throws Exception { final String groupAName = "groupA"; diff --git a/src/modules/rest/extjs/src/test/java/it/geosolutions/geostore/services/rest/impl/ServiceTestBase.java b/src/modules/rest/extjs/src/test/java/it/geosolutions/geostore/services/rest/impl/ServiceTestBase.java index 2dfcd181..00ea4353 100644 --- a/src/modules/rest/extjs/src/test/java/it/geosolutions/geostore/services/rest/impl/ServiceTestBase.java +++ b/src/modules/rest/extjs/src/test/java/it/geosolutions/geostore/services/rest/impl/ServiceTestBase.java @@ -19,13 +19,28 @@ */ package it.geosolutions.geostore.services.rest.impl; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; import it.geosolutions.geostore.core.dao.ResourceDAO; import it.geosolutions.geostore.core.dao.UserDAO; -import it.geosolutions.geostore.core.model.*; +import it.geosolutions.geostore.core.model.Category; +import it.geosolutions.geostore.core.model.Resource; +import it.geosolutions.geostore.core.model.SecurityRule; +import it.geosolutions.geostore.core.model.StoredData; +import it.geosolutions.geostore.core.model.User; +import it.geosolutions.geostore.core.model.UserAttribute; +import it.geosolutions.geostore.core.model.UserGroup; import it.geosolutions.geostore.core.model.enums.Role; -import it.geosolutions.geostore.services.*; +import it.geosolutions.geostore.services.CategoryService; +import it.geosolutions.geostore.services.FavoriteService; +import it.geosolutions.geostore.services.ResourcePermissionService; +import it.geosolutions.geostore.services.ResourceService; +import it.geosolutions.geostore.services.StoredDataService; +import it.geosolutions.geostore.services.TagService; +import it.geosolutions.geostore.services.UserGroupService; +import it.geosolutions.geostore.services.UserService; import it.geosolutions.geostore.services.dto.ResourceSearchParameters; import it.geosolutions.geostore.services.dto.ShortResource; import it.geosolutions.geostore.services.exception.BadRequestServiceEx; @@ -69,6 +84,7 @@ public class ServiceTestBase { protected static UserService userService; protected static UserGroupService userGroupService; protected static TagService tagService; + protected static FavoriteService favoriteService; protected static ResourcePermissionService resourcePermissionService; protected static ResourceDAO resourceDAO; @@ -97,6 +113,7 @@ public ServiceTestBase() { userService = (UserService) ctx.getBean("userService"); userGroupService = (UserGroupService) ctx.getBean("userGroupService"); tagService = (TagService) ctx.getBean("tagService"); + favoriteService = (FavoriteService) ctx.getBean("favoriteService"); resourcePermissionService = (ResourcePermissionService) ctx.getBean("resourcePermissionService"); diff --git a/src/modules/rest/impl/src/main/java/it/geosolutions/geostore/services/rest/impl/RESTFavoriteServiceImpl.java b/src/modules/rest/impl/src/main/java/it/geosolutions/geostore/services/rest/impl/RESTFavoriteServiceImpl.java new file mode 100644 index 00000000..803d8d65 --- /dev/null +++ b/src/modules/rest/impl/src/main/java/it/geosolutions/geostore/services/rest/impl/RESTFavoriteServiceImpl.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2025 GeoSolutions S.A.S. + * http://www.geo-solutions.it + * + * GPLv3 + Classpath exception + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. + * + * ==================================================================== + * + * This software consists of voluntary contributions made by developers + * of GeoSolutions. For more information on GeoSolutions, please see + * . + * + */ +package it.geosolutions.geostore.services.rest.impl; + +import it.geosolutions.geostore.services.FavoriteService; +import it.geosolutions.geostore.services.exception.NotFoundServiceEx; +import it.geosolutions.geostore.services.rest.RESTFavoriteService; +import it.geosolutions.geostore.services.rest.exception.NotFoundWebEx; +import javax.ws.rs.core.SecurityContext; + +public class RESTFavoriteServiceImpl implements RESTFavoriteService { + + private FavoriteService favoriteService; + + public void setFavoriteService(FavoriteService favoriteService) { + this.favoriteService = favoriteService; + } + + @Override + public void addFavorite(SecurityContext sc, long id, long resourceId) throws NotFoundWebEx { + try { + favoriteService.addFavorite(id, resourceId); + } catch (NotFoundServiceEx e) { + throw new NotFoundWebEx(e.getMessage()); + } + } + + @Override + public void removeFavorite(SecurityContext sc, long id, long resourceId) throws NotFoundWebEx { + try { + favoriteService.removeFavorite(id, resourceId); + } catch (NotFoundServiceEx e) { + throw new NotFoundWebEx(e.getMessage()); + } + } +} diff --git a/src/modules/rest/impl/src/main/resources/applicationContext.xml b/src/modules/rest/impl/src/main/resources/applicationContext.xml index 30b49100..42e3adc6 100644 --- a/src/modules/rest/impl/src/main/resources/applicationContext.xml +++ b/src/modules/rest/impl/src/main/resources/applicationContext.xml @@ -58,14 +58,20 @@ + + + + + @@ -177,6 +183,7 @@ +