diff --git a/.gitignore b/.gitignore
index 5183737..c765529 100644
--- a/.gitignore
+++ b/.gitignore
@@ -95,5 +95,7 @@ api-project-platform/src/main/java/org/opendevstack/apiservice/projectplatform/a
api-project-platform/src/main/java/org/opendevstack/apiservice/projectplatform/model
api-project/src/main/java/org/opendevstack/apiservice/project/api
api-project/src/main/java/org/opendevstack/apiservice/project/model
+api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/api
+api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/model
**/.openapi-generator
diff --git a/api-project-component-v0/openapi/api-project-component-v0.yaml b/api-project-component-v0/openapi/api-project-component-v0.yaml
new file mode 100644
index 0000000..dcd3912
--- /dev/null
+++ b/api-project-component-v0/openapi/api-project-component-v0.yaml
@@ -0,0 +1,166 @@
+openapi: 3.0.3
+info:
+ title: ODS API Server
+ description: API documentation for ODS (Open DevStack) API Service
+ contact:
+ name: ODS Team
+ version: v0.0.1
+servers:
+ - url: http://{baseurl}/api/pub/v0
+ variables:
+ baseurl:
+ default: localhost:8080
+ description: Development environment
+tags:
+ - name: ProjectComponents
+ description: API for managing project components
+
+paths:
+ /projects/{projectId}/components/:
+ post:
+ tags:
+ - projectComponents
+ summary: Create a component in a project
+ operationId: createProjectComponent
+ description: Retrieves information about a specific component
+ parameters:
+ - name: projectId
+ in: path
+ required: true
+ description: Project id
+ schema:
+ type: string
+ requestBody:
+ description: Component data
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CreateComponentRequest'
+ responses:
+ '201':
+ description: Created
+ headers:
+ Location:
+ schema:
+ type: string
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CreateComponentResponse'
+ '400':
+ description: Bad Request
+ '401':
+ $ref: '#/components/responses/UnauthorizedError'
+ '403':
+ description: Forbidden
+ '404':
+ description: Endpoint not found / Project not found / Product not found
+ '500':
+ description: Internal Server Error
+
+ /projects/{projectId}/components/{componentId}:
+ get:
+ tags:
+ - projectComponents
+ summary: Get component information
+ operationId: getProjectComponent
+ description: Retrieves information about a specific component
+ parameters:
+ - name: projectId
+ in: path
+ required: true
+ description: Project id
+ schema:
+ type: string
+ - name: componentId
+ in: path
+ required: true
+ description: Component id
+ schema:
+ type: string
+ responses:
+ '200':
+ description: Component information
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Component'
+
+components:
+ securitySchemes:
+ basicAuth:
+ type: http
+ scheme: basic
+ bearerAuth:
+ type: http
+ scheme: bearer
+ bearerFormat: JWT
+ responses:
+ UnauthorizedError:
+ description: Authentication information is missing or invalid
+ headers:
+ WWW_Authenticate:
+ schema:
+ type: string
+ schemas:
+ Component:
+ type: object
+ properties:
+ id:
+ type: string
+ description: Component ID
+ name:
+ type: string
+ description: Name of the component
+ productDescription:
+ type: string
+ description: Description of the product
+ productName:
+ type: string
+ description: Name of the product (e.g. Docker plain)
+ productId:
+ type: string
+ description: Product ID
+ environment:
+ type: string
+ description: Environment (e.g. DEV)
+ status:
+ type: string
+ description: Status of the component (e.g. READY, NOT_READY)
+ resultTraceback:
+ type: string
+ description: Traceback information in case of error
+ repositoryURL:
+ type: string
+ description: URL of the repository (for ODS products)
+ params:
+ type: object
+ description: Additional parameters (key-value pairs)
+ component-type:
+ type: string
+ description: Type of component (ods|awx)
+ CreateComponentResponse:
+ type: object
+ properties:
+ errorCode:
+ type: integer
+ field:
+ type: string
+ message:
+ type: string
+ location:
+ type: string
+ format: url
+ CreateComponentRequest:
+ type: object
+ properties:
+ name:
+ type: string
+ description: component name
+ productId:
+ type: string
+ description: product id
+ params:
+ type: object
+ additionalProperties:
+ type: string
\ No newline at end of file
diff --git a/api-project-component-v0/pom.xml b/api-project-component-v0/pom.xml
new file mode 100644
index 0000000..a3f50df
--- /dev/null
+++ b/api-project-component-v0/pom.xml
@@ -0,0 +1,163 @@
+
+ 4.0.0
+
+
+ org.opendevstack.apiservice
+ devstack-api-service
+ 0.0.3
+
+
+ api-project-component-v0
+ API Project Components V0
+ API module for managing EDP project components
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
+
+ org.springframework.boot
+ spring-boot-starter-oauth2-resource-server
+
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
+
+ org.springdoc
+ springdoc-openapi-starter-webmvc-ui
+
+
+
+ org.opendevstack.apiservice
+ service-projects
+ ${project.version}
+
+
+
+ io.jsonwebtoken
+ jjwt-api
+ 0.12.3
+
+
+
+ io.jsonwebtoken
+ jjwt-impl
+ 0.12.3
+ runtime
+
+
+
+ io.jsonwebtoken
+ jjwt-jackson
+ 0.12.3
+ runtime
+
+
+
+ org.projectlombok
+ lombok
+ provided
+
+
+
+ org.mapstruct
+ mapstruct
+ 1.6.3
+
+
+
+ org.openapitools
+ jackson-databind-nullable
+ ${jackson-databind-nullable.version}
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ org.springframework.security
+ spring-security-core
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+
+ org.projectlombok
+ lombok
+ ${lombok.version}
+
+
+ org.mapstruct
+ mapstruct-processor
+ 1.6.3
+
+
+
+
+
+ org.openapitools
+ openapi-generator-maven-plugin
+
+
+ generate-api-project-component-v0
+
+ generate
+
+
+ spring
+
+ spring-boot
+ ${project.basedir}/openapi/api-project-component-v0.yaml
+ org.opendevstack.apiservice.project.api
+ org.opendevstack.apiservice.project.model
+ org.opendevstack.apiservice.project
+ false
+ false
+ false
+ false
+ false
+ false
+
+ true
+ true
+ springdoc
+ true
+ true
+ true
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+ true
+
+
+
+
+
+
\ No newline at end of file
diff --git a/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/controller/ComponentsResponseFactory.java b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/controller/ComponentsResponseFactory.java
new file mode 100644
index 0000000..c3ef127
--- /dev/null
+++ b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/controller/ComponentsResponseFactory.java
@@ -0,0 +1,25 @@
+package org.opendevstack.apiservice.project.controller;
+
+import org.opendevstack.apiservice.project.model.CreateComponentResponse;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+
+public class ComponentsResponseFactory {
+
+ private ComponentsResponseFactory() {
+ }
+
+ public static CreateComponentResponse error(String projectId) {
+ CreateComponentResponse response = new CreateComponentResponse();
+ response.setErrorCode(HttpStatus.INTERNAL_SERVER_ERROR.value());
+ response.setMessage("Failed to create component for project '" + projectId + "'");
+ return response;
+ }
+
+ public static CreateComponentResponse entityCreated(String projectId, String componentName) {
+ CreateComponentResponse response = new CreateComponentResponse();
+ response.setErrorCode(HttpStatus.CREATED.value());
+ response.setMessage(componentName + " component created successfully in project " + projectId);
+ return response;
+ }
+}
diff --git a/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/controller/ProjectComponentsController.java b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/controller/ProjectComponentsController.java
new file mode 100644
index 0000000..2a49194
--- /dev/null
+++ b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/controller/ProjectComponentsController.java
@@ -0,0 +1,52 @@
+package org.opendevstack.apiservice.project.controller;
+
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.opendevstack.apiservice.project.api.ProjectComponentsApi;
+import org.opendevstack.apiservice.project.mapper.ComponentResponseMapper;
+import org.opendevstack.apiservice.project.model.Component;
+import org.opendevstack.apiservice.project.model.CreateComponentRequest;
+import org.opendevstack.apiservice.project.model.CreateComponentResponse;
+import org.opendevstack.apiservice.project.service.ComponentsService;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@AllArgsConstructor
+@Slf4j
+public class ProjectComponentsController implements ProjectComponentsApi {
+
+ private final ComponentsService componentsService;
+
+ private final ComponentResponseMapper componentResponseMapper;
+
+ @Override
+ public ResponseEntity createProjectComponent(String projectId, CreateComponentRequest createComponentRequest) {
+ try {
+ Component component = componentsService.createProjectComponent(projectId, createComponentRequest);
+ if (component == null) {
+ log.error("Failed to create component for project '{}'", projectId);
+ return componentResponseMapper.toResponseEntity(ComponentsResponseFactory.error(projectId));
+ }
+ return componentResponseMapper.toResponseEntity(ComponentsResponseFactory.entityCreated(projectId, component.getName()));
+ } catch (Exception e) {
+ log.error("Error while trying to create component for project '" + projectId + "': " + e.getMessage(), e);
+ return componentResponseMapper.toResponseEntity(ComponentsResponseFactory.error(projectId));
+ }
+ }
+
+ @Override
+ public ResponseEntity getProjectComponent(String projectId, String componentId) {
+ try {
+ Component component = componentsService.getProjectComponent(projectId, componentId);
+ if (component == null) {
+ return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
+ }
+ return ResponseEntity.status(HttpStatus.OK).body(component);
+ } catch (Exception e) {
+ log.error("Error retrieving component '{}' for project '{}': {}", componentId, projectId, e.getMessage(), e);
+ return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
+ }
+ }
+}
diff --git a/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/mapper/ComponentResponseMapper.java b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/mapper/ComponentResponseMapper.java
new file mode 100644
index 0000000..4c3198b
--- /dev/null
+++ b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/mapper/ComponentResponseMapper.java
@@ -0,0 +1,15 @@
+package org.opendevstack.apiservice.project.mapper;
+
+import org.mapstruct.Mapper;
+import org.opendevstack.apiservice.project.model.CreateComponentResponse;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+
+@Mapper(componentModel = "spring")
+public interface ComponentResponseMapper {
+
+ default ResponseEntity toResponseEntity(CreateComponentResponse response) {
+ return new ResponseEntity<>(response, HttpStatus.valueOf(response.getErrorCode()));
+ }
+
+}
diff --git a/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/service/ComponentsService.java b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/service/ComponentsService.java
new file mode 100644
index 0000000..959127a
--- /dev/null
+++ b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/service/ComponentsService.java
@@ -0,0 +1,43 @@
+package org.opendevstack.apiservice.project.service;
+
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.opendevstack.apiservice.externalservice.api.ExternalService;
+import org.opendevstack.apiservice.project.model.Component;
+import org.opendevstack.apiservice.project.model.CreateComponentRequest;
+import org.springframework.stereotype.Service;
+
+@Service
+@AllArgsConstructor
+@Slf4j
+public class ComponentsService {
+
+ private final MarketplaceExternalServicePlaceholder marketplaceExternalService;
+
+ public Component getProjectComponent(String projectId, String componentId) {
+ return marketplaceExternalService.getProjectComponent(projectId, componentId);
+ }
+
+ public Component createProjectComponent(String projectId, CreateComponentRequest createComponentRequest) {
+ return marketplaceExternalService.createProjectComponent(projectId, createComponentRequest);
+ }
+
+ @Service
+ class MarketplaceExternalServicePlaceholder implements ExternalService {
+
+ @Override
+ public boolean isHealthy() {
+ return false;
+ }
+
+ public Component getProjectComponent(String projectId, String componentId) {
+ log.info("Get component with id '" + componentId + "' for project '" + projectId + "'");
+ return null;
+ }
+
+ public Component createProjectComponent(String projectId, CreateComponentRequest createComponentRequest) {
+ log.info("Creating component for project '" + projectId + "'" + " with request: " + createComponentRequest);
+ return null;
+ }
+ }
+}
diff --git a/api-project-component-v0/src/test/java/org/opendevstack/apiservice/project/controller/ProjectComponentsControllerTest.java b/api-project-component-v0/src/test/java/org/opendevstack/apiservice/project/controller/ProjectComponentsControllerTest.java
new file mode 100644
index 0000000..4d46cbb
--- /dev/null
+++ b/api-project-component-v0/src/test/java/org/opendevstack/apiservice/project/controller/ProjectComponentsControllerTest.java
@@ -0,0 +1,100 @@
+package org.opendevstack.apiservice.project.controller;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mapstruct.factory.Mappers;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.opendevstack.apiservice.project.mapper.ComponentResponseMapper;
+import org.opendevstack.apiservice.project.model.Component;
+import org.opendevstack.apiservice.project.model.CreateComponentRequest;
+import org.opendevstack.apiservice.project.model.CreateComponentResponse;
+import org.opendevstack.apiservice.project.service.ComponentsService;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.when;
+import static org.opendevstack.apiservice.project.util.TestHelper.*;
+
+@ExtendWith(MockitoExtension.class)
+class ProjectComponentsControllerTest {
+
+ @Mock
+ private ComponentsService componentsService;
+
+ private final ComponentResponseMapper componentResponseMapper = Mappers.getMapper(ComponentResponseMapper.class);
+
+ private ProjectComponentsController projectComponentsController;
+
+ @BeforeEach
+ void setup() {
+ projectComponentsController = new ProjectComponentsController(componentsService, componentResponseMapper);
+ }
+
+ @Test
+ void testCreateProjectComponent_whenSuccess_thenReturnOk() throws Exception {
+ Component testComponent = buildTestComponent();
+ String testProjectId = "testProjectId";
+ CreateComponentRequest testCreateComponentRequest = buildTestCreateComponentRequest();
+ CreateComponentResponse testServiceResponseSuccess = buildTestCreateComponentResponseSuccess(testComponent.getName(),
+ testProjectId);
+
+ when(componentsService.createProjectComponent(anyString(), any(CreateComponentRequest.class)))
+ .thenReturn(testComponent);
+
+ ResponseEntity response = projectComponentsController.createProjectComponent(testProjectId,
+ testCreateComponentRequest);
+
+ assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
+ assertThat(response.getBody()).isNotNull();
+ assertThat(response.getBody()).isEqualTo(testServiceResponseSuccess);
+ }
+
+ @Test
+ void testCreateProjectComponent_whenFailure_thenReturnErrorResponse() throws Exception {
+ CreateComponentRequest testCreateComponentRequest = buildTestCreateComponentRequest();
+ String testProjectId = "testProjectId";
+ CreateComponentResponse testServiceResponseFailure = buildTestCreateComponentResponseFailure(testProjectId);
+
+ when(componentsService.createProjectComponent(anyString(), any(CreateComponentRequest.class)))
+ .thenReturn(null);
+
+ ResponseEntity response = projectComponentsController.createProjectComponent(testProjectId,
+ testCreateComponentRequest);
+
+ assertThat(response.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR);
+ assertThat(response.getBody()).isNotNull();
+ assertThat(response.getBody()).isEqualTo(testServiceResponseFailure);
+ }
+
+ @Test
+ void testGetProjectComponent_whenSuccess_thenReturnOk() throws Exception {
+ Component testComponent = buildTestComponent();
+
+ when(componentsService.getProjectComponent(anyString(), anyString()))
+ .thenReturn(testComponent);
+
+ ResponseEntity response = projectComponentsController.getProjectComponent("projectId",
+ "testId");
+
+ assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
+ assertThat(response.getBody()).isNotNull();
+ assertThat(response.getBody()).isEqualTo(testComponent);
+ }
+
+ @Test
+ void testGetProjectComponent_whenFailure_thenReturnErrorResponse() throws Exception {
+ when(componentsService.getProjectComponent(anyString(), anyString()))
+ .thenReturn(null);
+
+ ResponseEntity response = projectComponentsController.getProjectComponent("projectId",
+ "testId");
+
+ assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
+ assertThat(response.getBody()).isNull();
+ }
+}
\ No newline at end of file
diff --git a/api-project-component-v0/src/test/java/org/opendevstack/apiservice/project/service/ComponentsServiceTest.java b/api-project-component-v0/src/test/java/org/opendevstack/apiservice/project/service/ComponentsServiceTest.java
new file mode 100644
index 0000000..1ce3b3d
--- /dev/null
+++ b/api-project-component-v0/src/test/java/org/opendevstack/apiservice/project/service/ComponentsServiceTest.java
@@ -0,0 +1,74 @@
+package org.opendevstack.apiservice.project.service;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.opendevstack.apiservice.project.model.Component;
+import org.opendevstack.apiservice.project.model.CreateComponentRequest;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.when;
+import static org.opendevstack.apiservice.project.util.TestHelper.buildTestComponent;
+import static org.opendevstack.apiservice.project.util.TestHelper.buildTestCreateComponentRequest;
+
+@ExtendWith(MockitoExtension.class)
+class ComponentsServiceTest {
+
+ @Mock
+ private ComponentsService.MarketplaceExternalServicePlaceholder marketPlaceExternalServicePlaceholder;
+
+ private ComponentsService componentsService;
+
+ @BeforeEach
+ void setup() {
+ componentsService = new ComponentsService(marketPlaceExternalServicePlaceholder);
+ }
+
+ @Test
+ void testGetProjectComponent_whenSuccess_thenReturnCorrectComponent() throws Exception {
+ Component testComponent = buildTestComponent();
+
+ when(marketPlaceExternalServicePlaceholder.getProjectComponent(anyString(), eq("testId")))
+ .thenReturn(testComponent);
+
+ Component retrievedComponent = componentsService.getProjectComponent("testId", "testId");
+ assertThat(retrievedComponent).isEqualTo(testComponent);
+ }
+
+ @Test
+ void testGetProjectComponent_whenNoComponentFound_thenReturnNull() throws Exception {
+ when(marketPlaceExternalServicePlaceholder.getProjectComponent(anyString(), eq("testId")))
+ .thenReturn(null);
+
+ Component retrievedComponent = componentsService.getProjectComponent("testId", "testId");
+ assertThat(retrievedComponent).isNull();
+ }
+
+ @Test
+ void testCreateProjectComponent_whenSuccess_thenReturnCorrectComponent() throws Exception {
+ Component testComponent = buildTestComponent();
+ CreateComponentRequest testRequest = buildTestCreateComponentRequest();
+
+ when(marketPlaceExternalServicePlaceholder.createProjectComponent(anyString(), eq(testRequest)))
+ .thenReturn(testComponent);
+
+ Component retrievedComponent = componentsService.createProjectComponent("testId", testRequest);
+ assertThat(retrievedComponent).isEqualTo(testComponent);
+ }
+
+
+ @Test
+ void testCreateProjectComponent_whenFailure_thenReturnNull() throws Exception {
+ CreateComponentRequest testRequest = buildTestCreateComponentRequest();
+
+ when(marketPlaceExternalServicePlaceholder.createProjectComponent(anyString(), eq(testRequest)))
+ .thenReturn(null);
+
+ Component retrievedComponent = componentsService.createProjectComponent("testId", testRequest);
+ assertThat(retrievedComponent).isNull();
+ }
+}
\ No newline at end of file
diff --git a/api-project-component-v0/src/test/java/org/opendevstack/apiservice/project/util/TestHelper.java b/api-project-component-v0/src/test/java/org/opendevstack/apiservice/project/util/TestHelper.java
new file mode 100644
index 0000000..40c0ac6
--- /dev/null
+++ b/api-project-component-v0/src/test/java/org/opendevstack/apiservice/project/util/TestHelper.java
@@ -0,0 +1,42 @@
+package org.opendevstack.apiservice.project.util;
+
+import org.opendevstack.apiservice.project.model.Component;
+import org.opendevstack.apiservice.project.model.CreateComponentRequest;
+import org.opendevstack.apiservice.project.model.CreateComponentResponse;
+import org.springframework.http.HttpStatus;
+
+public class TestHelper {
+
+ private TestHelper() {
+ }
+
+ public static Component buildTestComponent() {
+ Component component = new Component();
+ component.setId("testId");
+ component.setName("testComponentName");
+ component.environment("testEnv");
+ component.setComponentType("testComponentType");
+ return component;
+ }
+
+ public static CreateComponentRequest buildTestCreateComponentRequest() {
+ CreateComponentRequest request = new CreateComponentRequest();
+ request.setName("testComponentName");
+ request.setProductId("testProductId");
+ return request;
+ }
+
+ public static CreateComponentResponse buildTestCreateComponentResponseSuccess(String componentName, String projectId) {
+ CreateComponentResponse response = new CreateComponentResponse();
+ response.setErrorCode(HttpStatus.CREATED.value());
+ response.setMessage(componentName + " component created successfully in project " + projectId);
+ return response;
+ }
+
+ public static CreateComponentResponse buildTestCreateComponentResponseFailure(String projectId) {
+ CreateComponentResponse response = new CreateComponentResponse();
+ response.setErrorCode(HttpStatus.INTERNAL_SERVER_ERROR.value());
+ response.setMessage("Failed to create component for project '" + projectId + "'");
+ return response;
+ }
+}
diff --git a/pom.xml b/pom.xml
index c825aa0..6382c31 100644
--- a/pom.xml
+++ b/pom.xml
@@ -57,6 +57,7 @@
api-project-users
api-project-platform
api-project
+ api-project-component-v0