이전 게시물에서 플러그인을 사용하여 SCM Manager의 기본 기능을 확장하는 이유와 방법, 기본 아키텍처의 모양, 개발한 플러그인을 커뮤니티와 공유하는 방법에 대해 설명했습니다. 이 게시물에서는 새 플러그인의 백엔드를 개발하여 처음부터 플러그인을 만드는 방법을 보여줍니다.
플러그인 아이디어
얼마 전에 SCM Manager 사용자는 SCM Manager에 대한 자체 링크(예: 임프린트 또는 관련 문제 추적 시스템)를 추가하고 싶다고 밝혔습니다. 이러한 링크는 SCM 관리자의 모든 페이지에서 구성 및 액세스할 수 있어야 합니다. 그래서 우리는 SCM 관리자용 플러그인을 만드는 방법의 예로 사용자 정의 링크 플러그인을 작성하기로 결정했습니다. 데이터를 유지하기 위한 백엔드 저장소, REST API 리소스, 관리자 패널의 프런트엔드 구성 페이지 및 SCM 관리자의 바닥글에 링크를 표시하는 확장으로 구성됩니다.
플러그인용 부트스트랩
먼저 플러그인을 설정해야 합니다. create-plugin.scm-manager.org를 사용하고 필요한 정보를 입력합니다.
제출 후 시작해야 하는 플러그인의 기본 구조가 포함된 패키지를 받게 됩니다.
일반 정보
- SCM Manager는 MIT 라이선스에 따라 라이선스가 부여됩니다. 따라서 거의 모든 파일에는 유효한 라이선스 헤더가 포함되어야 합니다. 라이센스 헤더와 관련된 오류가 발생하면 다음을 시도하십시오.
./gradlew licenseFormat
자동으로 수정합니다. - 구현하는 논리에 대해 단위 테스트를 작성하는 것이 좋습니다. 단위 테스트를 만드는 데 문제가 있거나 모범 사례를 찾고 있는 경우 여기 또는 다른 SCM 관리자 플러그인을 확인할 수 있습니다.
IDE 설정
플러그인 개발을 위해 다음 설정과 함께 IntelliJ IDEA를 사용합니다. IDE 구성
특히 포인트 Lizenzen
그리고 Formatierung
실제 작업에서 빠르게 주의를 분산시킬 수 있으므로 가능한 한 많이 자동화해야 합니다.
데이터 지속성
코딩할 시간입니다! 단일 사용자 지정 링크 엔터티를 나타내는 데이터 개체를 만듭니다. 우리의 경우에 대한 두 개의 필드 Name
그리고 Url
충분한. 이 객체는 저장하고 싶기 때문에 직렬화 가능해야 합니다. getter, setter 및 두 개의 생성자를 추가해 보겠습니다.
팁: Lombok 주석을 좋아할 수도 있습니다. @Getter
또는 @NoArgsConstructor
사용.
public class CustomLink {
private String name;
private String url;
CustomLink() {
}
public CustomLink(String name, String url) {
this.name = name;
this.url = url;
}
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;
}
}
데이터가 어떻게 보이는지 알고 있으므로 이러한 사용자 지정 링크를 저장할 저장소를 만들 수 있습니다. SCM Manager는 데이터 스토리지, 구성 스토리지 및 Blob 스토리지와 같은 다양한 스토리지 유형을 제공합니다. 사용 사례에서는 ConfigurationEntryStore를 선택했습니다. 이 상점은 여러 사용자 지정 링크를 독립적으로 관리할 수 있습니다. 저희 매장은 3가지 퍼블릭 방식으로 구성되어 있습니다. getAllLinks
, addLink
그리고 deleteLink
.
package com.cloudogu.customlinks;
import com.google.common.annotations.VisibleForTesting;
import sonia.scm.store.ConfigurationEntryStore;
import sonia.scm.store.ConfigurationEntryStoreFactory;
import javax.inject.Inject;
import java.util.Collection;
public class CustomLinkConfigStore {
@VisibleForTesting
public static final String STORE_NAME = "custom-links";
private final ConfigurationEntryStoreFactory configurationEntryStoreFactory;
@Inject
public CustomLinkConfigStore(ConfigurationEntryStoreFactory configurationEntryStoreFactory) {
this.configurationEntryStoreFactory = configurationEntryStoreFactory;
}
public Collection<CustomLink> getAllLinks() {
return getStore().getAll().values();
}
public void addLink(String name, String url) {
//TODO Manage links permission
getStore().put(name, new CustomLink(name, url));
}
public void removeLink(String name) {
//TODO Manage links permission
getStore().remove(name);
}
private ConfigurationEntryStore<CustomLink> getStore() {
return configurationEntryStoreFactory.withType(CustomLink.class).withName(STORE_NAME).build();
}
}
이제 내부 SCM Manager 데이터 저장소에 이미 데이터를 유지할 수 있습니다. 플러그인에서 저장된 데이터는 다음과 같습니다.
<?xml version="1.0" ?>
<configuration type="config-entry">
<entry>
<key>Imprint</key>
<value>
<name>Imprint</name>
<url>https://scm-manager.org/imprint</url>
</value>
</entry>
</configuration>
권한
REST API로 전환하기 전에 권한을 살펴보겠습니다. SCM Manager는 Apache Shiro를 사용하여 권한을 처리합니다. 로그인한 각 사용자는 권한이 부여된 주체입니다. 사용 사례의 경우 모든 사용자가 로그인하지 않은 상태에서도 사용자 정의 링크를 볼 수 있기를 원합니다. 그러나 승인된 사용자만 사용자 정의 링크를 변경할 수 있습니다.
따라서 PermissionCheck 클래스를 만듭니다. 이 클래스는 새로운 권한을 소개합니다. manageCustomLinks
이를 위해 두 가지 시험을 제공합니다. Shiro에서 우리의 새로운 권한은 다음과 같습니다. configuration:manageCustomLinks
밖으로. 우리는 이러한 인증 확인을 통해 우리 가게의 쓰기 행위를 보호합니다. 승인되지 않은 사용자가 맞춤 링크를 수정하려고 시도하면 AuthorizationException
던져.
사용자는 이 권한을 어떻게 얻습니까? 그것은 그들에게 부여되어야 합니다. 그러나 이러한 일이 발생하기 전에 SCM 관리자에 새 권한을 도입해야 합니다. 이를 위해 하나를 만듭니다. permissions.xml
폴더에서 resources/META-INF/scm
.
<permissions>
<permission>
<value>configuration:manageCustomLinks</value>
</permission>
</permissions>
이 파일에서 새 권한을 가져오려면 서버를 한 번 다시 시작해야 합니다. 이 작업을 수행하기 전에 새 권한에 대한 번역을 입력할 수 있습니다. 자동으로 생성된 번역 키는 plugins.json 파일에 번역을 추가하여 대체할 수 있습니다.
REST API
SCM Manager는 레벨 3 REST API를 사용하고 하이퍼미디어 유형에 대한 HAL 사양에 의존합니다. 이러한 요구 사항을 충족하기 위해 SCM Manager는 많은 표준 코드를 줄이는 자체 라이브러리를 제공합니다. 사용자 지정 링크 엔터티에서 DTO(데이터 전송 개체)를 생성하여 시작하겠습니다. 데이터 클래스에서 자동으로 DTO 개체 클래스를 생성하기 위해 데이터 클래스에 배치되는 주석도 있습니다. 상상할 수 있듯이 @GenerateDto는 DTO 객체 클래스를 생성합니다. 우리는 우리 안에 있는 분야가 필요합니다. CustomLinkDto.java
에 포함된다 @Include
(com.cloudogu.conveyor.Include
) 표시합니다.
사용하려는 두 번째 SCM 관리자 관련 주석은 다음과 같습니다. @GenerateLinkBuilder
. 이렇게 하면 전체 REST API에 대한 링크 빌더를 포함하는 새 클래스가 생성됩니다(주석 기반). @Path
jax-rs에 의해). 특정 클래스 이름을 선택했습니다. RestAPI 앞으로의 예제를 이해하기 쉽게 만들기로 결정했습니다. HAL 사양을 준수해야 하므로 JSON 응답에 대한 링크를 추가할 때 이러한 링크 빌더가 필요합니다.
@GenerateDto
@GenerateLinkBuilder(className = "RestAPI")
public class CustomLink {
@Include
@NotEmpty
private String name;
@Include
@NotEmpty
private String url;
다음으로 스토어에 인터페이스하는 세 가지 메서드로 구성된 REST 리소스를 만듭니다. HTTP 동사와 미디어 유형 외에도 각 메서드에 OpenAPI 주석도 추가합니다. 이를 통해 OpenAPI 사양이 생성되고 REST API가 OpenAPI 플러그인에 적절하게 문서화됩니다. 우리 방법의 구현에서 getAllLinks
먼저 저장소에서 링크를 수집한 다음 DTO에 매핑하고 delete
-사용자에게 권한이 있는 경우 각 개별 엔터티에 연결합니다. 마지막으로 HalRepresentation 개체에 다음과 같이 래핑합니다. _embedded
전체에 대해 더 많은 링크를 추가합니다. Collection
추가했습니다.
일반적으로 거의 사용되지 않는 특별한 주석은 AllowAnonymousAccess
. 인증 필터를 무시하므로 이 REST 메서드에 대한 공용 액세스를 허용합니다. 사용 사례의 경우 엔드포인트에 필요합니다. @GET
세트.
@OpenAPIDefinition(tags = {
@Tag(name = "Custom Links", description = "Custom links plugin related endpoints")
})
@Path(CUSTOM_LINKS_CONFIG_PATH)
public class CustomLinksResource {
public static final String CUSTOM_LINKS_MEDIA_TYPE = VndMediaType.PREFIX + "custom-links" + VndMediaType.SUFFIX;
public static final String CUSTOM_LINKS_CONFIG_PATH = "v2/custom-links";
private final CustomLinkConfigStore configStore;
@Inject //javax.inject.Inject
CustomLinksResource(CustomLinkConfigStore configStore) {
this.configStore = configStore;
}
@GET
@Path("")
@Produces(CUSTOM_LINKS_MEDIA_TYPE)
@Operation(
summary = "Get all custom links",
description = "Returns all custom links.",
tags = "Custom Links",
operationId = "custom_links_get_all_links"
)
@ApiResponse(
responseCode = "200",
description = "success",
content = @Content(
mediaType = CUSTOM_LINKS_MEDIA_TYPE,
schema = @Schema(implementation = HalRepresentation.class)
)
)
@ApiResponse(
responseCode = "500",
description = "internal server error",
content = @Content(
mediaType = VndMediaType.ERROR_TYPE,
schema = @Schema(implementation = ErrorDto.class)
)
)
@AllowAnonymousAccess
public HalRepresentation getAllCustomLinks(@Context UriInfo uriInfo) {
RestAPI restAPI = new RestAPI(uriInfo);
Collection<CustomLink> customLinks = configStore.getAllLinks();
List<CustomLinkDto> linkDtos = mapCustomLinksToDtos(restAPI, customLinks);
return new HalRepresentation(createCollectionLinks(restAPI), Embedded.embedded("customLinks", linkDtos));
}
힌트: 클래스 RestAPI
주석을 통해 생성됩니다. 이 클래스를 사용할 수 있으려면 ./gradlew build
실행됩니다.
REST 리소스를 구현한 후 백엔드가 준비되었으며 이미 curl로 테스트할 수 있습니다. 예를 들어 curl http://localhost:8081/scm/api/v2/custom-links
. 그러나 SCM Manager 프런트엔드는 새로운 REST 끝점과 이를 사용하는 방법에 대해 정확히 어떻게 알 수 있습니까? 우리 원하다 프런트엔드 코드에 링크나 URL을 하드코딩하지 마세요.
농축기
정답은 농축기. 우리의 경우에는 보강자가 하나만 필요하지만 보강할 수 있는 개체가 여러 개 있습니다. SCM Manager에 액세스할 때 프런트엔드는 항상 색인 (http://localhost:8081/scm/api/v2). 인덱스가 무엇으로 구성되어 있는지에 따라 프런트엔드는 렌더링해야 하는 페이지, 탐색 지점 및 작업을 알고 있습니다. 즉, 프런트엔드의 모든 인증 확인은 존재하는 링크를 기반으로 합니다.
예: 사용자는 리포지토리를 만들 수 없습니다. 그는 SCM Manager에 로그인하고 인덱스를 가져옵니다. 색인에서 리포지토리를 검색하는 링크를 찾습니다. 따라서 “저장소” 링크는 사용자가 볼 수 있는 모든 저장소를 검색합니다. Repository Collection JSON 응답에서 다음과 같은 리포지토리를 찾습니다. _embedded
그리고 다시 _links
. 우리는 self
-링크 및 일부 페이지 링크가 있지만 없음 create
-링크. 이것은 프론트엔드가 Create Repository
버튼을 표시할 수 없습니다.
IndexDtoGenerator는 권한에 따라 각 사용자에 대한 인덱스를 생성합니다. IndexLinkEnricher를 생성하여 “사용자 지정 링크”로 이 인덱스를 보강합니다. 사용 사례에서는 인덱스에 두 개의 서로 다른 링크를 추가합니다. customLinks
모든 사용자가 사용할 수 있어야 하는 사용자 지정 링크를 검색하는 데 사용됩니다.customLinksConfig
로그인한 사용자가 관리자 페이지에서 사용자 지정 링크를 관리할 수 있는지 확인하는 데 사용됩니다.
@Extension // sonia.scm.plugin.Extension
@Enrich(Index.class) // sonia.scm.api.v2.resources.Enrich
public class IndexLinkEnricher implements HalEnricher {
private final Provider<ScmPathInfoStore> scmPathInfoStore;
@Inject
public IndexLinkEnricher(Provider<ScmPathInfoStore> scmPathInfoStore) {
this.scmPathInfoStore = scmPathInfoStore;
}
@Override
public void enrich(HalEnricherContext context, HalAppender appender) {
String link = new RestAPI(scmPathInfoStore.get().get().getApiRestUri()).customLinks().getAllCustomLinks().asString();
appender.appendLink("customLinks", link);
if (PermissionCheck.mayManageCustomLinks()) {
appender.appendLink("customLinksConfig", link);
}
}
}
결론
그게 다야. 맞춤형 링크 플러그인 백엔드가 준비되었습니다. curl을 통한 HTTP 요청으로 데이터가 예상대로 전달되고 있음을 알 수 있습니다. 프런트엔드 통합부터 시작할 수 있습니다. 기다릴 수 없다면 우리가 프런트엔드를 구축하는 방법을 이미 살펴볼 수 있습니다. 자세한 단계별 설명은 이 시리즈의 다음 블로그 게시물과 마지막 블로그 게시물에 있습니다.
▶ curl http://localhost:8081/scm/api/v2/custom-links | jq
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 165 100 165 0 0 23571 0 --:--:-- --:--:-- --:--:-- 23571
{
"_links": {
"self": {
"href": "http://localhost:8081/scm/api/v2/custom-links"
}
},
"_embedded": {
"customLinks": [
{
"name": "Imprint",
"url": "https://scm-manager.org/imprint"
}
]
}
}