Adding initial fixes for lifecycle state

feature/appm-store/pbac
megala21 7 years ago
parent 03e63071ef
commit 6e912fccfc

@ -38,7 +38,7 @@ import javax.validation.Valid;
import javax.ws.rs.*; import javax.ws.rs.*;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import java.util.List;
@SwaggerDefinition( @SwaggerDefinition(
info = @Info( info = @Info(
@ -112,12 +112,6 @@ public interface ApplicationManagementAPI {
response = ErrorResponse.class) response = ErrorResponse.class)
}) })
Response getApplications( Response getApplications(
@ApiParam(
name = "If-Modified-Since",
value = "Validates if the requested variant has not been modified since the time specified",
required = false)
@HeaderParam("If-Modified-Since") String ifModifiedSince,
@ApiParam( @ApiParam(
name = "offset", name = "offset",
value = "Provide from which position apps should return", value = "Provide from which position apps should return",
@ -176,4 +170,66 @@ public interface ApplicationManagementAPI {
required = true) required = true)
@Valid Application application); @Valid Application application);
@PUT
@Consumes("application/json")
@Path("/{uuid}/lifecycle")
@ApiOperation(
consumes = MediaType.APPLICATION_JSON,
produces = MediaType.APPLICATION_JSON,
httpMethod = "PUT",
value = "Change the life cycle state of the application",
notes = "This will change the life-cycle state of the application",
tags = "Application Management"
)
@ApiResponses(
value = {
@ApiResponse(
code = 200,
message = "OK. \n Successfully changed application state."),
@ApiResponse(
code = 500,
message = "Internal Server Error. \n Error occurred while getting the application list.",
response = ErrorResponse.class)
})
Response changeLifecycleState(
@ApiParam(
name = "UUID",
value = "Unique identifier of the Application",
required = true)
@PathParam("uuid") String applicationUUID,
@ApiParam(
name = "state",
value = "Lifecycle State that need to be changed to",
required = true)
@QueryParam("state") String state);
@GET
@Consumes("application/json")
@Path("/{uuid}/lifecycle")
@ApiOperation(
consumes = MediaType.APPLICATION_JSON,
produces = MediaType.APPLICATION_JSON,
httpMethod = "GET",
value = "Change the life cycle state of the application",
notes = "This will retrieve the next life cycle states of the application based on the user and the "
+ "current state",
tags = "Application Management"
)
@ApiResponses(
value = {
@ApiResponse(
code = 200,
message = "OK. \n Successfully retrieved the lifecycle states.",
response = List.class),
@ApiResponse(
code = 500,
message = "Internal Server Error. \n Error occurred while getting the life-cycle states.",
response = ErrorResponse.class)
})
Response getLifeCycleStates(
@ApiParam(
name = "UUID",
value = "Unique identifier of the Application",
required = true)
@PathParam("uuid") String applicationUUID);
} }

