diff --git a/src/main/java/org/ohdsi/webapi/security/model/EntityType.java b/src/main/java/org/ohdsi/webapi/security/model/EntityType.java index c294cdb17..15cb80ea9 100644 --- a/src/main/java/org/ohdsi/webapi/security/model/EntityType.java +++ b/src/main/java/org/ohdsi/webapi/security/model/EntityType.java @@ -13,6 +13,7 @@ import org.ohdsi.webapi.reusable.domain.Reusable; import org.ohdsi.webapi.source.Source; import org.ohdsi.webapi.tag.domain.Tag; +import org.ohdsi.webapi.tool.Tool; public enum EntityType { COHORT_DEFINITION(CohortDefinition.class), @@ -26,6 +27,7 @@ public enum EntityType { PREDICTION(PredictionAnalysis.class), COHORT_SAMPLE(CohortSample.class), TAG(Tag.class), + TOOL(Tool.class), REUSABLE(Reusable.class); private final Class entityClass; diff --git a/src/main/java/org/ohdsi/webapi/security/model/ToolPermissionSchema.java b/src/main/java/org/ohdsi/webapi/security/model/ToolPermissionSchema.java new file mode 100644 index 000000000..5ebd45509 --- /dev/null +++ b/src/main/java/org/ohdsi/webapi/security/model/ToolPermissionSchema.java @@ -0,0 +1,23 @@ +package org.ohdsi.webapi.security.model; + +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.Map; + +@Component +public class ToolPermissionSchema extends EntityPermissionSchema { + private static Map writePermissions = new HashMap() {{ + put("tool:%s:delete", "Delete Tool with id = %s"); + }}; + private static Map readPermissions = new HashMap() { + { + put("tool:%s:get", "View Tool with id = %s"); + } + }; + + public ToolPermissionSchema() { + super(EntityType.TOOL, readPermissions, writePermissions); + } + +} \ No newline at end of file diff --git a/src/main/java/org/ohdsi/webapi/tool/Tool.java b/src/main/java/org/ohdsi/webapi/tool/Tool.java new file mode 100644 index 000000000..93f9d3135 --- /dev/null +++ b/src/main/java/org/ohdsi/webapi/tool/Tool.java @@ -0,0 +1,93 @@ +package org.ohdsi.webapi.tool; + +import org.hibernate.annotations.GenericGenerator; +import org.hibernate.annotations.Parameter; +import org.ohdsi.webapi.model.CommonEntity; + +import javax.persistence.Entity; +import javax.persistence.Table; +import javax.persistence.Id; +import javax.persistence.GeneratedValue; +import javax.persistence.Column; +import java.util.Objects; + +@Entity +@Table(name = "tool") +public class Tool extends CommonEntity { + @Id + @GenericGenerator( + name = "tool_generator", + strategy = "org.hibernate.id.enhanced.SequenceStyleGenerator", + parameters = { + @Parameter(name = "sequence_name", value = "tool_seq"), + @Parameter(name = "increment_size", value = "1") + } + ) + @GeneratedValue(generator = "tool_generator") + private Integer id; + + @Column(name = "name") + private String name; + + @Column(name = "url") + private String url; + + @Column(name = "description") + private String description; + @Column(name = "is_enabled") + private Boolean enabled; + + @Override + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Boolean getEnabled() { + return enabled; + } + + public void setEnabled(Boolean enabled) { + this.enabled = enabled; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Tool tool = (Tool) o; + return Objects.equals(name, tool.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } +} diff --git a/src/main/java/org/ohdsi/webapi/tool/ToolController.java b/src/main/java/org/ohdsi/webapi/tool/ToolController.java new file mode 100644 index 000000000..2b593076c --- /dev/null +++ b/src/main/java/org/ohdsi/webapi/tool/ToolController.java @@ -0,0 +1,62 @@ +package org.ohdsi.webapi.tool; + +import org.ohdsi.webapi.tool.dto.ToolDTO; +import org.springframework.stereotype.Controller; + +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import java.util.List; + +@Controller +@Path("/tool") +public class ToolController { + private final ToolServiceImpl service; + + public ToolController(ToolServiceImpl service) { + this.service = service; + } + + @GET + @Path("") + @Produces(MediaType.APPLICATION_JSON) + public List getTools() { + return service.getTools(); + } + + @GET + @Path("/{id}") + @Produces(MediaType.APPLICATION_JSON) + public ToolDTO getToolById(@PathParam("id") Integer id) { + return service.getById(id); + } + + @POST + @Path("") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + public ToolDTO createTool(ToolDTO dto) { + return service.saveTool(dto); + } + + @DELETE + @Path("/{id}") + @Produces(MediaType.APPLICATION_JSON) + public void delete(@PathParam("id") Integer id) { + service.delete(id); + } + + @PUT + @Path("") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + public ToolDTO updateTool(ToolDTO toolDTO) { + return service.saveTool(toolDTO); + } +} diff --git a/src/main/java/org/ohdsi/webapi/tool/ToolRepository.java b/src/main/java/org/ohdsi/webapi/tool/ToolRepository.java new file mode 100644 index 000000000..d18ea106f --- /dev/null +++ b/src/main/java/org/ohdsi/webapi/tool/ToolRepository.java @@ -0,0 +1,11 @@ +package org.ohdsi.webapi.tool; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface ToolRepository extends JpaRepository { + List findAllByEnabled(boolean enabled); +} diff --git a/src/main/java/org/ohdsi/webapi/tool/ToolService.java b/src/main/java/org/ohdsi/webapi/tool/ToolService.java new file mode 100644 index 000000000..32501d4c4 --- /dev/null +++ b/src/main/java/org/ohdsi/webapi/tool/ToolService.java @@ -0,0 +1,13 @@ +package org.ohdsi.webapi.tool; + +import org.ohdsi.webapi.tool.dto.ToolDTO; + +import java.util.List; + +public interface ToolService { + List getTools(); + ToolDTO saveTool(ToolDTO toolDTO); + ToolDTO getById(Integer id); + + void delete(Integer id); +} diff --git a/src/main/java/org/ohdsi/webapi/tool/ToolServiceImpl.java b/src/main/java/org/ohdsi/webapi/tool/ToolServiceImpl.java new file mode 100644 index 000000000..52a498b3c --- /dev/null +++ b/src/main/java/org/ohdsi/webapi/tool/ToolServiceImpl.java @@ -0,0 +1,120 @@ +package org.ohdsi.webapi.tool; + +import java.text.SimpleDateFormat; +import java.time.Instant; +import java.util.Date; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.shiro.SecurityUtils; +import org.ohdsi.webapi.service.AbstractDaoService; +import org.ohdsi.webapi.shiro.Entities.UserEntity; +import org.ohdsi.webapi.tool.dto.ToolDTO; +import org.springframework.stereotype.Service; + +@Service +public class ToolServiceImpl extends AbstractDaoService implements ToolService { + private static final String DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss"; + + private final ToolRepository toolRepository; + + public ToolServiceImpl(ToolRepository toolRepository) { + this.toolRepository = toolRepository; + } + + @Override + public List getTools() { + List tools = (isAdmin() || canManageTools()) ? toolRepository.findAll() : toolRepository.findAllByEnabled(true); + return tools.stream() + .map(this::toDTO).collect(Collectors.toList()); + } + + @Override + public ToolDTO saveTool(ToolDTO toolDTO) { + Tool tool = saveToolFromDTO(toolDTO, getCurrentUser()); + return toDTO(toolRepository.saveAndFlush(tool)); + } + + private Tool saveToolFromDTO(ToolDTO toolDTO, UserEntity currentUser) { + Tool tool = toEntity(toolDTO); + if (toolDTO.getId() == null) { + tool.setCreatedBy(currentUser); + } + tool.setModifiedBy(currentUser); + return tool; + } + + @Override + public ToolDTO getById(Integer id) { + return toDTO(toolRepository.findOne(id)); + } + + @Override + public void delete(Integer id) { + toolRepository.delete(id); + } + + private boolean canManageTools() { + return Stream.of("tool:put", "tool:post", "tool:*:delete") + .allMatch(permission -> SecurityUtils.getSubject().isPermitted(permission)); + } + + Tool toEntity(ToolDTO toolDTO) { + boolean isNewTool = toolDTO.getId() == null; + Tool tool = isNewTool ? new Tool() : toolRepository.findOne(toolDTO.getId()); + Instant currentInstant = Instant.now(); + if (isNewTool) { + setCreationDetails(tool, currentInstant); + } else { + setModificationDetails(tool, currentInstant); + } + updateToolFromDTO(tool, toolDTO); + return tool; + } + + private void setCreationDetails(Tool tool, Instant currentInstant) { + tool.setCreatedDate(Date.from(currentInstant)); + tool.setCreatedBy(getCurrentUser()); + } + + private void setModificationDetails(Tool tool, Instant currentInstant) { + tool.setModifiedDate(Date.from(currentInstant)); + tool.setModifiedBy(getCurrentUser()); + } + + private void updateToolFromDTO(Tool tool, ToolDTO toolDTO) { + Optional.ofNullable(toolDTO.getName()).ifPresent(tool::setName); + Optional.ofNullable(toolDTO.getUrl()).ifPresent(tool::setUrl); + Optional.ofNullable(toolDTO.getDescription()).ifPresent(tool::setDescription); + Optional.ofNullable(toolDTO.getEnabled()).ifPresent(tool::setEnabled); + } + + ToolDTO toDTO(Tool tool) { + return Optional.ofNullable(tool) + .map(t -> { + ToolDTO toolDTO = new ToolDTO(); + toolDTO.setId(t.getId()); + toolDTO.setName(t.getName()); + toolDTO.setUrl(t.getUrl()); + toolDTO.setDescription(t.getDescription()); + Optional.ofNullable(tool.getCreatedBy()) + .map(UserEntity::getId) + .map(userRepository::findOne) + .map(UserEntity::getName) + .ifPresent(toolDTO::setCreatedByName); + Optional.ofNullable(tool.getModifiedBy()) + .map(UserEntity::getId) + .map(userRepository::findOne) + .map(UserEntity::getName) + .ifPresent(toolDTO::setModifiedByName); + toolDTO.setCreatedDate(t.getCreatedDate() != null ? new SimpleDateFormat(DATE_TIME_FORMAT).format(t.getCreatedDate()) : null); + toolDTO.setModifiedDate(t.getModifiedDate() != null ? new SimpleDateFormat(DATE_TIME_FORMAT).format(t.getModifiedDate()) : null); + toolDTO.setEnabled(t.getEnabled()); + return toolDTO; + }) + .orElse(null); + } + +} \ No newline at end of file diff --git a/src/main/java/org/ohdsi/webapi/tool/dto/ToolDTO.java b/src/main/java/org/ohdsi/webapi/tool/dto/ToolDTO.java new file mode 100644 index 000000000..9a068f6af --- /dev/null +++ b/src/main/java/org/ohdsi/webapi/tool/dto/ToolDTO.java @@ -0,0 +1,104 @@ +package org.ohdsi.webapi.tool.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import org.ohdsi.webapi.shiro.Entities.UserEntity; + +import java.util.Objects; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class ToolDTO { + private Integer id; + private String name; + private String url; + private String description; + private String createdByName; + private String modifiedByName; + private String createdDate; + private String modifiedDate; + private Boolean isEnabled; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getCreatedByName() { + return createdByName; + } + + public void setCreatedByName(String createdByName) { + this.createdByName = createdByName; + } + + public String getModifiedByName() { + return modifiedByName; + } + + public void setModifiedByName(String modifiedByName) { + this.modifiedByName = modifiedByName; + } + + public String getCreatedDate() { + return createdDate; + } + + public void setCreatedDate(String createdDate) { + this.createdDate = createdDate; + } + + public String getModifiedDate() { + return modifiedDate; + } + + public void setModifiedDate(String modifiedDate) { + this.modifiedDate = modifiedDate; + } + + public Boolean getEnabled() { + return isEnabled; + } + + public void setEnabled(Boolean enabled) { + isEnabled = enabled; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ToolDTO toolDTO = (ToolDTO) o; + return Objects.equals(name, toolDTO.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } +} diff --git a/src/main/resources/db/migration/postgresql/V2.15.0.20231213141052__create_table_tools_and_permissions.sql b/src/main/resources/db/migration/postgresql/V2.15.0.20231213141052__create_table_tools_and_permissions.sql new file mode 100644 index 000000000..765948bd7 --- /dev/null +++ b/src/main/resources/db/migration/postgresql/V2.15.0.20231213141052__create_table_tools_and_permissions.sql @@ -0,0 +1,48 @@ +CREATE TABLE IF NOT EXISTS ${ohdsiSchema}.tool ( + id BIGINT, + name VARCHAR(255) NOT NULL, + url VARCHAR(1000) NOT NULL, + description VARCHAR(1000), + is_enabled BOOLEAN, + created_by_id INTEGER, + modified_by_id INTEGER, + created_date TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT (now()), + modified_date TIMESTAMP WITH TIME ZONE +); + +ALTER TABLE ${ohdsiSchema}.tool ADD CONSTRAINT PK_tool PRIMARY KEY (id); + +ALTER TABLE ${ohdsiSchema}.tool ADD CONSTRAINT fk_tool_ser_user_creator FOREIGN KEY (created_by_id) REFERENCES ${ohdsiSchema}.sec_user(id); +ALTER TABLE ${ohdsiSchema}.tool ADD CONSTRAINT fk_tool_ser_user_updater FOREIGN KEY (modified_by_id) REFERENCES ${ohdsiSchema}.sec_user(id); + +CREATE SEQUENCE ${ohdsiSchema}.tool_seq START WITH 1 INCREMENT BY 1 MAXVALUE 9223372036854775807 NO CYCLE; + +INSERT INTO ${ohdsiSchema}.sec_permission(id, value, description) VALUES + (nextval('${ohdsiSchema}.sec_permission_id_seq'), 'tool:post', 'Create Tool'); +INSERT INTO ${ohdsiSchema}.sec_permission(id, value, description) VALUES + (nextval('${ohdsiSchema}.sec_permission_id_seq'), 'tool:put', 'Update Tool'); +INSERT INTO ${ohdsiSchema}.sec_permission(id, value, description) VALUES + (nextval('${ohdsiSchema}.sec_permission_id_seq'), 'tool:get', 'List Tools'); +INSERT INTO ${ohdsiSchema}.sec_permission(id, value, description) VALUES + (nextval('${ohdsiSchema}.sec_permission_id_seq'), 'tool:*:get', 'View Tool'); +INSERT INTO ${ohdsiSchema}.sec_permission(id, value, description) VALUES + (nextval('${ohdsiSchema}.sec_permission_id_seq'), 'tool:*:delete', 'Delete Tool'); + +INSERT INTO ${ohdsiSchema}.sec_role_permission(id, role_id, permission_id) +SELECT nextval('${ohdsiSchema}.sec_role_permission_sequence'), sr.id, sp.id +FROM ${ohdsiSchema}.sec_permission SP, ${ohdsiSchema}.sec_role sr +WHERE sp.value IN ( + 'tool:post', + 'tool:put', + 'tool:get', + 'tool:*:get', + 'tool:*:delete' + ) AND sr.name IN ('admin'); + +INSERT INTO ${ohdsiSchema}.sec_role_permission(id, role_id, permission_id) +SELECT nextval('${ohdsiSchema}.sec_role_permission_sequence'), sr.id, sp.id +FROM ${ohdsiSchema}.sec_permission SP, ${ohdsiSchema}.sec_role sr +WHERE sp.value IN ( + 'tool:get', + 'tool:*:get' + ) AND sr.name IN ('Atlas users'); diff --git a/src/main/resources/i18n/messages_en.json b/src/main/resources/i18n/messages_en.json index 75b843546..57b15533c 100644 --- a/src/main/resources/i18n/messages_en.json +++ b/src/main/resources/i18n/messages_en.json @@ -116,7 +116,26 @@ "incidenceRate": "Incidence Rate", "ple": "Population Level Effect Estimation", "plp": "Patient Level Prediction", - "reusable": "Reusable" + "reusable": "Reusable", + "addTools": "Add Tool", + "url": "URL", + "apply": "Apply", + "tools": "Tools" + }, + "tool":{ + "name": "* Name", + "url": "* URL", + "nameValidMess": "Tool name cannot be empty.", + "urlValidMess": "Tool url cannot be empty.", + "loading": "Loading tools", + "error": { + "list": "Unable to retrieve the list data at this time; please try again later.", + "create": "Failed to create Tool.", + "update": "Failed to update Tool.", + "delete": "Failed to delete Tool." + }, + "toolDisabled": "Tool disabled", + "toolEnabled": "Tool enabled" }, "commonErrors": { "noSources": "The current WebAPI has no sources defined.
Please add one or more on configuration page.", @@ -139,7 +158,8 @@ "prediction": "Prediction", "profiles": "Profiles", "search": "Search", - "tagging": "Tagging" + "tagging": "Tagging", + "tools": "Tools" }, "options": { "after": "After", diff --git a/src/main/resources/i18n/messages_ko.json b/src/main/resources/i18n/messages_ko.json index 9ec971244..f203bc3d7 100644 --- a/src/main/resources/i18n/messages_ko.json +++ b/src/main/resources/i18n/messages_ko.json @@ -116,7 +116,26 @@ "incidenceRate": "Incidence Rate", "ple": "Population Level Effect Estimation", "plp": "Patient Level Prediction", - "reusable": "Reusable" + "reusable": "Reusable", + "addTools": "도구 추가", + "url": "URL", + "apply": "적용하다", + "tools": "도구" + }, + "tool":{ + "name": "* 이름", + "url": "* URL", + "nameValidMess": "도구 이름은 비워둘 수 없습니다.", + "urlValidMess": "도구 URL은 비워둘 수 없습니다.", + "loading": "도구 로딩", + "error": { + "list": "현재 목록 데이터를 가져올 수 없습니다. 나중에 다시 시도해주세요.", + "create": "도구를 만들지 못했습니다.", + "update": "도구 업데이트에 실패했습니다.", + "delete": "도구 삭제에 실패했습니다." + }, + "toolDisabled": "도구 비활성화됨", + "toolEnabled": "도구 활성화됨" }, "commonErrors": { "noSources": "현재 WebAPI에 정의된 소스가 없습니다.
해당 페이지에서 하나 이상을 추가하십시오 configuration page.", @@ -139,7 +158,8 @@ "prediction": "예측", "profiles": "프로필", "search": "검색", - "tagging": "Tagging" + "tagging": "Tagging", + "tools": "도구" }, "options": { "after": "다음", diff --git a/src/main/resources/i18n/messages_ru.json b/src/main/resources/i18n/messages_ru.json index aae125875..946e6a49e 100644 --- a/src/main/resources/i18n/messages_ru.json +++ b/src/main/resources/i18n/messages_ru.json @@ -116,7 +116,26 @@ "incidenceRate": "Инцидентность", "ple": "Оценочный анализ", "plp": "Прогнозирование", - "reusable": "Сниппет" + "reusable": "Сниппет", + "addTools": "Добавить инструмент", + "url": "URL-адрес", + "apply": "Применять", + "tools": "Инструменты" + }, + "tool":{ + "name": "* Имя", + "url": "* URL", + "nameValidMess": "Имя инструмента не может быть пустым.", + "urlValidMess": "URL инструмента не может быть пустым.", + "loading": "Загрузка инструментов", + "error": { + "list": "Невозможно получить список данных в данный момент; попробуйте снова позже.", + "create": "Не удалось создать инструмент.", + "update": "Не удалось обновить инструмент.", + "delete": "Не удалось удалить инструмент." + }, + "toolDisabled": "Инструмент отключен", + "toolEnabled": "Инструмент включен" }, "commonErrors": { "browserWarning": "Пожалуйста, обратите внимание, что официально поддерживается только браузер Chrome версии не ниже v.63", @@ -139,7 +158,8 @@ "jobs": "Задания", "configuration": "Конфигурация", "feedback": "Обратная связь", - "tagging": "Теггинг" + "tagging": "Теггинг", + "tools": "Инструменты" }, "options": { "atMost": "В большинстве", diff --git a/src/main/resources/i18n/messages_zh.json b/src/main/resources/i18n/messages_zh.json index 207cfcd70..6259cb5c0 100644 --- a/src/main/resources/i18n/messages_zh.json +++ b/src/main/resources/i18n/messages_zh.json @@ -116,7 +116,26 @@ "incidenceRate": "Incidence Rate", "ple": "Population Level Effect Estimation", "plp": "Patient Level Prediction", - "reusable": "Reusable" + "reusable": "Reusable", + "addTools": "添加工具", + "url": "网址", + "apply": "申请", + "tools": "工具" + }, + "tool":{ + "name": "* 名称", + "url": "* URL", + "nameValidMess": "工具名称不能为空。", + "urlValidMess": "工具网址不能为空。", + "loading": "装载工具", + "error": { + "list": "当前无法检索列表数据,请稍后重试。", + "create": "创建工具失败。", + "update": "更新工具失败。", + "delete": "删除工具失败。" + }, + "toolDisabled":"工具已禁用", + "toolEnabled": "工具已启用" }, "commonErrors": { "noSources": "当前的WebAPI尚未定义源。
请在配置页上添加一个或多个。", @@ -139,7 +158,8 @@ "prediction": "预测", "profiles": "数据概要", "search": "搜索", - "tagging": "Tagging" + "tagging": "Tagging", + "tools": "工具" }, "options": { "after": "在 之后",