implementing application lifecycle management

feature/appm-store/pbac
Chathura Ekanayake 7 years ago
parent 6605c1372b
commit 071e45129d

@ -24,6 +24,7 @@ import org.wso2.carbon.context.PrivilegedCarbonContext;
import org.wso2.carbon.device.application.mgt.api.beans.ErrorResponse;
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.LifecycleStateManager;
import org.wso2.carbon.device.application.mgt.common.services.PlatformManager;
import javax.ws.rs.core.Response;
@ -38,6 +39,7 @@ public class APIUtil {
private static ApplicationManager applicationManager;
private static PlatformManager platformManager;
private static LifecycleStateManager lifecycleStateManager;
public static ApplicationManager getApplicationManager() {
if (applicationManager == null) {
@ -76,6 +78,24 @@ public class APIUtil {
return platformManager;
}
public static LifecycleStateManager getLifecycleStateManager() {
if (lifecycleStateManager == null) {
synchronized (APIUtil.class) {
if (lifecycleStateManager == null) {
PrivilegedCarbonContext ctx = PrivilegedCarbonContext.getThreadLocalCarbonContext();
lifecycleStateManager =
(LifecycleStateManager) ctx.getOSGiService(LifecycleStateManager.class, null);
if (lifecycleStateManager == null) {
String msg = "Lifecycle Manager service has not initialized.";
log.error(msg);
throw new IllegalStateException(msg);
}
}
}
}
return lifecycleStateManager;
}
public static Response getResponse(ApplicationManagementException ex, Response.Status status) {
return getResponse(ex.getMessage(), status);
}

@ -0,0 +1,76 @@
/*
* Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
*
* WSO2 Inc. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* you may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.wso2.carbon.device.application.mgt.api.services;
import io.swagger.annotations.Api;
import io.swagger.annotations.Extension;
import io.swagger.annotations.ExtensionProperty;
import io.swagger.annotations.Info;
import io.swagger.annotations.SwaggerDefinition;
import io.swagger.annotations.Tag;
import org.wso2.carbon.apimgt.annotations.api.Scope;
import org.wso2.carbon.apimgt.annotations.api.Scopes;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
@SwaggerDefinition(
info = @Info(
version = "1.0.0",
title = "Lifecycle Management Service",
extensions = {
@Extension(properties = {
@ExtensionProperty(name = "name", value = "LifecycleManagementService"),
@ExtensionProperty(name = "context", value = "/api/application-mgt/v1.0/lifecycle"),
})
}
),
tags = {
@Tag(name = "lifecycle_management", description = "Lifecycle Management related APIs")
}
)
@Scopes(
scopes = {
@Scope(
name = "Get Lifecycle Details",
description = "Get lifecycle details",
key = "perm:lifecycle:get",
permissions = {"/device-mgt/lifecycle/get"}
),
@Scope(
name = "Add a lifecycle state",
description = "Add a lifecycle state",
key = "perm:lifecycle:add",
permissions = {"/device-mgt/lifecycle/add"}
),
}
)
@Path("/lifecycle")
@Api(value = "Lifecycle Management", description = "This API carries all lifecycle management related operations.")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public interface LifecycleManagementAPI {
@GET
@Path("/states")
Response getLifecycleStates();
}

@ -64,6 +64,29 @@ public class ApplicationManagementAPIImpl {
}
}
@GET
@Consumes("application/json")
@Path("applications/{uuid}")
public Response getApplication(@PathParam("uuid") String uuid) {
ApplicationManager applicationManager = APIUtil.getApplicationManager();
return null;
}
@PUT
@Consumes("application/json")
@Path("applications/{uuid}/lifecycle")
public Response changeLifecycleState(@PathParam("uuid") String applicationUUID, @QueryParam("state") String state) {
ApplicationManager applicationManager = APIUtil.getApplicationManager();
try {
applicationManager.changeLifecycle(applicationUUID, state);
} catch (ApplicationManagementException e) {
String msg = "Error occurred while changing the lifecycle of application: " + applicationUUID;
log.error(msg, e);
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();
}
@POST
@Consumes("application/json")
@Path("applications")

@ -0,0 +1,88 @@
/*
* Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
*
* WSO2 Inc. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* you may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.wso2.carbon.device.application.mgt.api.services.impl;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.wso2.carbon.device.application.mgt.api.APIUtil;
import org.wso2.carbon.device.application.mgt.api.services.LifecycleManagementAPI;
import org.wso2.carbon.device.application.mgt.common.LifecycleState;
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.LifecycleManagementException;
import org.wso2.carbon.device.application.mgt.common.services.ApplicationManager;
import org.wso2.carbon.device.application.mgt.common.services.LifecycleStateManager;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.core.Response;
import java.util.ArrayList;
import java.util.List;
@Path("/lifecycle")
public class LifecycleManagementAPIImpl implements LifecycleManagementAPI {
private static Log log = LogFactory.getLog(LifecycleManagementAPIImpl.class);
@GET
@Path("/states")
public Response getLifecycleStates() {
LifecycleStateManager lifecycleStateManager = APIUtil.getLifecycleStateManager();
List<LifecycleState> lifecycleStates = new ArrayList<>();
try {
lifecycleStates = lifecycleStateManager.getLifecycleStates();
} catch (LifecycleManagementException e) {
String msg = "Error occurred while retrieving lifecycle states.";
log.error(msg, e);
return Response.status(Response.Status.BAD_REQUEST).build();
}
return Response.status(Response.Status.OK).entity(lifecycleStates).build();
}
@POST
@Path("/states")
public Response addLifecycleState(LifecycleState state) {
LifecycleStateManager lifecycleStateManager = APIUtil.getLifecycleStateManager();
try {
lifecycleStateManager.addLifecycleState(state);
} catch (LifecycleManagementException e) {
String msg = "Error occurred while adding lifecycle state.";
log.error(msg, e);
return Response.status(Response.Status.BAD_REQUEST).build();
}
return Response.status(Response.Status.OK).entity("Lifecycle state added successfully.").build();
}
@DELETE
@Path("/states/{identifier}")
public Response deleteLifecycleState(@PathParam("identifier") String identifier) {
LifecycleStateManager lifecycleStateManager = APIUtil.getLifecycleStateManager();
try {
lifecycleStateManager.deleteLifecycleState(identifier);
} catch (LifecycleManagementException e) {
String msg = "Error occurred while deleting lifecycle state.";
log.error(msg, e);
return Response.status(Response.Status.BAD_REQUEST).build();
}
return Response.status(Response.Status.OK).entity("Lifecycle state deleted successfully.").build();
}
}

@ -26,6 +26,7 @@ http://cxf.apache.org/jaxrs http://cxf.apache.org/schemas/jaxrs.xsd">
<jaxrs:server id="applicationMgtService" address="/">
<jaxrs:serviceBeans>
<ref bean="applicationMgtServiceBean"/>
<ref bean="lifecycleMgtServiceBean"/>
</jaxrs:serviceBeans>
<jaxrs:providers>
<ref bean="jsonProvider"/>
@ -33,6 +34,7 @@ http://cxf.apache.org/jaxrs http://cxf.apache.org/schemas/jaxrs.xsd">
</jaxrs:server>
<bean id="applicationMgtServiceBean" class="org.wso2.carbon.device.application.mgt.api.services.impl.ApplicationManagementAPIImpl"/>
<bean id="lifecycleMgtServiceBean" class="org.wso2.carbon.device.application.mgt.api.services.impl.LifecycleManagementAPIImpl"/>
<!--<bean id="platformManagementAPIBean" class="org.wso2.carbon.device.application.mgt.api.services.impl.PlatformManagementAPIImpl" />-->
<bean id="jsonProvider" class="org.wso2.carbon.device.application.mgt.api.JSONMessageHandler"/>

@ -0,0 +1,32 @@
/*
* Copyright (c) 2017, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
* WSO2 Inc. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*
*/
package org.wso2.carbon.device.application.mgt.common.exception;
/**
* Exception caused during the lifecycle management.
*/
public class LifecycleManagementException extends ApplicationManagementException {
public LifecycleManagementException(String message, Throwable ex) {
super(message, ex);
}
public LifecycleManagementException(String message) {
super(message);
}
}

@ -34,7 +34,7 @@ public interface ApplicationManager {
* @return Created application
* @throws ApplicationManagementException Application Management Exception
*/
public Application createApplication(Application application) throws ApplicationManagementException;
Application createApplication(Application application) throws ApplicationManagementException;
/**
* Updates an already existing application.
@ -42,14 +42,14 @@ public interface ApplicationManager {
* @return Updated Application
* @throws ApplicationManagementException Application Management Exception
*/
public Application editApplication(Application application) throws ApplicationManagementException;
Application editApplication(Application application) throws ApplicationManagementException;
/**
* Delete an application identified by the unique ID.
* @param uuid Unique ID for tha application
* @throws ApplicationManagementException Application Management Exception
*/
public void deleteApplication(String uuid) throws ApplicationManagementException;
void deleteApplication(String uuid) throws ApplicationManagementException;
/**
* To get the applications based on the search filter.
@ -57,5 +57,7 @@ public interface ApplicationManager {
* @return Applications that matches the given filter criteria.
* @throws ApplicationManagementException Application Management Exception
*/
public ApplicationList getApplications(Filter filter) throws ApplicationManagementException;
ApplicationList getApplications(Filter filter) throws ApplicationManagementException;
void changeLifecycle(String applicationUUID, String lifecycleIdentifier) throws ApplicationManagementException;
}

@ -18,9 +18,19 @@
*/
package org.wso2.carbon.device.application.mgt.common.services;
import org.wso2.carbon.device.application.mgt.common.LifecycleState;
import org.wso2.carbon.device.application.mgt.common.exception.LifecycleManagementException;
import java.util.List;
/**
* This interface manages all the operations related with lifecycle state.
*/
public interface LifecycleStateManager {
List<LifecycleState> getLifecycleStates() throws LifecycleManagementException;
void addLifecycleState(LifecycleState state) throws LifecycleManagementException;
void deleteLifecycleState(String identifier) throws LifecycleManagementException;
}

@ -52,4 +52,6 @@ public interface ApplicationDAO {
void addRelease(ApplicationRelease release) throws ApplicationManagementDAOException;
void changeLifecycle(String applicationUUID, String lifecycleIdentifier) throws ApplicationManagementDAOException;
}

@ -20,9 +20,18 @@ package org.wso2.carbon.device.application.mgt.core.dao;
import org.wso2.carbon.device.application.mgt.common.LifecycleState;
import org.wso2.carbon.device.application.mgt.core.exception.ApplicationManagementDAOException;
import org.wso2.carbon.device.application.mgt.core.exception.DAOException;
import java.util.List;
public interface LifecycleStateDAO {
public LifecycleState getLifeCycleStateByIdentifier(String identifier) throws ApplicationManagementDAOException;
LifecycleState getLifeCycleStateByIdentifier(String identifier) throws ApplicationManagementDAOException;
List<LifecycleState> getLifecycleStates() throws DAOException;
void addLifecycleState(LifecycleState state) throws DAOException;
void deleteLifecycleState(String identifier) throws DAOException;
}

@ -47,6 +47,11 @@ public class H2ApplicationDAOImpl extends AbstractApplicationDAOImpl {
return null;
}
@Override
public void changeLifecycle(String applicationUUID, String lifecycleIdentifier) throws ApplicationManagementDAOException {
}
@Override
public ApplicationList getApplications(Filter filter) throws ApplicationManagementDAOException {

@ -375,6 +375,34 @@ public class MySQLApplicationDAOImpl extends AbstractApplicationDAOImpl {
}
}
@Override
public void changeLifecycle(String applicationUUID, String lifecycleIdentifier) throws ApplicationManagementDAOException {
Connection conn = null;
PreparedStatement stmt = null;
ResultSet rs = null;
try {
conn = this.getDBConnection();
String sql = "UPDATE APPM_APPLICATION SET " +
"LIFECYCLE_STATE_ID = (SELECT ID FROM APPM_LIFECYCLE_STATE WHERE IDENTIFIER = ?), " +
"LIFECYCLE_STATE_MODIFIED_BY = ?, " +
"LIFECYCLE_STATE_MODIFIED_AT = ? " +
"WHERE UUID = ?";
stmt = conn.prepareStatement(sql);
stmt.setString(1, lifecycleIdentifier);
stmt.setString(2, "admin");
stmt.setDate(3, new Date(System.currentTimeMillis()));
stmt.setString(4, applicationUUID);
stmt.executeUpdate();
} catch (DBConnectionException e) {
throw new ApplicationManagementDAOException("Error occurred while obtaining the DB connection.", e);
} catch (SQLException e) {
throw new ApplicationManagementDAOException("Error occurred while changing lifecycle of application: " + applicationUUID + " to: " + lifecycleIdentifier + " state.", e);
} finally {
Util.cleanupResources(stmt, rs);
}
}
@Override
public void deleteTags(int applicationId) throws ApplicationManagementDAOException {

@ -24,11 +24,14 @@ import org.wso2.carbon.device.application.mgt.core.dao.LifecycleStateDAO;
import org.wso2.carbon.device.application.mgt.core.dao.common.Util;
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.DAOException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
public class GenericLifecycleStateImpl extends AbstractDAOImpl implements LifecycleStateDAO {
@ -59,7 +62,6 @@ public class GenericLifecycleStateImpl extends AbstractDAOImpl implements Lifecy
lifecycleState.setIdentifier(rs.getString("IDENTIFIER"));
lifecycleState.setDescription(rs.getString("DESCRIPTION"));
}
return lifecycleState;
} catch (SQLException e) {
@ -69,7 +71,77 @@ public class GenericLifecycleStateImpl extends AbstractDAOImpl implements Lifecy
} finally {
Util.cleanupResources(stmt, rs);
}
}
@Override
public List<LifecycleState> getLifecycleStates() throws DAOException {
List<LifecycleState> lifecycleStates = new ArrayList<>();
Connection conn = null;
PreparedStatement stmt = null;
ResultSet rs = null;
try {
conn = this.getDBConnection();
String sql = "SELECT IDENTIFIER, NAME, DESCRIPTION FROM APPM_LIFECYCLE_STATE";
stmt = conn.prepareStatement(sql);
rs = stmt.executeQuery();
while(rs.next()) {
LifecycleState lifecycleState = new LifecycleState();
lifecycleState.setIdentifier(rs.getString("IDENTIFIER"));
lifecycleState.setName(rs.getString("NAME"));
lifecycleState.setDescription(rs.getString("DESCRIPTION"));
lifecycleStates.add(lifecycleState);
}
} catch (DBConnectionException e) {
throw new DAOException("Error occurred while obtaining the DB connection.", e);
} catch (SQLException e) {
throw new DAOException("Error occurred while retrieving lifecycle states.", e);
} finally {
Util.cleanupResources(stmt, rs);
}
return lifecycleStates;
}
@Override
public void addLifecycleState(LifecycleState state) throws DAOException {
Connection conn = null;
PreparedStatement stmt = null;
ResultSet rs = null;
try {
conn = this.getDBConnection();
String sql = "INSERT INTO APPM_LIFECYCLE_STATE ('NAME', 'IDENTIFIER', 'DESCRIPTION') VALUES (?, ?, ?)";
stmt = conn.prepareStatement(sql);
stmt.setString(1, state.getName());
stmt.setString(2, state.getIdentifier());
stmt.setString(3, state.getDescription());
stmt.executeUpdate();
} catch (DBConnectionException e) {
throw new DAOException("Error occurred while obtaining the DB connection.", e);
} catch (SQLException e) {
throw new DAOException("Error occurred while adding lifecycle: " + state.getIdentifier(), e);
} finally {
Util.cleanupResources(stmt, rs);
}
}
@Override
public void deleteLifecycleState(String identifier) throws DAOException {
Connection conn = null;
PreparedStatement stmt = null;
ResultSet rs = null;
try {
conn = this.getDBConnection();
String sql = "DELETE FROM APPM_LIFECYCLE_STATE WHERE IDENTIFIER = ?";
stmt = conn.prepareStatement(sql);
stmt.setString(1, identifier);
stmt.executeUpdate();
} catch (DBConnectionException e) {
throw new DAOException("Error occurred while obtaining the DB connection.", e);
} catch (SQLException e) {
throw new DAOException("Error occurred while deleting lifecycle: " + identifier, e);
} finally {
Util.cleanupResources(stmt, rs);
}
}
}

@ -0,0 +1,29 @@
/*
* Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
*
* WSO2 Inc. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* you may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.wso2.carbon.device.application.mgt.core.exception;
public class DAOException extends Exception {
public DAOException(String message) {
super(message);
}
public DAOException(String message, Throwable cause) {
super(message, cause);
}
}

@ -148,6 +148,17 @@ public class ApplicationManagerImpl implements ApplicationManager {
}
@Override
public void changeLifecycle(String applicationUUID, String lifecycleIdentifier) throws ApplicationManagementException {
try {
ConnectionManagerUtil.openDBConnection();
ApplicationDAO applicationDAO = DAOFactory.getApplicationDAO();
applicationDAO.changeLifecycle(applicationUUID, lifecycleIdentifier);
} finally {
ConnectionManagerUtil.closeDBConnection();
}
}
private void validateApplication(Application application, boolean isEdit) throws ValidationException {
if (application.getName() == null) {

@ -17,7 +17,62 @@
*/
package org.wso2.carbon.device.application.mgt.core.impl;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.wso2.carbon.device.application.mgt.common.LifecycleState;
import org.wso2.carbon.device.application.mgt.common.exception.DBConnectionException;
import org.wso2.carbon.device.application.mgt.common.exception.LifecycleManagementException;
import org.wso2.carbon.device.application.mgt.common.services.LifecycleStateManager;
import org.wso2.carbon.device.application.mgt.core.dao.LifecycleStateDAO;
import org.wso2.carbon.device.application.mgt.core.dao.common.DAOFactory;
import org.wso2.carbon.device.application.mgt.core.exception.DAOException;
import org.wso2.carbon.device.application.mgt.core.util.ConnectionManagerUtil;
import java.util.List;
public class LifecycleStateManagerImpl implements LifecycleStateManager {
private static final Log log = LogFactory.getLog(LifecycleStateManagerImpl.class);
@Override
public List<LifecycleState> getLifecycleStates() throws LifecycleManagementException {
List<LifecycleState> lifecycleStates = null;
try {
ConnectionManagerUtil.openDBConnection();
LifecycleStateDAO lifecycleStateDAO = DAOFactory.getLifecycleStateDAO();
lifecycleStates = lifecycleStateDAO.getLifecycleStates();
} catch (DAOException | DBConnectionException e) {
throw new LifecycleManagementException("Failed get lifecycle states.", e);
} finally {
ConnectionManagerUtil.closeDBConnection();
}
return lifecycleStates;
}
@Override
public void addLifecycleState(LifecycleState state) throws LifecycleManagementException {
try {
ConnectionManagerUtil.openDBConnection();
LifecycleStateDAO lifecycleStateDAO = DAOFactory.getLifecycleStateDAO();
lifecycleStateDAO.addLifecycleState(state);
} catch (DAOException | DBConnectionException e) {
throw new LifecycleManagementException("Failed to add lifecycle state", e);
} finally {
ConnectionManagerUtil.closeDBConnection();
}
}
@Override
public void deleteLifecycleState(String identifier) throws LifecycleManagementException {
try {
ConnectionManagerUtil.openDBConnection();
LifecycleStateDAO lifecycleStateDAO = DAOFactory.getLifecycleStateDAO();
lifecycleStateDAO.deleteLifecycleState(identifier);
} catch (DAOException | DBConnectionException e) {
throw new LifecycleManagementException("Failed to add lifecycle state: " + identifier, e);
} finally {
ConnectionManagerUtil.closeDBConnection();
}
}
}

@ -41,15 +41,23 @@ public class ConnectionManagerUtil {
private static ThreadLocal<TxState> currentTxState = new ThreadLocal<>();
private static DataSource dataSource;
public static void openDBConnection() throws DBConnectionException {
Connection conn = currentConnection.get();
if (conn != null) {
throw new IllegalTransactionStateException("Database connection has already been obtained.");
}
try {
conn = dataSource.getConnection();
} catch (SQLException e) {
throw new DBConnectionException("Failed to get a database connection.", e);
}
currentConnection.set(conn);
}
public static Connection getDBConnection() throws DBConnectionException {
Connection conn = currentConnection.get();
if (conn == null) {
try {
conn = dataSource.getConnection();
currentConnection.set(conn);
} catch (SQLException e) {
throw new DBConnectionException("Failed to get database connection.", e);
}
throw new IllegalTransactionStateException("Database connection is not active.");
}
return conn;
}
@ -57,7 +65,7 @@ public class ConnectionManagerUtil {
public static void beginDBTransaction() throws TransactionManagementException, DBConnectionException {
Connection conn = currentConnection.get();
if (conn == null) {
conn = getDBConnection();
throw new IllegalTransactionStateException("Database connection is not active.");
}
if (inTransaction(conn)) {

Loading…
Cancel
Save