Add tool upgrade api

This commit is contained in:
2024-01-30 19:24:44 +08:00
parent 835fa7c27f
commit 7aca58f1de
10 changed files with 226 additions and 58 deletions

View File

@@ -2,14 +2,12 @@ package top.fatweb.oxygen.api.controller.tool
import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.Operation
import jakarta.validation.Valid import jakarta.validation.Valid
import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.*
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import top.fatweb.oxygen.api.annotation.BaseController import top.fatweb.oxygen.api.annotation.BaseController
import top.fatweb.oxygen.api.entity.common.ResponseCode import top.fatweb.oxygen.api.entity.common.ResponseCode
import top.fatweb.oxygen.api.entity.common.ResponseResult import top.fatweb.oxygen.api.entity.common.ResponseResult
import top.fatweb.oxygen.api.param.tool.ToolCreateParam import top.fatweb.oxygen.api.param.tool.ToolCreateParam
import top.fatweb.oxygen.api.param.tool.ToolUpgradeParam
import top.fatweb.oxygen.api.service.tool.IEditService import top.fatweb.oxygen.api.service.tool.IEditService
import top.fatweb.oxygen.api.vo.tool.ToolCategoryVo import top.fatweb.oxygen.api.vo.tool.ToolCategoryVo
import top.fatweb.oxygen.api.vo.tool.ToolTemplateVo import top.fatweb.oxygen.api.vo.tool.ToolTemplateVo
@@ -79,6 +77,17 @@ class EditController(
fun create(@RequestBody @Valid toolCreateParam: ToolCreateParam): ResponseResult<ToolVo> = fun create(@RequestBody @Valid toolCreateParam: ToolCreateParam): ResponseResult<ToolVo> =
ResponseResult.databaseSuccess(ResponseCode.DATABASE_INSERT_SUCCESS, data = editService.create(toolCreateParam)) ResponseResult.databaseSuccess(ResponseCode.DATABASE_INSERT_SUCCESS, data = editService.create(toolCreateParam))
/**
* Upgrade tool
*
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Operation(summary = "更新工具")
@PatchMapping
fun upgrade(@RequestBody @Valid toolUpgradeParam: ToolUpgradeParam): ResponseResult<ToolVo> =
ResponseResult.databaseSuccess(ResponseCode.DATABASE_UPDATE_SUCCESS, data = editService.upgrade(toolUpgradeParam))
/** /**
* Get personal tool * Get personal tool
* *
@@ -103,4 +112,16 @@ class EditController(
ResponseCode.DATABASE_SELECT_SUCCESS, ResponseCode.DATABASE_SELECT_SUCCESS,
data = editService.detail(username, toolId, ver) data = editService.detail(username, toolId, ver)
) )
/**
* Delete tool
*
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Operation(summary = "删除工具")
@DeleteMapping("/{id}")
fun delete(@PathVariable id: Long): ResponseResult<Nothing> =
if (editService.delete(id)) ResponseResult.databaseSuccess(ResponseCode.DATABASE_DELETE_SUCCESS)
else ResponseResult.databaseFail(ResponseCode.DATABASE_DELETE_FAILED)
} }

View File

@@ -31,6 +31,14 @@ enum class BusinessCode(val code: Int) {
*/ */
DATABASE(300), DATABASE(300),
/**
* Tool
*
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
TOOL(400),
/** /**
* Avatar API * Avatar API
* *

View File

@@ -58,6 +58,8 @@ enum class ResponseCode(val code: Int) {
DATABASE_DUPLICATE_KEY(BusinessCode.DATABASE, 51), DATABASE_DUPLICATE_KEY(BusinessCode.DATABASE, 51),
DATABASE_NO_RECORD_FOUND(BusinessCode.DATABASE, 52), DATABASE_NO_RECORD_FOUND(BusinessCode.DATABASE, 52),
TOOL_ILLEGAL_VERSION(BusinessCode.TOOL, 50),
API_AVATAR_SUCCESS(BusinessCode.API_AVATAR, 0), API_AVATAR_SUCCESS(BusinessCode.API_AVATAR, 0),
API_AVATAR_ERROR(BusinessCode.API_AVATAR, 50); API_AVATAR_ERROR(BusinessCode.API_AVATAR, 50);

View File

@@ -0,0 +1,3 @@
package top.fatweb.oxygen.api.exception
class IllegalVersionException : RuntimeException("Illegal Version")

View File

@@ -211,6 +211,12 @@ class ExceptionHandler {
ResponseResult.fail(ResponseCode.DATABASE_EXECUTE_ERROR, e.localizedMessage, null) ResponseResult.fail(ResponseCode.DATABASE_EXECUTE_ERROR, e.localizedMessage, null)
} }
/* Tool */
is IllegalVersionException -> {
logger.debug(e.localizedMessage, e)
ResponseResult.fail(ResponseCode.TOOL_ILLEGAL_VERSION, e.localizedMessage, null)
}
/* Other */ /* Other */
is MatchSensitiveWordException -> { is MatchSensitiveWordException -> {
logger.debug(e.localizedMessage, e) logger.debug(e.localizedMessage, e)

View File

@@ -0,0 +1,39 @@
package top.fatweb.oxygen.api.param.tool
import io.swagger.v3.oas.annotations.media.Schema
import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.Pattern
/**
* Upgrade tool parameters
*
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Schema(description = "更新工具请求参数")
data class ToolUpgradeParam(
/**
* Tool ID
*
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Schema(description = "工具唯一 ID", required = true, example = "tool_a")
@field: NotBlank(message = "ToolId can not be blank")
@field: Pattern(
regexp = "^[a-zA-Z-_][0-9a-zA-Z-_]{2,19}\$",
message = "Ver can only match '^[a-zA-Z-_][0-9a-zA-Z-_]{2,19}\$'"
)
val toolId: String?,
/**
* Version
*
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Schema(description = "版本", required = true, example = "1.0.3")
@field: NotBlank(message = "Ver can not be blank")
@field: Pattern(regexp = "^\\d+\\.\\d+\\.\\d+\$", message = "Ver can only match '<number>.<number>.<number>'")
val ver: String?
)

View File

@@ -99,7 +99,7 @@ class AuthenticationServiceImpl(
@Transactional @Transactional
override fun resend() { override fun resend() {
val user = userService.getById(WebUtil.getLoginUserId()) ?: throw AccessDeniedException("Access Denied") val user = userService.getById(WebUtil.getLoginUserId())
user.verify ?: throw NoVerificationRequiredException() user.verify ?: throw NoVerificationRequiredException()
@@ -124,7 +124,7 @@ class AuthenticationServiceImpl(
@EventLogRecord(EventLog.Event.VERIFY) @EventLogRecord(EventLog.Event.VERIFY)
@Transactional @Transactional
override fun verify(verifyParam: VerifyParam) { override fun verify(verifyParam: VerifyParam) {
val user = userService.getById(WebUtil.getLoginUserId()) ?: throw AccessDeniedException("Access Denied") val user = userService.getById(WebUtil.getLoginUserId())
user.verify ?: throw NoVerificationRequiredException() user.verify ?: throw NoVerificationRequiredException()
if (LocalDateTime.ofInstant(Instant.ofEpochMilli(user.verify!!.split("-").first().toLong()), ZoneOffset.UTC) if (LocalDateTime.ofInstant(Instant.ofEpochMilli(user.verify!!.split("-").first().toLong()), ZoneOffset.UTC)
.isBefore(LocalDateTime.now(ZoneOffset.UTC).minusHours(2)) || user.verify != verifyParam.code .isBefore(LocalDateTime.now(ZoneOffset.UTC).minusHours(2)) || user.verify != verifyParam.code

View File

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.extension.service.IService
import top.fatweb.oxygen.api.entity.tool.Tool import top.fatweb.oxygen.api.entity.tool.Tool
import top.fatweb.oxygen.api.param.tool.ToolCreateParam import top.fatweb.oxygen.api.param.tool.ToolCreateParam
import top.fatweb.oxygen.api.param.tool.ToolUpdateParam import top.fatweb.oxygen.api.param.tool.ToolUpdateParam
import top.fatweb.oxygen.api.param.tool.ToolUpgradeParam
import top.fatweb.oxygen.api.vo.tool.ToolCategoryVo import top.fatweb.oxygen.api.vo.tool.ToolCategoryVo
import top.fatweb.oxygen.api.vo.tool.ToolTemplateVo import top.fatweb.oxygen.api.vo.tool.ToolTemplateVo
import top.fatweb.oxygen.api.vo.tool.ToolVo import top.fatweb.oxygen.api.vo.tool.ToolVo
@@ -51,6 +52,7 @@ interface IEditService : IService<Tool> {
/** /**
* Get tool by ID * Get tool by ID
* *
* @param
* @author FatttSnake, fatttsnake@gmail.com * @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0 * @since 1.0.0
*/ */
@@ -64,6 +66,16 @@ interface IEditService : IService<Tool> {
*/ */
fun create(toolCreateParam: ToolCreateParam): ToolVo fun create(toolCreateParam: ToolCreateParam): ToolVo
/**
* Upgrade tool
*
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
* @see ToolUpgradeParam
* @see ToolVo
*/
fun upgrade(toolUpgradeParam: ToolUpgradeParam): ToolVo
/** /**
* Update tool * Update tool
* *
@@ -87,4 +99,12 @@ interface IEditService : IService<Tool> {
* @since 1.0.0 * @since 1.0.0
*/ */
fun detail(username: String, toolId: String, ver: String): ToolVo fun detail(username: String, toolId: String, ver: String): ToolVo
/**
* Delete tool
*
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
fun delete(id: Long): Boolean
} }

View File

@@ -2,17 +2,19 @@ package top.fatweb.oxygen.api.service.tool.impl
import com.baomidou.mybatisplus.extension.kotlin.KtQueryWrapper import com.baomidou.mybatisplus.extension.kotlin.KtQueryWrapper
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl
import org.springframework.dao.DuplicateKeyException
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional import org.springframework.transaction.annotation.Transactional
import top.fatweb.oxygen.api.converter.tool.ToolCategoryConverter import top.fatweb.oxygen.api.converter.tool.ToolCategoryConverter
import top.fatweb.oxygen.api.converter.tool.ToolConverter import top.fatweb.oxygen.api.converter.tool.ToolConverter
import top.fatweb.oxygen.api.converter.tool.ToolTemplateConverter import top.fatweb.oxygen.api.converter.tool.ToolTemplateConverter
import top.fatweb.oxygen.api.entity.tool.* import top.fatweb.oxygen.api.entity.tool.*
import top.fatweb.oxygen.api.exception.IllegalVersionException
import top.fatweb.oxygen.api.exception.NoRecordFoundException import top.fatweb.oxygen.api.exception.NoRecordFoundException
import top.fatweb.oxygen.api.exception.UserNotFoundException
import top.fatweb.oxygen.api.mapper.tool.EditMapper import top.fatweb.oxygen.api.mapper.tool.EditMapper
import top.fatweb.oxygen.api.param.tool.ToolCreateParam import top.fatweb.oxygen.api.param.tool.ToolCreateParam
import top.fatweb.oxygen.api.param.tool.ToolUpdateParam import top.fatweb.oxygen.api.param.tool.ToolUpdateParam
import top.fatweb.oxygen.api.param.tool.ToolUpgradeParam
import top.fatweb.oxygen.api.service.tool.* import top.fatweb.oxygen.api.service.tool.*
import top.fatweb.oxygen.api.util.WebUtil import top.fatweb.oxygen.api.util.WebUtil
import top.fatweb.oxygen.api.vo.tool.ToolCategoryVo import top.fatweb.oxygen.api.vo.tool.ToolCategoryVo
@@ -49,11 +51,15 @@ class EditServiceImpl(
.map(ToolCategoryConverter::toolCategoryToToolCategoryVo) .map(ToolCategoryConverter::toolCategoryToToolCategoryVo)
override fun getOne(id: Long): ToolVo = override fun getOne(id: Long): ToolVo =
baseMapper.selectOne(id, WebUtil.getLoginUserId() ?: throw UserNotFoundException()) baseMapper.selectOne(id, WebUtil.getLoginUserId()!!)
?.let(ToolConverter::toolToToolVo) ?: throw NoRecordFoundException() ?.let(ToolConverter::toolToToolVo) ?: throw NoRecordFoundException()
@Transactional @Transactional
override fun create(toolCreateParam: ToolCreateParam): ToolVo { override fun create(toolCreateParam: ToolCreateParam): ToolVo {
baseMapper.selectOne(
KtQueryWrapper(Tool()).eq(Tool::toolId, toolCreateParam.toolId!!)
.eq(Tool::authorId, WebUtil.getLoginUserId()!!)
)?.let { throw DuplicateKeyException("Duplicate Key") }
val template = this.getTemplate(toolCreateParam.templateId!!) val template = this.getTemplate(toolCreateParam.templateId!!)
val newSource = ToolData().apply { data = template.source!!.data } val newSource = ToolData().apply { data = template.source!!.data }
val newDist = ToolData().apply { data = "" } val newDist = ToolData().apply { data = "" }
@@ -65,11 +71,12 @@ class EditServiceImpl(
icon = toolCreateParam.icon icon = toolCreateParam.icon
description = toolCreateParam.description description = toolCreateParam.description
baseId = template.base!!.id baseId = template.base!!.id
authorId = WebUtil.getLoginUserId() ?: throw UserNotFoundException() authorId = WebUtil.getLoginUserId()!!
ver = toolCreateParam.ver!!.split(".").map(String::toLong).joinToString(".") ver = toolCreateParam.ver!!.split(".").map(String::toLong).joinToString(".")
keywords = toolCreateParam.keywords keywords = toolCreateParam.keywords
sourceId = newSource.id sourceId = newSource.id
distId = newDist.id distId = newDist.id
entryPoint = template.entryPoint
} }
this.save(tool) this.save(tool)
@@ -85,13 +92,60 @@ class EditServiceImpl(
return this.getOne(tool.id!!) return this.getOne(tool.id!!)
} }
@Transactional
override fun upgrade(toolUpgradeParam: ToolUpgradeParam): ToolVo {
val originalTool = this.detail("!", toolUpgradeParam.toolId!!, "latest")
val originalVersion = originalTool.ver!!
if (originalVersion.split(".").map(String::toLong).joinToString(".") == toolUpgradeParam.ver!!.split(".")
.map(String::toLong).joinToString(".")
) {
throw IllegalVersionException()
}
originalVersion.split(".").forEachIndexed { index, s ->
if ((toolUpgradeParam.ver.split(".")[index].toLong() < s.toLong())) {
throw IllegalVersionException()
}
}
val newSource = ToolData().apply { data = originalTool.source!!.data }
val newDist = ToolData().apply { data = "" }
toolDataService.saveBatch(listOf(newSource, newDist))
val tool = Tool().apply {
name = originalTool.name!!
toolId = originalTool.toolId
icon = originalTool.icon
description = originalTool.description
baseId = originalTool.base!!.id
authorId = WebUtil.getLoginUserId()!!
ver = toolUpgradeParam.ver.split(".").map(String::toLong).joinToString(".")
keywords = originalTool.keywords
sourceId = newSource.id
distId = newDist.id
entryPoint = originalTool.entryPoint
}
this.save(tool)
originalTool.categories!!.forEach {
toolCategoryService.getById(it.id) ?: throw NoRecordFoundException()
rToolCategoryService.save(RToolCategory().apply {
toolId = tool.id
categoryId = it.id
})
}
return this.getOne(tool.id!!)
}
@Transactional @Transactional
override fun update(toolUpdateParam: ToolUpdateParam): ToolVo { override fun update(toolUpdateParam: ToolUpdateParam): ToolVo {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
override fun get(): List<ToolVo> = override fun get(): List<ToolVo> =
baseMapper.selectPersonal(WebUtil.getLoginUserId() ?: throw UserNotFoundException()) baseMapper.selectPersonal(WebUtil.getLoginUserId()!!)
.map(ToolConverter::toolToToolVo) .map(ToolConverter::toolToToolVo)
override fun detail(username: String, toolId: String, ver: String): ToolVo { override fun detail(username: String, toolId: String, ver: String): ToolVo {
@@ -102,4 +156,16 @@ class EditServiceImpl(
return baseMapper.detail(username, toolId, ver, WebUtil.getLoginUsername())?.let(ToolConverter::toolToToolVo) return baseMapper.detail(username, toolId, ver, WebUtil.getLoginUsername())?.let(ToolConverter::toolToToolVo)
?: throw NoRecordFoundException() ?: throw NoRecordFoundException()
} }
@Transactional
override fun delete(id: Long): Boolean {
val tool = baseMapper.selectOne(
KtQueryWrapper(Tool()).eq(Tool::id, id)
.eq(Tool::authorId, WebUtil.getLoginUserId()!!)
) ?: throw NoRecordFoundException()
toolDataService.removeBatchByIds(listOf(tool.sourceId, tool.distId))
rToolCategoryService.remove(KtQueryWrapper(RToolCategory()).eq(RToolCategory::toolId, tool.id))
return this.removeById(id)
}
} }

View File

@@ -31,50 +31,50 @@
</select> </select>
<select id="selectOne" resultMap="toolWithDataMap"> <select id="selectOne" resultMap="toolWithDataMap">
select t_b_tool_main.id as tool_id, select t_b_tool_main.id as tool_id,
t_b_tool_main.name as tool_name, t_b_tool_main.name as tool_name,
t_b_tool_main.tool_id as tool_tool_id, t_b_tool_main.tool_id as tool_tool_id,
t_b_tool_main.icon as tool_icon, t_b_tool_main.icon as tool_icon,
t_b_tool_main.description as tool_description, t_b_tool_main.description as tool_description,
t_b_tool_main.base_id as tool_base_id, t_b_tool_main.base_id as tool_base_id,
t_b_tool_main.author_id as tool_author_id, t_b_tool_main.author_id as tool_author_id,
t_b_tool_main.ver as tool_ver, t_b_tool_main.ver as tool_ver,
t_b_tool_main.keywords as tool_keywords, t_b_tool_main.keywords as tool_keywords,
t_b_tool_main.source_id as tool_source_id, t_b_tool_main.source_id as tool_source_id,
t_b_tool_main.dist_id as tool_dist_id, t_b_tool_main.dist_id as tool_dist_id,
t_b_tool_main.entry_point as tool_entry_point, t_b_tool_main.entry_point as tool_entry_point,
t_b_tool_main.publish as tool_publish, t_b_tool_main.publish as tool_publish,
t_b_tool_main.review as tool_review, t_b_tool_main.review as tool_review,
t_b_tool_main.create_time as tool_create_time, t_b_tool_main.create_time as tool_create_time,
t_b_tool_main.update_time as tool_update_time, t_b_tool_main.update_time as tool_update_time,
t_b_tool_main.deleted as tool_deleted, t_b_tool_main.deleted as tool_deleted,
t_b_tool_main.version as tool_version, t_b_tool_main.version as tool_version,
tsu.id as user_id, tsu.id as user_id,
tsu.username as user_username, tsu.username as user_username,
tsui.id as user_info_id, tsui.id as user_info_id,
tsui.nickname as user_info_nickname, tsui.nickname as user_info_nickname,
tsui.avatar as user_info_avatar, tsui.avatar as user_info_avatar,
tsui.email as user_info_email, tsui.email as user_info_email,
tbtb.name as tool_base_name, tbtb.name as tool_base_name,
tbtb.dist_id as tool_base_dist_id, tbtb.dist_id as tool_base_dist_id,
tbtbd.data as tool_base_dist_data, tbtbd.data as tool_base_dist_data,
tbts.data as tool_source_data, tbts.data as tool_source_data,
tbts.create_time as tool_source_create_time, tbts.create_time as tool_source_create_time,
tbts.update_time as tool_source_update_time, tbts.update_time as tool_source_update_time,
tbts.deleted as tool_source_deleted, tbts.deleted as tool_source_deleted,
tbts.version as tool_source_version, tbts.version as tool_source_version,
tbtd.data as tool_dist_data, tbtd.data as tool_dist_data,
tbtd.create_time as tool_dist_create_time, tbtd.create_time as tool_dist_create_time,
tbtd.update_time as tool_dist_update_time, tbtd.update_time as tool_dist_update_time,
tbtd.deleted as tool_dist_deleted, tbtd.deleted as tool_dist_deleted,
tbtd.version as tool_dist_version, tbtd.version as tool_dist_version,
tbtc.id as tool_category_id, tbtc.id as tool_category_id,
tbtc.name as tool_category_name, tbtc.name as tool_category_name,
tbtc.enable as tool_category_enable, tbtc.enable as tool_category_enable,
tbtc.create_time as tool_category_create_time, tbtc.create_time as tool_category_create_time,
tbtc.update_time as tool_category_update_time, tbtc.update_time as tool_category_update_time,
tbtc.deleted as tool_category_deleted, tbtc.deleted as tool_category_deleted,
tbtc.version as tool_category_version tbtc.version as tool_category_version
from t_b_tool_main from t_b_tool_main
left join (select * from t_s_user where deleted = 0) as tsu on tsu.id = t_b_tool_main.author_id left join (select * from t_s_user where deleted = 0) as tsu on tsu.id = t_b_tool_main.author_id
left join (select * from t_s_user_info where deleted = 0) as tsui left join (select * from t_s_user_info where deleted = 0) as tsui
@@ -125,7 +125,7 @@
on tbtc.id = trtmc.category_id on tbtc.id = trtmc.category_id
where t_b_tool_main.deleted = 0 where t_b_tool_main.deleted = 0
and t_b_tool_main.author_id = #{userId} and t_b_tool_main.author_id = #{userId}
order by t_b_tool_main.tool_id desc order by t_b_tool_main.id desc
</select> </select>
<select id="detail" resultMap="toolWithDataMap"> <select id="detail" resultMap="toolWithDataMap">
@@ -208,9 +208,11 @@
</choose> </choose>
</where> </where>
order by t_b_tool_main.id desc order by t_b_tool_main.id desc
limit 1
</select> </select>
<resultMap id="toolTemplateWithBaseDataMap" type="toolTemplate" extends="top.fatweb.oxygen.api.mapper.tool.ToolTemplateMapper.toolTemplateWithDataMap"> <resultMap id="toolTemplateWithBaseDataMap" type="toolTemplate"
extends="top.fatweb.oxygen.api.mapper.tool.ToolTemplateMapper.toolTemplateWithDataMap">
<association property="base"> <association property="base">
<id property="id" column="tool_template_base_id"/> <id property="id" column="tool_template_base_id"/>
<result property="name" column="tool_template_base_name"/> <result property="name" column="tool_template_base_name"/>
@@ -236,8 +238,9 @@
<result property="createTime" column="tool_create_time"/> <result property="createTime" column="tool_create_time"/>
<result property="deleted" column="tool_deleted"/> <result property="deleted" column="tool_deleted"/>
<result property="version" column="tool_version"/> <result property="version" column="tool_version"/>
<collection property="keywords" ofType="string" column="tool_keywords"/> <result property="keywords" column="tool_keywords" typeHandler="com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler"/>
<collection property="categories" resultMap="top.fatweb.oxygen.api.mapper.tool.ToolCategoryMapper.toolCategoryMap"/> <collection property="categories"
resultMap="top.fatweb.oxygen.api.mapper.tool.ToolCategoryMapper.toolCategoryMap"/>
</resultMap> </resultMap>
<resultMap id="toolWithAuthor" type="tool" extends="toolMap"> <resultMap id="toolWithAuthor" type="tool" extends="toolMap">