@ -20,10 +20,12 @@ package org.wso2.carbon.device.application.mgt.api.services.impl;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import org.wso2.carbon.device.application.mgt.api.services.ApplicationManagementAPI;
import org.wso2.carbon.device.application.mgt.common.*; import org.wso2.carbon.device.application.mgt.common.*;
import org.wso2.carbon.device.application.mgt.common.exception.ApplicationManagementException; import org.wso2.carbon.device.application.mgt.common.exception.ApplicationManagementException;
import org.wso2.carbon.device.application.mgt.common.services.ApplicationManager; import org.wso2.carbon.device.application.mgt.common.services.ApplicationManager;
import org.wso2.carbon.device.application.mgt.api.APIUtil; import org.wso2.carbon.device.application.mgt.api.APIUtil;
import org.wso2.carbon.device.application.mgt.core.exception.ApplicationManagementDAOException;
import javax.validation.Valid; import javax.validation.Valid;
import javax.ws.rs.*; import javax.ws.rs.*;
@ -32,7 +34,8 @@ import java.util.Date;
@Produces({"application/json"}) @Produces({"application/json"})
@Consumes({"application/json"}) @Consumes({"application/json"})
public class ApplicationManagementAPIImpl { @Path("/applications")
public class ApplicationManagementAPIImpl implements ApplicationManagementAPI{
public static final int DEFAULT_LIMIT = 20; public static final int DEFAULT_LIMIT = 20;
@ -42,7 +45,6 @@ public class ApplicationManagementAPIImpl {
@GET @GET
@Consumes("application/json") @Consumes("application/json")
@Path("applications")
public Response getApplications(@QueryParam("offset") int offset, @QueryParam("limit") int limit, public Response getApplications(@QueryParam("offset") int offset, @QueryParam("limit") int limit,
@QueryParam("query") String searchQuery) { @QueryParam("query") String searchQuery) {
ApplicationManager applicationManager = APIUtil.getApplicationManager(); ApplicationManager applicationManager = APIUtil.getApplicationManager();
@ -66,7 +68,7 @@ public class ApplicationManagementAPIImpl {
@GET @GET
@Consumes("application/json") @Consumes("application/json")
@Path("applications/{uuid}") @Path("/{uuid}")
public Response getApplication(@PathParam("uuid") String uuid) { public Response getApplication(@PathParam("uuid") String uuid) {
ApplicationManager applicationManager = APIUtil.getApplicationManager(); ApplicationManager applicationManager = APIUtil.getApplicationManager();
return null; return null;
@ -74,7 +76,7 @@ public class ApplicationManagementAPIImpl {
@PUT @PUT
@Consumes("application/json") @Consumes("application/json")
@Path("applications/{uuid}/lifecycle") @Path("/{uuid}/lifecycle")
public Response changeLifecycleState(@PathParam("uuid") String applicationUUID, @QueryParam("state") String state) { public Response changeLifecycleState(@PathParam("uuid") String applicationUUID, @QueryParam("state") String state) {
ApplicationManager applicationManager = APIUtil.getApplicationManager(); ApplicationManager applicationManager = APIUtil.getApplicationManager();
try { try {
@ -84,35 +86,42 @@ public class ApplicationManagementAPIImpl {
log.error(msg, e); log.error(msg, e);
return Response.status(Response.Status.BAD_REQUEST).build(); return Response.status(Response.Status.BAD_REQUEST).build();
} }
return Response.status(Response.Status.OK).entity("Successfully changed the lifecycle state of the application: " + applicationUUID).build(); return Response.status(Response.Status.OK)
.entity("Successfully changed the lifecycle state of the application: " + applicationUUID).build();
}
@GET
@Path("/{uuid}/lifecycle")
@Override
public Response getLifeCycleStates(String applicationUUID) {
ApplicationManager applicationManager = APIUtil.getApplicationManager();
try {
return Response.status(Response.Status.OK).entity(applicationManager.getLifeCycleStates(applicationUUID))
.build();
} catch (ApplicationManagementException e) {
log.error("Application Management Exception while trying to get next states for the applications with "
+ "the application ID", e);
return APIUtil.getResponse(e, Response.Status.INTERNAL_SERVER_ERROR);
}
} }
@POST @POST
@Consumes("application/json") @Consumes("application/json")
@Path("applications")
public Response createApplication(@Valid Application application) { public Response createApplication(@Valid Application application) {
ApplicationManager applicationManager = APIUtil.getApplicationManager(); ApplicationManager applicationManager = APIUtil.getApplicationManager();
//TODO : Get username and tenantId
User user = new User("admin", -1234);
application.setUser(user);
try { try {
application = applicationManager.createApplication(application); application = applicationManager.createApplication(application);
return Response.status(Response.Status.OK).entity(application).build();
} catch (ApplicationManagementException e) { } catch (ApplicationManagementException e) {
String msg = "Error occurred while creating the application"; String msg = "Error occurred while creating the application";
log.error(msg, e); log.error(msg, e);
return Response.status(Response.Status.BAD_REQUEST).build(); return Response.status(Response.Status.BAD_REQUEST).build();
} }
return Response.status(Response.Status.OK).entity(application).build();
} }
@PUT @PUT
@Consumes("application/json") @Consumes("application/json")
@Path("applications")
public Response editApplication(@Valid Application application) { public Response editApplication(@Valid Application application) {
ApplicationManager applicationManager = APIUtil.getApplicationManager(); ApplicationManager applicationManager = APIUtil.getApplicationManager();
@ -133,7 +142,7 @@ public class ApplicationManagementAPIImpl {
} }
@DELETE @DELETE
@Path("applications/{appuuid}") @Path("/{appuuid}")
public Response deleteApplication(@PathParam("appuuid") String uuid) { public Response deleteApplication(@PathParam("appuuid") String uuid) {
ApplicationManager applicationManager = APIUtil.getApplicationManager(); ApplicationManager applicationManager = APIUtil.getApplicationManager();
try { try {
@ -147,5 +156,4 @@ public class ApplicationManagementAPIImpl {
String responseMsg = "Successfully deleted the application: " + uuid; String responseMsg = "Successfully deleted the application: " + uuid;
return Response.status(Response.Status.OK).entity(responseMsg).build(); return Response.status(Response.Status.OK).entity(responseMsg).build();
} }
} }

@ -0,0 +1,34 @@
package org.wso2.carbon.device.application.mgt.common;
/**
* This represents the LifeCycleStateTransition from one state to next state.
*/
public class LifecycleStateTransition {
private String nextState;
private String permission;
private String description;
public String getNextState() {
return nextState;
}
public String getPermission() {
return permission;
}
public String getDescription() {
return description;
}
public void setNextState(String nextState) {
this.nextState = nextState;
}
public void setPermission(String permission) {
this.permission = permission;
}
public void setDescription(String description) {
this.description = description;
}
}

@ -18,11 +18,11 @@
*/ */
package org.wso2.carbon.device.application.mgt.common.services; package org.wso2.carbon.device.application.mgt.common.services;
import org.wso2.carbon.device.application.mgt.common.Application; import org.wso2.carbon.device.application.mgt.common.*;
import org.wso2.carbon.device.application.mgt.common.ApplicationList;
import org.wso2.carbon.device.application.mgt.common.Filter;
import org.wso2.carbon.device.application.mgt.common.exception.ApplicationManagementException; import org.wso2.carbon.device.application.mgt.common.exception.ApplicationManagementException;
import java.util.List;
/** /**
* This interface manages the application creation, deletion and editing of the application. * This interface manages the application creation, deletion and editing of the application.
*/ */
@ -34,7 +34,7 @@ public interface ApplicationManager {
* @return Created application * @return Created application
* @throws ApplicationManagementException Application Management Exception * @throws ApplicationManagementException Application Management Exception
*/ */
Application createApplication(Application application) throws ApplicationManagementException; public Application createApplication(Application application) throws ApplicationManagementException;
/** /**
* Updates an already existing application. * Updates an already existing application.
@ -42,14 +42,14 @@ public interface ApplicationManager {
* @return Updated Application * @return Updated Application
* @throws ApplicationManagementException Application Management Exception * @throws ApplicationManagementException Application Management Exception
*/ */
Application editApplication(Application application) throws ApplicationManagementException; public Application editApplication(Application application) throws ApplicationManagementException;
/** /**
* Delete an application identified by the unique ID. * Delete an application identified by the unique ID.
* @param uuid Unique ID for tha application * @param uuid Unique ID for tha application
* @throws ApplicationManagementException Application Management Exception * @throws ApplicationManagementException Application Management Exception
*/ */
void deleteApplication(String uuid) throws ApplicationManagementException; public void deleteApplication(String uuid) throws ApplicationManagementException;
/** /**
* To get the applications based on the search filter. * To get the applications based on the search filter.
@ -57,7 +57,17 @@ public interface ApplicationManager {
* @return Applications that matches the given filter criteria. * @return Applications that matches the given filter criteria.
* @throws ApplicationManagementException Application Management Exception * @throws ApplicationManagementException Application Management Exception
*/ */
ApplicationList getApplications(Filter filter) throws ApplicationManagementException; public ApplicationList getApplications(Filter filter) throws ApplicationManagementException;
void changeLifecycle(String applicationUUID, String lifecycleIdentifier) throws ApplicationManagementException; void changeLifecycle(String applicationUUID, String lifecycleIdentifier) throws ApplicationManagementException;
/**
* To get the next possible life-cycle states for the application.
*
* @param applicationUUID UUID of the application.
* @return the List of possible states
* @throws ApplicationManagementException Application Management Exception
*/
public List<LifecycleStateTransition> getLifeCycleStates(String applicationUUID)
throws ApplicationManagementException;
} }

@ -54,4 +54,7 @@ public interface ApplicationDAO {
void changeLifecycle(String applicationUUID, String lifecycleIdentifier) throws ApplicationManagementDAOException; void changeLifecycle(String applicationUUID, String lifecycleIdentifier) throws ApplicationManagementDAOException;
List<LifecycleStateTransition> getNextLifeCycleStates(String applicationUUID, int tenantId) throws
ApplicationManagementDAOException;
} }

@ -27,16 +27,114 @@ import org.wso2.carbon.device.application.mgt.core.dao.ApplicationDAO;
import org.wso2.carbon.device.application.mgt.core.dao.impl.AbstractDAOImpl; import org.wso2.carbon.device.application.mgt.core.dao.impl.AbstractDAOImpl;
import org.wso2.carbon.device.application.mgt.core.exception.ApplicationManagementDAOException; import org.wso2.carbon.device.application.mgt.core.exception.ApplicationManagementDAOException;
import org.wso2.carbon.device.application.mgt.core.dao.common.Util; import org.wso2.carbon.device.application.mgt.core.dao.common.Util;
import org.wso2.carbon.device.application.mgt.core.util.ConnectionManagerUtil;
import org.wso2.carbon.device.application.mgt.core.util.JSONUtil;
import java.sql.Connection; import java.sql.*;
import java.sql.PreparedStatement; import java.util.Iterator;
import java.sql.ResultSet; import java.util.Map;
import java.sql.SQLException;
public abstract class AbstractApplicationDAOImpl extends AbstractDAOImpl implements ApplicationDAO { public abstract class AbstractApplicationDAOImpl extends AbstractDAOImpl implements ApplicationDAO {
private static final Log log = LogFactory.getLog(AbstractApplicationDAOImpl.class); private static final Log log = LogFactory.getLog(AbstractApplicationDAOImpl.class);
public Application createApplication(Application application) throws ApplicationManagementDAOException {
if (log.isDebugEnabled()) {
log.debug("Request received in DAO Layer to create an application");
log.debug("Application Details : ");
log.debug("UUID : " + application.getUuid() + " Name : " + application.getName() + " User name : "
+ application.getUser().getUserName());
}
Connection conn = null;
PreparedStatement stmt = null;
ResultSet rs = null;
String sql = "";
boolean isBatchExecutionSupported = ConnectionManagerUtil.isBatchQuerySupported();
try {
conn = this.getDBConnection();
sql += "INSERT INTO APPM_APPLICATION (UUID, IDENTIFIER, NAME, SHORT_DESCRIPTION, DESCRIPTION, ICON_NAME, "
+ "BANNER_NAME, VIDEO_NAME, SCREENSHOTS, CREATED_BY, CREATED_AT, MODIFIED_AT, "
+ "APPLICATION_CATEGORY_ID, PLATFORM_ID, TENANT_ID, LIFECYCLE_STATE_ID, "
+ "LIFECYCLE_STATE_MODIFIED_AT, LIFECYCLE_STATE_MODIFIED_BY) VALUES "
+ "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
stmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
stmt.setString(1, application.getUuid());
stmt.setString(2, application.getIdentifier());
stmt.setString(3, application.getName());
stmt.setString(4, application.getShortDescription());
stmt.setString(5, application.getDescription());
stmt.setString(6, application.getIconName());
stmt.setString(7, application.getBannerName());
stmt.setString(8, application.getVideoName());
stmt.setString(9, JSONUtil.listToJsonArrayString(application.getScreenshots()));
stmt.setString(10, application.getUser().getUserName());
stmt.setDate(11, new Date(application.getCreatedAt().getTime()));
stmt.setDate(12, new Date(application.getModifiedAt().getTime()));
stmt.setInt(13, application.getCategory().getId());
stmt.setInt(14, application.getPlatform().getId());
stmt.setInt(15, application.getUser().getTenantId());
stmt.setInt(16, application.getCurrentLifecycle().getLifecycleState().getId());
stmt.setDate(17, new Date(
application.getCurrentLifecycle().getLifecycleStateModifiedAt().getTime()));
stmt.setString(18, application.getCurrentLifecycle().getGetLifecycleStateModifiedBy());
stmt.executeUpdate();
rs = stmt.getGeneratedKeys();
if (rs.next()) {
application.setId(rs.getInt(1));
}
if (application.getTags() != null && application.getTags().size() > 0) {
sql = "INSERT INTO APPM_APPLICATION_TAG (NAME, APPLICATION_ID) VALUES (?, ?); ";
stmt = conn.prepareStatement(sql);
for (String tag : application.getTags()) {
stmt.setString(1, tag);
stmt.setInt(2, application.getId());
if (isBatchExecutionSupported) {
stmt.addBatch();
} else {
stmt.execute();
}
}
if (isBatchExecutionSupported) {
stmt.executeBatch();
}
}
if (application.getProperties() != null && application.getProperties().size() > 0) {
sql = "INSERT INTO APPM_APPLICATION_PROPERTY (PROP_KEY, PROP_VAL, APPLICATION_ID) VALUES (?, ?, ?); ";
stmt = conn.prepareStatement(sql);
Iterator it = application.getProperties().entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String, String> property = (Map.Entry) it.next();
stmt.setString(1, property.getKey());
stmt.setString(2, property.getValue());
stmt.setInt(3, application.getId());
if (isBatchExecutionSupported) {
stmt.addBatch();
} else {
stmt.execute();
}
}
if (isBatchExecutionSupported) {
stmt.executeBatch();
}
}
} catch (DBConnectionException e) {
throw new ApplicationManagementDAOException("Error occurred while obtaining the DB connection.", e);
} catch (SQLException e) {
throw new ApplicationManagementDAOException("Error occurred while adding the application", e);
} finally {
Util.cleanupResources(stmt, rs);
}
return application;
}
@Override @Override
public int getApplicationCount(Filter filter) throws ApplicationManagementDAOException { public int getApplicationCount(Filter filter) throws ApplicationManagementDAOException {
if(log.isDebugEnabled()){ if(log.isDebugEnabled()){

@ -43,13 +43,46 @@ public class H2ApplicationDAOImpl extends AbstractApplicationDAOImpl {
private static final Log log = LogFactory.getLog(H2ApplicationDAOImpl.class); private static final Log log = LogFactory.getLog(H2ApplicationDAOImpl.class);
@Override @Override
public Application createApplication(Application application) throws ApplicationManagementDAOException { public void changeLifecycle(String applicationUUID, String lifecycleIdentifier) throws ApplicationManagementDAOException {
return null;
} }
@Override @Override
public void changeLifecycle(String applicationUUID, String lifecycleIdentifier) throws ApplicationManagementDAOException { public List<LifecycleStateTransition> getNextLifeCycleStates(String applicationUUID, int tenantId)
throws ApplicationManagementDAOException {
Connection connection = null;
PreparedStatement preparedStatement = null;
ResultSet resultSet = null;
String sql = "SELECT STATE.NAME, TRANSITION.DESCRIPTION, TRANSITION.PERMISSION FROM ( SELECT * FROM "
+ "APPM_LIFECYCLE_STATE ) STATE RIGHT JOIN (SELECT * FROM APPM_LIFECYCLE_STATE_TRANSITION WHERE "
+ "INITIAL_STATE = (SELECT LIFECYCLE_STATE_ID FROM APPM_APPLICATION WHERE UUID = ?)) "
+ "TRANSITION ON TRANSITION.NEXT_STATE = STATE.ID";
try {
connection = this.getDBConnection();
preparedStatement = connection.prepareStatement(sql);
preparedStatement.setString(1, applicationUUID);
resultSet = preparedStatement.executeQuery();
List<LifecycleStateTransition> lifecycleStateTransitions = new ArrayList<>();
while(resultSet.next()) {
LifecycleStateTransition lifecycleStateTransition = new LifecycleStateTransition();
lifecycleStateTransition.setDescription(resultSet.getString(2));
lifecycleStateTransition.setNextState(resultSet.getString(1));
lifecycleStateTransition.setPermission(resultSet.getString(3));
lifecycleStateTransitions.add(lifecycleStateTransition);
}
return lifecycleStateTransitions;
} catch (DBConnectionException e) {
throw new ApplicationManagementDAOException("Error while getting the DBConnection for getting the life "
+ "cycle states for the application with the UUID : " + applicationUUID, e);
} catch (SQLException e) {
throw new ApplicationManagementDAOException("SQL exception while executing the query '" + sql + "'.", e);
} finally {
Util.cleanupResources(preparedStatement, resultSet);
}
} }
@Override @Override

@ -355,10 +355,8 @@ public class MySQLApplicationDAOImpl extends AbstractApplicationDAOImpl {
@Override @Override
public void deleteProperties(int applicationId) throws ApplicationManagementDAOException { public void deleteProperties(int applicationId) throws ApplicationManagementDAOException {
Connection conn = null; Connection conn = null;
PreparedStatement stmt = null; PreparedStatement stmt = null;
ResultSet rs = null;
try { try {
conn = this.getDBConnection(); conn = this.getDBConnection();
String sql = "DELETE FROM APPM_APPLICATION_PROPERTY WHERE APPLICATION_ID = ?"; String sql = "DELETE FROM APPM_APPLICATION_PROPERTY WHERE APPLICATION_ID = ?";
@ -371,7 +369,7 @@ public class MySQLApplicationDAOImpl extends AbstractApplicationDAOImpl {
} catch (SQLException e) { } catch (SQLException e) {
throw new ApplicationManagementDAOException("Error occurred while deleting properties of application: " + applicationId, e); throw new ApplicationManagementDAOException("Error occurred while deleting properties of application: " + applicationId, e);
} finally { } finally {
Util.cleanupResources(stmt, rs); Util.cleanupResources(stmt, null);
} }
} }
@ -403,6 +401,12 @@ public class MySQLApplicationDAOImpl extends AbstractApplicationDAOImpl {
} }
} }
@Override
public List<LifecycleStateTransition> getNextLifeCycleStates(String applicationUUID, int tenantId)
throws ApplicationManagementDAOException {
return null;
}
@Override @Override
public void deleteTags(int applicationId) throws ApplicationManagementDAOException { public void deleteTags(int applicationId) throws ApplicationManagementDAOException {
@ -434,88 +438,4 @@ public class MySQLApplicationDAOImpl extends AbstractApplicationDAOImpl {
public void addRelease(ApplicationRelease release) throws ApplicationManagementDAOException { public void addRelease(ApplicationRelease release) throws ApplicationManagementDAOException {
} }
@Override
public Application createApplication(Application application) throws ApplicationManagementDAOException {
if (log.isDebugEnabled()) {
log.debug("Request received in DAO Layer to create an application");
log.debug("Application Details : ");
log.debug("UUID : " + application.getUuid() + " Name : " + application.getName() + " User name : " +
application.getUser().getUserName());
}
Connection conn = null;
PreparedStatement stmt = null;
ResultSet rs = null;
String sql = "";
try {
conn = this.getConnection();
sql += "INSERT INTO APPM_APPLICATION (UUID, IDENTIFIER, NAME, SHORT_DESCRIPTION, DESCRIPTION, ICON_NAME, BANNER_NAME, " +
"VIDEO_NAME, SCREENSHOTS, CREATED_BY, CREATED_AT, MODIFIED_AT, APPLICATION_CATEGORY_ID, " + "" +
"PLATFORM_ID, TENANT_ID, LIFECYCLE_STATE_ID, LIFECYCLE_STATE_MODIFIED_AT, " +
"LIFECYCLE_STATE_MODIFIED_BY) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
stmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
stmt.setString(1, application.getUuid());
stmt.setString(2, application.getIdentifier());
stmt.setString(3, application.getName());
stmt.setString(4, application.getShortDescription());
stmt.setString(5, application.getDescription());
stmt.setString(6, application.getIconName());
stmt.setString(7, application.getBannerName());
stmt.setString(8, application.getVideoName());
stmt.setString(9, JSONUtil.listToJsonArrayString(application.getScreenshots()));
stmt.setString(10, application.getUser().getUserName());
stmt.setDate(11, new Date(application.getCreatedAt().getTime()));
stmt.setDate(12, new Date(application.getModifiedAt().getTime()));
stmt.setInt(13, application.getCategory().getId());
stmt.setInt(14, application.getPlatform().getId());
stmt.setInt(15, application.getUser().getTenantId());
stmt.setInt(16, application.getCurrentLifecycle().getLifecycleState().getId());
stmt.setDate(17, new Date(application.getCurrentLifecycle().getLifecycleStateModifiedAt().getTime()));
stmt.setString(18, application.getCurrentLifecycle().getGetLifecycleStateModifiedBy());
stmt.executeUpdate();
rs = stmt.getGeneratedKeys();
if (rs.next()) {
application.setId(rs.getInt(1));
}
if (application.getTags() != null && application.getTags().size() > 0) {
sql = "INSERT INTO APPM_APPLICATION_TAG (NAME, APPLICATION_ID) VALUES (?, ?); ";
stmt = conn.prepareStatement(sql);
for (String tag : application.getTags()) {
stmt.setString(1, tag);
stmt.setInt(2, application.getId());
stmt.addBatch();
}
stmt.executeBatch();
}
if (application.getProperties() != null && application.getProperties().size() > 0) {
sql = "INSERT INTO APPM_APPLICATION_PROPERTY (PROP_KEY, PROP_VAL, APPLICATION_ID) VALUES (?, ?, ?); ";
stmt = conn.prepareStatement(sql);
Iterator it = application.getProperties().entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String, String> property = (Map.Entry) it.next();
stmt.setString(1, property.getKey());
stmt.setString(2, property.getValue());
stmt.setInt(3, application.getId());
stmt.addBatch();
}
stmt.executeBatch();
}
} catch (DBConnectionException e) {
throw new ApplicationManagementDAOException("Error occurred while obtaining the DB connection.", e);
} catch (SQLException e) {
throw new ApplicationManagementDAOException("Error occurred while adding the application", e);
}
return application;
}
} }

@ -44,10 +44,8 @@ public class GenericLifecycleStateImpl extends AbstractDAOImpl implements Lifecy
String sql = ""; String sql = "";
try { try {
conn = this.getConnection(); conn = this.getDBConnection();
sql += "SELECT * "; sql += "SELECT * FROM APPM_LIFECYCLE_STATE WHERE IDENTIFIER = ? ";
sql += "FROM APPM_LIFECYCLE_STATE ";
sql += "WHERE IDENTIFIER = ? ";
stmt = conn.prepareStatement(sql); stmt = conn.prepareStatement(sql);
stmt.setString(1, identifier); stmt.setString(1, identifier);

@ -483,9 +483,8 @@ public class GenericPlatformDAOImpl extends AbstractDAOImpl implements PlatformD
platform.setIdentifier(rs.getString(2)); platform.setIdentifier(rs.getString(2));
platform.setShared(rs.getBoolean(8)); platform.setShared(rs.getBoolean(8));
platform.setDefaultTenantMapping(rs.getBoolean(9)); platform.setDefaultTenantMapping(rs.getBoolean(9));
platform.setId(rs.getInt(4));
if (!platform.isFileBased()) { if (!platform.isFileBased()) {
platform.setId(rs.getInt(4));
platform.setName(rs.getString(5)); platform.setName(rs.getString(5));
platform.setDescription(rs.getString(6)); platform.setDescription(rs.getString(6));
platform.setIconName(rs.getString(7)); platform.setIconName(rs.getString(7));

@ -18,12 +18,18 @@
*/ */
package org.wso2.carbon.device.application.mgt.core.impl; package org.wso2.carbon.device.application.mgt.core.impl;
import com.sun.corba.se.spi.legacy.connection.Connection;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import org.wso2.carbon.device.application.mgt.common.*; import org.wso2.carbon.context.PrivilegedCarbonContext;
import org.wso2.carbon.device.application.mgt.common.Application;
import org.wso2.carbon.device.application.mgt.common.ApplicationList;
import org.wso2.carbon.device.application.mgt.common.Filter;
import org.wso2.carbon.device.application.mgt.common.Lifecycle;
import org.wso2.carbon.device.application.mgt.common.LifecycleState;
import org.wso2.carbon.device.application.mgt.common.LifecycleStateTransition;
import org.wso2.carbon.device.application.mgt.common.Platform;
import org.wso2.carbon.device.application.mgt.common.User;
import org.wso2.carbon.device.application.mgt.common.exception.ApplicationManagementException; import org.wso2.carbon.device.application.mgt.common.exception.ApplicationManagementException;
import org.wso2.carbon.device.application.mgt.common.exception.DBConnectionException;
import org.wso2.carbon.device.application.mgt.common.services.ApplicationManager; import org.wso2.carbon.device.application.mgt.common.services.ApplicationManager;
import org.wso2.carbon.device.application.mgt.core.dao.ApplicationDAO; import org.wso2.carbon.device.application.mgt.core.dao.ApplicationDAO;
import org.wso2.carbon.device.application.mgt.core.dao.LifecycleStateDAO; import org.wso2.carbon.device.application.mgt.core.dao.LifecycleStateDAO;
@ -36,32 +42,45 @@ import org.wso2.carbon.device.application.mgt.core.util.ConnectionManagerUtil;
import org.wso2.carbon.device.application.mgt.core.util.HelperUtil; import org.wso2.carbon.device.application.mgt.core.util.HelperUtil;
import java.util.Date; import java.util.Date;
import java.util.List;
public class ApplicationManagerImpl implements ApplicationManager { public class ApplicationManagerImpl implements ApplicationManager {
private static final Log log = LogFactory.getLog(ApplicationManagerImpl.class); private static final Log log = LogFactory.getLog(ApplicationManagerImpl.class);
public static final String CREATED = "created"; public static final String CREATED = "CREATED";
@Override @Override
public Application createApplication(Application application) throws ApplicationManagementException { public Application createApplication(Application application) throws ApplicationManagementException {
application.setUser(new User(PrivilegedCarbonContext.getThreadLocalCarbonContext().getUsername(),
validateApplication(application, false); PrivilegedCarbonContext.getThreadLocalCarbonContext().getTenantId(true)));
if (log.isDebugEnabled()) {
log.debug("Create Application received for the tenant : " + application.getUser().getTenantId() + " From"
+ " the user : " + application.getUser().getUserName());
}
validateApplication(application);
application.setUuid(HelperUtil.generateApplicationUuid());
application.setCreatedAt(new Date());
application.setModifiedAt(new Date());
try { try {
ConnectionManagerUtil.openConnection(); ConnectionManagerUtil.beginDBTransaction();
ApplicationDAO applicationDAO = DAOFactory.getApplicationDAO();
application.setUuid(HelperUtil.generateApplicationUuid());
application.setCreatedAt(new Date());
application.setModifiedAt(new Date());
// Validating the platform
Platform platform = DAOFactory.getPlatformDAO()
.getPlatform(application.getUser().getTenantId(), application.getPlatform().getIdentifier());
if (platform == null) {
throw new NotFoundException("Invalid platform");
}
application.setPlatform(platform);
if (log.isDebugEnabled()) {
log.debug("Application creation pre-conditions are met and the platform mentioned by identifier "
+ platform.getIdentifier() + " is found");
}
LifecycleStateDAO lifecycleStateDAO = DAOFactory.getLifecycleStateDAO(); LifecycleStateDAO lifecycleStateDAO = DAOFactory.getLifecycleStateDAO();
LifecycleState lifecycleState = lifecycleStateDAO.getLifeCycleStateByIdentifier(CREATED); LifecycleState lifecycleState = lifecycleStateDAO.getLifeCycleStateByIdentifier(CREATED);
if (lifecycleState == null) { if (lifecycleState == null) {
ConnectionManagerUtil.commitDBTransaction();
throw new NotFoundException("Invalid lifecycle state."); throw new NotFoundException("Invalid lifecycle state.");
} }
Lifecycle lifecycle = new Lifecycle(); Lifecycle lifecycle = new Lifecycle();
lifecycle.setLifecycleState(lifecycleState); lifecycle.setLifecycleState(lifecycleState);
lifecycle.setLifecycleState(lifecycleState); lifecycle.setLifecycleState(lifecycleState);
@ -69,21 +88,17 @@ public class ApplicationManagerImpl implements ApplicationManager {
lifecycle.setGetLifecycleStateModifiedBy(application.getUser().getUserName()); lifecycle.setGetLifecycleStateModifiedBy(application.getUser().getUserName());
application.setCurrentLifecycle(lifecycle); application.setCurrentLifecycle(lifecycle);
PlatformDAO platformDAO = DAOFactory.getPlatformDAO(); application = DAOFactory.getApplicationDAO().createApplication(application);
Platform platform = platformDAO.getPlatform(application.getUser().getTenantId(), application.getPlatform().getIdentifier()); ConnectionManagerUtil.commitDBTransaction();
if (platform == null) { return application;
throw new NotFoundException("Invalid platform"); } catch (ApplicationManagementException e) {
} ConnectionManagerUtil.rollbackDBTransaction();
application.setPlatform(platform); throw e;
return applicationDAO.createApplication(application);
} finally { } finally {
ConnectionManagerUtil.closeConnection(); ConnectionManagerUtil.closeDBConnection();
} }
} }
@Override @Override
public Application editApplication(Application application) throws ApplicationManagementException { public Application editApplication(Application application) throws ApplicationManagementException {
@ -159,7 +174,25 @@ public class ApplicationManagerImpl implements ApplicationManager {
} }
} }
private void validateApplication(Application application, boolean isEdit) throws ValidationException { @Override
public List<LifecycleStateTransition> getLifeCycleStates(String applicationUUID)
throws ApplicationManagementException {
try {
int tenantId = PrivilegedCarbonContext.getThreadLocalCarbonContext().getTenantId(true);
ConnectionManagerUtil.openDBConnection();
return DAOFactory.getApplicationDAO().getNextLifeCycleStates(applicationUUID, tenantId);
} finally {
ConnectionManagerUtil.closeDBConnection();
}
}
/**
* To validate the application
*
* @param application Application that need to be created
* @throws ValidationException Validation Exception
*/
private void validateApplication(Application application) throws ValidationException {
if (application.getName() == null) { if (application.getName() == null) {
throw new ValidationException("Application name cannot be empty"); throw new ValidationException("Application name cannot be empty");

@ -20,4 +20,18 @@
<class>org.wso2.carbon.device.application.mgt.core.deployer.PlatformDeployer</class> <class>org.wso2.carbon.device.application.mgt.core.deployer.PlatformDeployer</class>
</deployer> </deployer>
</deployers> </deployers>
<ManagementPermissions>
<ManagementPermission>
<DisplayName>Application</DisplayName>
<ResourceId>/permission/admin/manage/device-mgt/application</ResourceId>
</ManagementPermission>
<ManagementPermission>
<DisplayName>Create</DisplayName>
<ResourceId>/permission/admin/manage/device-mgt/application/create</ResourceId>
</ManagementPermission>
<ManagementPermission>
<DisplayName>Review</DisplayName>
<ResourceId>/permission/admin/manage/device-mgt/application/review</ResourceId>
</ManagementPermission>
</ManagementPermissions>
</component> </component>

@ -36,7 +36,7 @@ FOREIGN KEY(PLATFORM_ID) REFERENCES APPM_PLATFORM(ID) ON DELETE CASCADE,
PRIMARY KEY (ID, TENANT_ID, PLATFORM_ID) PRIMARY KEY (ID, TENANT_ID, PLATFORM_ID)
); );
CREATE INDEX FK_PLATFROM_TENANT_MAPPING_PLATFORM ON APPM_PLATFORM_TENANT_MAPPING(PLATFORM_ID ASC); CREATE INDEX IF NOT EXISTS FK_PLATFROM_TENANT_MAPPING_PLATFORM ON APPM_PLATFORM_TENANT_MAPPING(PLATFORM_ID ASC);
-- ----------------------------------------------------- -- -----------------------------------------------------
-- Table APPM_APPLICATION_CATEGORY -- Table APPM_APPLICATION_CATEGORY
@ -48,61 +48,118 @@ CREATE TABLE IF NOT EXISTS APPM_APPLICATION_CATEGORY (
PUBLISHED TINYINT NULL, PUBLISHED TINYINT NULL,
PRIMARY KEY (ID)); PRIMARY KEY (ID));
INSERT INTO APPM_APPLICATION_CATEGORY (NAME, DESCRIPTION, PUBLISHED) VALUES ('Enterprise', 'Enterprise level
applications which the artifacts need to be provided', 1);
INSERT INTO APPM_APPLICATION_CATEGORY (NAME, DESCRIPTION, PUBLISHED) VALUES ('Public', 'Public category in which the
application need to be downloaded from the public application store', 1);
-- ----------------------------------------------------- -- -----------------------------------------------------
-- Table `APPM_LIFECYCLE_STATE` -- Table `APPM_LIFECYCLE_STATE`
-- ----------------------------------------------------- -- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS APPM_LIFECYCLE_STATE ( CREATE TABLE IF NOT EXISTS APPM_LIFECYCLE_STATE (
ID INT NOT NULL AUTO_INCREMENT, ID INT NOT NULL AUTO_INCREMENT UNIQUE,
NAME VARCHAR(100) NOT NULL, NAME VARCHAR(100) NOT NULL,
IDENTIFIER VARCHAR(100) NOT NULL, IDENTIFIER VARCHAR(100) NOT NULL,
DESCRIPTION TEXT NULL, DESCRIPTION TEXT NULL,
PRIMARY KEY (ID), PRIMARY KEY (ID),
UNIQUE INDEX APPM_LIFECYCLE_STATE_IDENTIFIER_UNIQUE (IDENTIFIER ASC)); UNIQUE INDEX APPM_LIFECYCLE_STATE_IDENTIFIER_UNIQUE (IDENTIFIER ASC));
INSERT INTO APPM_LIFECYCLE_STATE (NAME, IDENTIFIER, DESCRIPTION) VALUES ('CREATED', 'CREATED', 'Application creation
initial state');
INSERT INTO APPM_LIFECYCLE_STATE (NAME, IDENTIFIER, DESCRIPTION)
VALUES ('IN REVIEW', 'IN REFVIEW', 'Application is in in-review state');
INSERT INTO APPM_LIFECYCLE_STATE (NAME, IDENTIFIER, DESCRIPTION)
VALUES ('APPROVED', 'APPROVED', 'State in which Application is approved after reviewing.');
INSERT INTO APPM_LIFECYCLE_STATE (NAME, IDENTIFIER, DESCRIPTION)
VALUES ('REJECTED', 'REJECTED', 'State in which Application is rejected after reviewing.');
INSERT INTO APPM_LIFECYCLE_STATE (NAME, IDENTIFIER, DESCRIPTION)
VALUES ('PUBLISHED', 'PUBLISHED', 'State in which Application is in published state.');
INSERT INTO APPM_LIFECYCLE_STATE (NAME, IDENTIFIER, DESCRIPTION)
VALUES ('UNPUBLISHED', 'UNPUBLISHED', 'State in which Application is in un published state.');
INSERT INTO APPM_LIFECYCLE_STATE (NAME, IDENTIFIER, DESCRIPTION)
VALUES ('RETIRED', 'RETIRED', 'Retiring an application to indicate end of life state,');
CREATE TABLE IF NOT EXISTS APPM_LIFECYCLE_STATE_TRANSITION
(
ID INT NOT NULL AUTO_INCREMENT UNIQUE,
INITIAL_STATE INT,
NEXT_STATE INT,
PERMISSION VARCHAR(1024),
DESCRIPTION VARCHAR(2048),
PRIMARY KEY (INITIAL_STATE, NEXT_STATE),
FOREIGN KEY (INITIAL_STATE) REFERENCES APPM_LIFECYCLE_STATE(ID) ON DELETE CASCADE,
FOREIGN KEY (NEXT_STATE) REFERENCES APPM_LIFECYCLE_STATE(ID) ON DELETE CASCADE
);
INSERT INTO APPM_LIFECYCLE_STATE_TRANSITION(INITIAL_STATE, NEXT_STATE, PERMISSION, DESCRIPTION) VALUES
(1, 2, null, 'Submit for review');
INSERT INTO APPM_LIFECYCLE_STATE_TRANSITION(INITIAL_STATE, NEXT_STATE, PERMISSION, DESCRIPTION) VALUES
(2, 1, null, 'Revoke from review');
INSERT INTO APPM_LIFECYCLE_STATE_TRANSITION(INITIAL_STATE, NEXT_STATE, PERMISSION, DESCRIPTION) VALUES
(2, 3, '/permission/admin/manage/device-mgt/application/review', 'APPROVE');
INSERT INTO APPM_LIFECYCLE_STATE_TRANSITION(INITIAL_STATE, NEXT_STATE, PERMISSION, DESCRIPTION) VALUES
(2, 4, '/permission/admin/manage/device-mgt/application/review', 'REJECT');
INSERT INTO APPM_LIFECYCLE_STATE_TRANSITION(INITIAL_STATE, NEXT_STATE, PERMISSION, DESCRIPTION) VALUES
(3, 4, '/permission/admin/manage/device-mgt/application/review', 'REJECT');
INSERT INTO APPM_LIFECYCLE_STATE_TRANSITION(INITIAL_STATE, NEXT_STATE, PERMISSION, DESCRIPTION) VALUES
(3, 5, null, 'PUBLISH');
INSERT INTO APPM_LIFECYCLE_STATE_TRANSITION(INITIAL_STATE, NEXT_STATE, PERMISSION, DESCRIPTION) VALUES
(5, 6, null, 'UN PUBLISH');
INSERT INTO APPM_LIFECYCLE_STATE_TRANSITION(INITIAL_STATE, NEXT_STATE, PERMISSION, DESCRIPTION) VALUES
(6, 5, null, 'PUBLISH');
INSERT INTO APPM_LIFECYCLE_STATE_TRANSITION(INITIAL_STATE, NEXT_STATE, PERMISSION, DESCRIPTION) VALUES
(4, 1, null, 'Return to CREATE STATE');
INSERT INTO APPM_LIFECYCLE_STATE_TRANSITION(INITIAL_STATE, NEXT_STATE, PERMISSION, DESCRIPTION) VALUES
(6, 1, null, 'Return to CREATE STATE');
INSERT INTO APPM_LIFECYCLE_STATE_TRANSITION(INITIAL_STATE, NEXT_STATE, PERMISSION, DESCRIPTION) VALUES
(6, 7, null, 'Retire');
-- ----------------------------------------------------- -- -----------------------------------------------------
-- Table APPM_APPLICATION -- Table APPM_APPLICATION
-- ----------------------------------------------------- -- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS APPM_APPLICATION ( CREATE TABLE IF NOT EXISTS `APPM_APPLICATION` (
ID INT NOT NULL AUTO_INCREMENT, `ID` INT NOT NULL AUTO_INCREMENT,
UUID VARCHAR(100) NOT NULL, `UUID` VARCHAR(100) NOT NULL,
NAME VARCHAR(100) NOT NULL, `IDENTIFIER` VARCHAR(255) NULL,
SHORT_DESCRIPTION VARCHAR(255) NULL, `NAME` VARCHAR(100) NOT NULL,
DESCRIPTION TEXT NULL, `SHORT_DESCRIPTION` VARCHAR(255) NULL,
ICON_NAME VARCHAR(100) NULL, `DESCRIPTION` TEXT NULL,
BANNER_NAME VARCHAR(100) NULL, `ICON_NAME` VARCHAR(100) NULL,
VIDEO_NAME VARCHAR(100) NULL, `BANNER_NAME` VARCHAR(100) NULL,
SCREENSHOTS TEXT NULL, `VIDEO_NAME` VARCHAR(100) NULL,
CREATED_BY VARCHAR(255) NULL, `SCREENSHOTS` TEXT NULL,
CREATED_AT DATETIME NOT NULL, `CREATED_BY` VARCHAR(255) NULL,
MODIFIED_AT DATETIME NULL, `CREATED_AT` DATETIME NOT NULL,
APPLICATION_CATEGORY_ID INT NOT NULL, `MODIFIED_AT` DATETIME NULL,
PLATFORM_APPLICATION_MAPPING_ID INT NOT NULL, `IS_FREE` TINYINT(1) NULL,
APPM_LIFECYCLE_STATE_ID INT NOT NULL, `PAYMENT_CURRENCY` VARCHAR(45) NULL,
LIECYCLE_STATE_MODIFIED_BY VARCHAR(255) NULL, `PAYMENT_PRICE` DECIMAL(10,2) NULL,
LIFECYCLE_STATE_MODIFIED_AT DATETIME NULL, `APPLICATION_CATEGORY_ID` INT NOT NULL,
PRIMARY KEY (ID, APPLICATION_CATEGORY_ID, PLATFORM_APPLICATION_MAPPING_ID, APPM_LIFECYCLE_STATE_ID), `LIFECYCLE_STATE_ID` INT NOT NULL,
UNIQUE INDEX UUID_UNIQUE (UUID ASC), `LIFECYCLE_STATE_MODIFIED_BY` VARCHAR(255) NULL,
CONSTRAINT FK_APPLICATION_APPLICATION_CATEGORY `LIFECYCLE_STATE_MODIFIED_AT` DATETIME NULL,
FOREIGN KEY (APPLICATION_CATEGORY_ID) `TENANT_ID` INT NULL,
REFERENCES APPM_APPLICATION_CATEGORY (ID) `PLATFORM_ID` INT NOT NULL,
PRIMARY KEY (`ID`, `APPLICATION_CATEGORY_ID`, `LIFECYCLE_STATE_ID`, `PLATFORM_ID`),
UNIQUE INDEX `UUID_UNIQUE` (`UUID` ASC),
FOREIGN KEY (`APPLICATION_CATEGORY_ID`)
REFERENCES `APPM_APPLICATION_CATEGORY` (`ID`)
ON DELETE NO ACTION ON DELETE NO ACTION
ON UPDATE NO ACTION, ON UPDATE NO ACTION,
CONSTRAINT fk_APPM_APPLICATION_APPM_PLATFORM_TENANT_MAPPING1 CONSTRAINT `fk_APPM_APPLICATION_APPM_LIFECYCLE_STATE1`
FOREIGN KEY (PLATFORM_APPLICATION_MAPPING_ID) FOREIGN KEY (`LIFECYCLE_STATE_ID`)
REFERENCES APPM_PLATFORM_TENANT_MAPPING (ID) REFERENCES `APPM_LIFECYCLE_STATE` (`ID`)
ON DELETE NO ACTION ON DELETE NO ACTION
ON UPDATE NO ACTION, ON UPDATE NO ACTION,
CONSTRAINT fk_APPM_APPLICATION_APPM_LIFECYCLE_STATE1 CONSTRAINT `fk_APPM_APPLICATION_APPM_PLATFORM1`
FOREIGN KEY (APPM_LIFECYCLE_STATE_ID) FOREIGN KEY (`PLATFORM_ID`)
REFERENCES APPM_LIFECYCLE_STATE (ID) REFERENCES `APPM_PLATFORM` (`ID`)
ON DELETE NO ACTION ON DELETE NO ACTION
ON UPDATE NO ACTION); ON UPDATE NO ACTION);
CREATE INDEX FK_APPLICATION_APPLICATION_CATEGORY ON APPM_APPLICATION(APPLICATION_CATEGORY_ID ASC); CREATE INDEX IF NOT EXISTS FK_APPLICATION_APPLICATION_CATEGORY ON APPM_APPLICATION(APPLICATION_CATEGORY_ID ASC);
CREATE INDEX FK_APPLICATION_PLATFORM_APPLICATION_MAPPING ON APPM_APPLICATION(PLATFORM_APPLICATION_MAPPING_ID ASC);
-- ----------------------------------------------------- -- -----------------------------------------------------
-- Table APPM_APPLICATION_PROPERTY -- Table APPM_APPLICATION_PROPERTY
-- ----------------------------------------------------- -- -----------------------------------------------------

@ -37,7 +37,6 @@ FOREIGN KEY(PLATFORM_ID) REFERENCES APPM_PLATFORM(ID) ON DELETE CASCADE,
PRIMARY KEY (ID, PLATFORM_ID, PROP_NAME) PRIMARY KEY (ID, PLATFORM_ID, PROP_NAME)
); );
-- ----------------------------------------------------- -- -----------------------------------------------------
-- Table `APPM_APPLICATION_CATEGORY` -- Table `APPM_APPLICATION_CATEGORY`
-- ----------------------------------------------------- -- -----------------------------------------------------

Loading…
Cancel
Save