提交 9e29334d 编写于 作者: F Frederik Heremans

Added Model crud to REST

上级 04ddfbbd
......@@ -13,6 +13,9 @@
package org.activiti.engine.impl.persistence.entity;
import java.util.List;
import java.util.Map;
import org.activiti.engine.impl.ModelQueryImpl;
import org.activiti.engine.impl.Page;
import org.activiti.engine.impl.context.Context;
......@@ -20,13 +23,10 @@ import org.activiti.engine.impl.db.DbSqlSession;
import org.activiti.engine.impl.db.PersistentObject;
import org.activiti.engine.impl.interceptor.CommandContext;
import org.activiti.engine.impl.persistence.AbstractManager;
import org.activiti.engine.impl.util.ClockUtil;
import org.activiti.engine.repository.Model;
import org.activiti.engine.repository.ModelQuery;
import java.util.Date;
import java.util.List;
import java.util.Map;
/**
* @author Tijs Rademakers
......@@ -38,12 +38,14 @@ public class ModelEntityManager extends AbstractManager {
}
public void insertModel(Model model) {
((ModelEntity) model).setCreateTime(new Date());
((ModelEntity) model).setCreateTime(ClockUtil.getCurrentTime());
((ModelEntity) model).setLastUpdateTime(ClockUtil.getCurrentTime());
getDbSqlSession().insert((PersistentObject) model);
}
public void updateModel(ModelEntity updatedModel) {
CommandContext commandContext = Context.getCommandContext();
updatedModel.setLastUpdateTime(ClockUtil.getCurrentTime());
DbSqlSession dbSqlSession = commandContext.getDbSqlSession();
dbSqlSession.update(updatedModel);
}
......
......@@ -31,6 +31,7 @@ import org.activiti.engine.identity.User;
import org.activiti.engine.impl.bpmn.deployer.BpmnDeployer;
import org.activiti.engine.impl.persistence.entity.ProcessDefinitionEntity;
import org.activiti.engine.repository.Deployment;
import org.activiti.engine.repository.Model;
import org.activiti.engine.repository.ProcessDefinition;
import org.activiti.engine.runtime.Execution;
import org.activiti.engine.runtime.Job;
......@@ -69,6 +70,7 @@ import org.activiti.rest.api.management.TableResponse;
import org.activiti.rest.api.repository.DeploymentResourceResponse;
import org.activiti.rest.api.repository.DeploymentResourceResponse.DeploymentResourceType;
import org.activiti.rest.api.repository.DeploymentResponse;
import org.activiti.rest.api.repository.ModelResponse;
import org.activiti.rest.api.repository.ProcessDefinitionResponse;
import org.activiti.rest.api.runtime.process.ExecutionResponse;
import org.activiti.rest.api.runtime.process.ProcessInstanceResponse;
......@@ -626,6 +628,27 @@ public class RestResponseFactory {
return response;
}
public ModelResponse createModelResponse(SecuredResource securedResource, Model model) {
ModelResponse response = new ModelResponse();
response.setCategory(model.getCategory());
response.setCreateTime(model.getCreateTime());
response.setId(model.getId());
response.setKey(model.getKey());
response.setLastUpdateTime(model.getLastUpdateTime());
response.setMetaInfo(model.getMetaInfo());
response.setName(model.getName());
response.setDeploymentId(model.getDeploymentId());
response.setVersion(model.getVersion());
response.setUrl(securedResource.createFullResourceUrl(RestUrls.URL_MODEL, model.getId()));
if(model.getDeploymentId() != null) {
response.setDeploymentUrl(securedResource.createFullResourceUrl(RestUrls.URL_DEPLOYMENT, model.getDeploymentId()));
}
return response;
}
/**
* Called once when the converters need to be initialized. Override of custom conversion
* needs to be done between java and rest.
......
......@@ -68,6 +68,7 @@ public final class RestUrls {
public static final String SEGMENT_PROPERTIES = "properties";
public static final String SEGMENT_ENGINE_INFO = "engine";
public static final String SEGMENT_ACTIVITIES = "activities";
public static final String SEGMENT_MODEL_RESOURCE = "models";
/**
* URL template for the deployment collection: <i>repository/deployments</i>
......@@ -127,6 +128,16 @@ public final class RestUrls {
*/
public static final String[] URL_PROCESS_DEFINITION_MODEL = {SEGMENT_REPOSITORY_RESOURCES, SEGMENT_PROCESS_DEFINITION_RESOURCE, "{0}", SEGMENT_MODEL};
/**
* URL template for the model collection: <i>repository/models</i>
*/
public static final String[] URL_MODEL_COLLECTION = {SEGMENT_REPOSITORY_RESOURCES, SEGMENT_MODEL_RESOURCE};
/**
* URL template for a single model <i>repository/models/{0:modelId}</i>
*/
public static final String[] URL_MODEL = {SEGMENT_REPOSITORY_RESOURCES, SEGMENT_MODEL_RESOURCE, "{0}"};
/**
* URL template for task collection: <i>runtime/tasks/{0:taskId}</i>
*/
......
/* Licensed 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.activiti.rest.api.repository;
import org.activiti.engine.ActivitiIllegalArgumentException;
import org.activiti.engine.ActivitiObjectNotFoundException;
import org.activiti.engine.repository.Model;
import org.activiti.engine.repository.ProcessDefinition;
import org.activiti.rest.api.ActivitiUtil;
import org.activiti.rest.api.SecuredResource;
/**
* @author Frederik Heremans
*/
public class BaseModelResource extends SecuredResource {
/**
* Returns the {@link Model} that is requested. Throws the right exceptions
* when bad request was made or model is not found.
*/
protected Model getModelFromRequest() {
String modelId = getAttribute("modelId");
if(modelId == null) {
throw new ActivitiIllegalArgumentException("The modelId cannot be null");
}
Model model = ActivitiUtil.getRepositoryService().createModelQuery()
.modelId(modelId).singleResult();
if(model == null) {
throw new ActivitiObjectNotFoundException("Could not find a model with id '" + modelId + "'.", ProcessDefinition.class);
}
return model;
}
}
/* Licensed 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.activiti.rest.api.repository;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.activiti.engine.impl.ModelQueryProperty;
import org.activiti.engine.query.QueryProperty;
import org.activiti.engine.repository.Model;
import org.activiti.engine.repository.ModelQuery;
import org.activiti.rest.api.ActivitiUtil;
import org.activiti.rest.api.DataResponse;
import org.activiti.rest.application.ActivitiRestServicesApplication;
import org.restlet.data.Form;
import org.restlet.data.Status;
import org.restlet.resource.Get;
import org.restlet.resource.Post;
/**
* @author Frederik Heremans
*/
public class ModelCollectionResource extends BaseModelResource {
private static Map<String, QueryProperty> allowedSortProperties = new HashMap<String, QueryProperty>();
static {
allowedSortProperties.put("id", ModelQueryProperty.MODEL_ID);
allowedSortProperties.put("category", ModelQueryProperty.MODEL_CATEGORY);
allowedSortProperties.put("createTime", ModelQueryProperty.MODEL_CREATE_TIME);
allowedSortProperties.put("key", ModelQueryProperty.MODEL_KEY);
allowedSortProperties.put("lastUpdateTime", ModelQueryProperty.MODEL_LAST_UPDATE_TIME);
allowedSortProperties.put("name", ModelQueryProperty.MODEL_NAME);
allowedSortProperties.put("version", ModelQueryProperty.MODEL_VERSION);
}
@Get
public DataResponse getModels() {
ModelQuery modelQuery = ActivitiUtil.getRepositoryService().createModelQuery();
Form form = getQuery();
Set<String> names = form.getNames();
if(names.contains("id")) {
modelQuery.modelId(getQueryParameter("id", form));
}
if(names.contains("category")) {
modelQuery.modelCategory(getQueryParameter("category", form));
}
if(names.contains("categoryLike")) {
modelQuery.modelCategoryLike(getQueryParameter("categoryLike", form));
}
if(names.contains("categoryNotEquals")) {
modelQuery.modelCategoryNotEquals(getQueryParameter("categoryNotEquals", form));
}
if(names.contains("name")) {
modelQuery.modelName(getQueryParameter("name", form));
}
if(names.contains("nameLike")) {
modelQuery.modelNameLike(getQueryParameter("nameLike", form));
}
if(names.contains("key")) {
modelQuery.modelKey(getQueryParameter("key", form));
}
if(names.contains("version")) {
modelQuery.modelVersion(getQueryParameterAsInt("version", form));
}
if(names.contains("latestVersion")) {
boolean isLatestVersion = getQueryParameterAsBoolean("latestVersion", form);
if(isLatestVersion) {
modelQuery.latestVersion();
}
}
if(names.contains("deploymentId")) {
modelQuery.deploymentId(getQueryParameter("deploymentId", form));
}
if(names.contains("deployed")) {
boolean isDeployed = getQueryParameterAsBoolean("deployed", form);
if(isDeployed) {
modelQuery.deployed();
} else {
modelQuery.notDeployed();
}
}
return new ModelsPaginateList(this).paginateList(form, modelQuery, "id", allowedSortProperties);
}
@Post
public ModelResponse createModel(ModelRequest request) {
Model model = ActivitiUtil.getRepositoryService().newModel();
model.setCategory(request.getCategory());
model.setDeploymentId(request.getDeploymentId());
model.setKey(request.getKey());
model.setMetaInfo(request.getMetaInfo());
model.setName(request.getName());
model.setVersion(request.getVersion());
ActivitiUtil.getRepositoryService().saveModel(model);
setStatus(Status.SUCCESS_CREATED);
return getApplication(ActivitiRestServicesApplication.class).getRestResponseFactory().createModelResponse(this, model);
}
}
/* Licensed 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.activiti.rest.api.repository;
import org.codehaus.jackson.annotate.JsonIgnore;
/**
* @author Frederik Heremans
*/
public class ModelRequest {
protected String name;
protected String key;
protected String category;
protected Integer version;
protected String metaInfo;
protected String deploymentId;
protected boolean nameChanged;
protected boolean keyChanged;
protected boolean categoryChanged;
protected boolean versionChanged;
protected boolean metaInfoChanged;
protected boolean deploymentChanged;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
this.nameChanged = true;
}
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
this.keyChanged = true;
}
public String getCategory() {
return category;
}
public void setCategory(String category) {
this.category = category;
this.categoryChanged = true;
}
public Integer getVersion() {
return version;
}
public void setVersion(Integer version) {
this.version = version;
this.versionChanged = true;
}
public String getMetaInfo() {
return metaInfo;
}
public void setMetaInfo(String metaInfo) {
this.metaInfo = metaInfo;
this.metaInfoChanged = true;
}
public String getDeploymentId() {
return deploymentId;
}
public void setDeploymentId(String deploymentId) {
this.deploymentId = deploymentId;
this.deploymentChanged = true;
}
@JsonIgnore
public boolean isCategoryChanged() {
return categoryChanged;
}
@JsonIgnore
public boolean isKeyChanged() {
return keyChanged;
}
@JsonIgnore
public boolean isMetaInfoChanged() {
return metaInfoChanged;
}
@JsonIgnore
public boolean isNameChanged() {
return nameChanged;
}
@JsonIgnore
public boolean isVersionChanged() {
return versionChanged;
}
@JsonIgnore
public boolean isDeploymentChanged() {
return deploymentChanged;
}
}
/* Licensed 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.activiti.rest.api.repository;
import org.activiti.engine.repository.Model;
import org.activiti.rest.api.ActivitiUtil;
import org.activiti.rest.application.ActivitiRestServicesApplication;
import org.restlet.data.Status;
import org.restlet.resource.Delete;
import org.restlet.resource.Get;
import org.restlet.resource.Put;
/**
* @author Frederik Heremans
*/
public class ModelResource extends BaseModelResource {
@Get
public ModelResponse getModel() {
Model model = getModelFromRequest();
return getApplication(ActivitiRestServicesApplication.class).getRestResponseFactory()
.createModelResponse(this, model);
}
@Put
public ModelResponse updateModel(ModelRequest request) {
Model model = getModelFromRequest();
if(request.isCategoryChanged()) {
model.setCategory(request.getCategory());
}
if(request.isDeploymentChanged()) {
model.setDeploymentId(request.getDeploymentId());
}
if(request.isKeyChanged()) {
model.setKey(request.getKey());
}
if(request.isMetaInfoChanged()) {
model.setMetaInfo(request.getMetaInfo());
}
if(request.isNameChanged()) {
model.setName(request.getName());
}
if(request.isVersionChanged()) {
model.setVersion(request.getVersion());
}
ActivitiUtil.getRepositoryService().saveModel(model);
return getApplication(ActivitiRestServicesApplication.class).getRestResponseFactory()
.createModelResponse(this, model);
}
@Delete
public void deleteModel() {
Model model = getModelFromRequest();
ActivitiUtil.getRepositoryService().deleteModel(model.getId());
setStatus(Status.SUCCESS_NO_CONTENT);
}
}
/* Licensed 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.activiti.rest.api.repository;
import java.util.Date;
/**
* @author Frederik Heremans
*/
public class ModelResponse extends ModelRequest {
protected String id;
protected String url;
protected Date createTime;
protected Date lastUpdateTime;
protected String deploymentUrl;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public Date getCreateTime() {
return createTime;
}
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
public Date getLastUpdateTime() {
return lastUpdateTime;
}
public void setLastUpdateTime(Date lastUpdateTime) {
this.lastUpdateTime = lastUpdateTime;
}
public String getDeploymentUrl() {
return deploymentUrl;
}
public void setDeploymentUrl(String deploymentUrl) {
this.deploymentUrl = deploymentUrl;
}
}
/* Licensed 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.activiti.rest.api.repository;
import java.util.ArrayList;
import java.util.List;
import org.activiti.engine.repository.Model;
import org.activiti.rest.api.AbstractPaginateList;
import org.activiti.rest.api.RestResponseFactory;
import org.activiti.rest.api.SecuredResource;
import org.activiti.rest.application.ActivitiRestServicesApplication;
/**
* @author Frederik Heremans
*/
public class ModelsPaginateList extends AbstractPaginateList {
private SecuredResource resource;
public ModelsPaginateList(SecuredResource resource) {
this.resource = resource;
}
@SuppressWarnings("rawtypes")
@Override
protected List processList(List list) {
List<ModelResponse> responseList = new ArrayList<ModelResponse>();
RestResponseFactory restResponseFactory = resource.getApplication(ActivitiRestServicesApplication.class).getRestResponseFactory();
for (Object entity : list) {
responseList.add(restResponseFactory.createModelResponse(resource, (Model) entity));
}
return responseList;
}
}
......@@ -77,6 +77,9 @@ import org.activiti.rest.api.repository.DeploymentResource;
import org.activiti.rest.api.repository.DeploymentResourceCollectionResource;
import org.activiti.rest.api.repository.DeploymentResourceDataResource;
import org.activiti.rest.api.repository.DeploymentResourceResource;
import org.activiti.rest.api.repository.ModelCollectionResource;
import org.activiti.rest.api.repository.ModelResource;
import org.activiti.rest.api.repository.ModelResponse;
import org.activiti.rest.api.repository.ProcessDefinitionCollectionResource;
import org.activiti.rest.api.repository.ProcessDefinitionIdentityLinkCollectionResource;
import org.activiti.rest.api.repository.ProcessDefinitionIdentityLinkResource;
......@@ -144,6 +147,9 @@ public class RestServicesInit {
router.attach("/repository/process-definitions/{processDefinitionId}/identitylinks", ProcessDefinitionIdentityLinkCollectionResource.class);
router.attach("/repository/process-definitions/{processDefinitionId}/identitylinks/{family}/{identityId}", ProcessDefinitionIdentityLinkResource.class);
router.attach("/repository/models", ModelCollectionResource.class);
router.attach("/repository/models/{modelId}", ModelResource.class);
router.attach("/runtime/tasks", TaskCollectionResource.class);
router.attach("/runtime/tasks/{taskId}", TaskResource.class);
router.attach("/runtime/tasks/{taskId}/variables", TaskVariableCollectionResource.class);
......
/* Licensed 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.activiti.rest.api.repository;
import java.util.Calendar;
import org.activiti.engine.impl.util.ClockUtil;
import org.activiti.engine.repository.Model;
import org.activiti.engine.test.Deployment;
import org.activiti.rest.BaseRestTestCase;
import org.activiti.rest.api.RestUrls;
import org.codehaus.jackson.JsonNode;
import org.codehaus.jackson.node.ObjectNode;
import org.restlet.data.Status;
import org.restlet.representation.Representation;
import org.restlet.resource.ClientResource;
/**
* @author Frederik Heremans
*/
public class ModelCollectionResourceTest extends BaseRestTestCase {
@Deployment(resources={"org/activiti/rest/api/repository/oneTaskProcess.bpmn20.xml"})
public void testGetModels() throws Exception {
// Create 2 models
Model model1 = null;
Model model2 = null;
try {
model1 = repositoryService.newModel();
model1.setCategory("Model category");
model1.setKey("Model key");
model1.setMetaInfo("Model metainfo");
model1.setName("Model name");
model1.setVersion(2);
model1.setDeploymentId(deploymentId);
repositoryService.saveModel(model1);
model2 = repositoryService.newModel();
model2.setCategory("Another category");
model2.setKey("Another key");
model2.setMetaInfo("Another metainfo");
model2.setName("Another name");
model2.setVersion(3);
repositoryService.saveModel(model2);
// Try filter-less, should return all models
String url = RestUrls.createRelativeResourceUrl(RestUrls.URL_MODEL_COLLECTION);
assertResultsPresentInDataResponse(url, model1.getId(), model2.getId());
// Filter based on id
url = RestUrls.createRelativeResourceUrl(RestUrls.URL_MODEL_COLLECTION) + "?id=" + model1.getId();
assertResultsPresentInDataResponse(url, model1.getId());
// Filter based on category
url = RestUrls.createRelativeResourceUrl(RestUrls.URL_MODEL_COLLECTION) + "?category=Another category";
assertResultsPresentInDataResponse(url, model2.getId());
// Filter based on category like
url = RestUrls.createRelativeResourceUrl(RestUrls.URL_MODEL_COLLECTION) + "?categoryLike=" + encode("Mode%");
assertResultsPresentInDataResponse(url, model1.getId());
// Filter based on category not equals
url = RestUrls.createRelativeResourceUrl(RestUrls.URL_MODEL_COLLECTION) + "?categoryNotEquals=Another category";
assertResultsPresentInDataResponse(url, model1.getId());
// Filter based on name
url = RestUrls.createRelativeResourceUrl(RestUrls.URL_MODEL_COLLECTION) + "?name=Another name";
assertResultsPresentInDataResponse(url, model2.getId());
// Filter based on name like
url = RestUrls.createRelativeResourceUrl(RestUrls.URL_MODEL_COLLECTION) + "?nameLike=" + encode("%del name");
assertResultsPresentInDataResponse(url, model1.getId());
// Filter based on key
url = RestUrls.createRelativeResourceUrl(RestUrls.URL_MODEL_COLLECTION) + "?key=Model key";
assertResultsPresentInDataResponse(url, model1.getId());
// Filter based on version
url = RestUrls.createRelativeResourceUrl(RestUrls.URL_MODEL_COLLECTION) + "?version=3";
assertResultsPresentInDataResponse(url, model2.getId());
// Filter based on deploymentId
url = RestUrls.createRelativeResourceUrl(RestUrls.URL_MODEL_COLLECTION) + "?deploymentId=" + deploymentId;
assertResultsPresentInDataResponse(url, model1.getId());
// Filter based on deployed=true
url = RestUrls.createRelativeResourceUrl(RestUrls.URL_MODEL_COLLECTION) + "?deployed=true";
assertResultsPresentInDataResponse(url, model1.getId());
// Filter based on deployed=false
url = RestUrls.createRelativeResourceUrl(RestUrls.URL_MODEL_COLLECTION) + "?deployed=false";
assertResultsPresentInDataResponse(url, model2.getId());
// Filter based on latestVersion
url = RestUrls.createRelativeResourceUrl(RestUrls.URL_MODEL_COLLECTION) + "?key=Model key&latestVersion=true";
// Make sure both models have same key
model2 = repositoryService.createModelQuery().modelId(model2.getId()).singleResult();
model2.setKey("Model key");
repositoryService.saveModel(model2);
assertResultsPresentInDataResponse(url, model2.getId());
} finally {
if(model1 != null) {
try {
repositoryService.deleteModel(model1.getId());
} catch(Throwable ignore) { }
}
if(model2 != null) {
try {
repositoryService.deleteModel(model2.getId());
} catch(Throwable ignore) { }
}
}
}
@Deployment(resources={"org/activiti/rest/api/repository/oneTaskProcess.bpmn20.xml"})
public void testCreateModel() throws Exception {
Model model = null;
try {
Calendar createTime = Calendar.getInstance();
createTime.set(Calendar.MILLISECOND, 0);
ClockUtil.setCurrentTime(createTime.getTime());
// Create create request
ObjectNode requestNode = objectMapper.createObjectNode();
requestNode.put("name", "Model name");
requestNode.put("category", "Model category");
requestNode.put("key", "Model key");
requestNode.put("metaInfo", "Model metainfo");
requestNode.put("deploymentId", deploymentId);
requestNode.put("version", 2);
ClientResource client = getAuthenticatedClient(RestUrls.createRelativeResourceUrl(
RestUrls.URL_MODEL_COLLECTION));
Representation response = client.post(requestNode);
// Check "CREATED" status
assertEquals(Status.SUCCESS_CREATED, client.getResponse().getStatus());
JsonNode responseNode = objectMapper.readTree(response.getStream());
assertNotNull(responseNode);
assertEquals("Model name", responseNode.get("name").getTextValue());
assertEquals("Model key", responseNode.get("key").getTextValue());
assertEquals("Model category", responseNode.get("category").getTextValue());
assertEquals(2, responseNode.get("version").getIntValue());
assertEquals("Model metainfo", responseNode.get("metaInfo").getTextValue());
assertEquals(deploymentId, responseNode.get("deploymentId").getTextValue());
assertEquals(createTime.getTime().getTime(), getDateFromISOString(responseNode.get("createTime").getTextValue()).getTime());
assertEquals(createTime.getTime().getTime(), getDateFromISOString(responseNode.get("lastUpdateTime").getTextValue()).getTime());
assertTrue(responseNode.get("url").getTextValue().endsWith(RestUrls.createRelativeResourceUrl(RestUrls.URL_MODEL, responseNode.get("id").getTextValue())));
assertTrue(responseNode.get("deploymentUrl").getTextValue().endsWith(RestUrls.createRelativeResourceUrl(RestUrls.URL_DEPLOYMENT, deploymentId)));
model = repositoryService.createModelQuery().modelId(responseNode.get("id").getTextValue()).singleResult();
assertNotNull(model);
assertEquals("Model category", model.getCategory());
assertEquals("Model name", model.getName());
assertEquals("Model key", model.getKey());
assertEquals(deploymentId, model.getDeploymentId());
assertEquals("Model metainfo", model.getMetaInfo());
assertEquals(2, model.getVersion().intValue());
} finally {
if(model != null) {
try {
repositoryService.deleteModel(model.getId());
} catch(Throwable ignore) { }
}
}
}
}
/* Licensed 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.activiti.rest.api.repository;
import java.util.Calendar;
import org.activiti.engine.impl.util.ClockUtil;
import org.activiti.engine.repository.Model;
import org.activiti.engine.test.Deployment;
import org.activiti.rest.BaseRestTestCase;
import org.activiti.rest.api.RestUrls;
import org.codehaus.jackson.JsonNode;
import org.codehaus.jackson.node.ObjectNode;
import org.restlet.data.Status;
import org.restlet.representation.Representation;
import org.restlet.resource.ClientResource;
import org.restlet.resource.ResourceException;
/**
* @author Frederik Heremans
*/
public class ModelResourceTest extends BaseRestTestCase {
@Deployment(resources={"org/activiti/rest/api/repository/oneTaskProcess.bpmn20.xml"})
public void testGetModel() throws Exception {
Model model = null;
try {
Calendar now = Calendar.getInstance();
now.set(Calendar.MILLISECOND, 0);
ClockUtil.setCurrentTime(now.getTime());
model = repositoryService.newModel();
model.setCategory("Model category");
model.setKey("Model key");
model.setMetaInfo("Model metainfo");
model.setName("Model name");
model.setVersion(2);
model.setDeploymentId(deploymentId);
repositoryService.saveModel(model);
ClientResource client = getAuthenticatedClient(RestUrls.createRelativeResourceUrl(
RestUrls.URL_MODEL, model.getId()));
Representation response = client.get();
// Check "OK" status
assertEquals(Status.SUCCESS_OK, client.getResponse().getStatus());
JsonNode responseNode = objectMapper.readTree(response.getStream());
assertNotNull(responseNode);
assertEquals("Model name", responseNode.get("name").getTextValue());
assertEquals("Model key", responseNode.get("key").getTextValue());
assertEquals("Model category", responseNode.get("category").getTextValue());
assertEquals(2, responseNode.get("version").getIntValue());
assertEquals("Model metainfo", responseNode.get("metaInfo").getTextValue());
assertEquals(deploymentId, responseNode.get("deploymentId").getTextValue());
assertEquals(model.getId(), responseNode.get("id").getTextValue());
assertEquals(now.getTime().getTime(), getDateFromISOString(responseNode.get("createTime").getTextValue()).getTime());
assertEquals(now.getTime().getTime(), getDateFromISOString(responseNode.get("lastUpdateTime").getTextValue()).getTime());
assertTrue(responseNode.get("url").getTextValue().endsWith(RestUrls.createRelativeResourceUrl(RestUrls.URL_MODEL, model.getId())));
assertTrue(responseNode.get("deploymentUrl").getTextValue().endsWith(RestUrls.createRelativeResourceUrl(RestUrls.URL_DEPLOYMENT, deploymentId)));
} finally
{
try {
repositoryService.deleteModel(model.getId());
} catch(Throwable ignore) {
// Ignore, model might not be created
}
}
}
public void testGetUnexistingModel() throws Exception {
ClientResource client = getAuthenticatedClient(RestUrls.createRelativeResourceUrl(RestUrls.URL_MODEL, "unexisting"));
try {
client.get();
fail("404 expected, but was: " + client.getResponse().getStatus());
} catch(ResourceException expected) {
assertEquals(Status.CLIENT_ERROR_NOT_FOUND, client.getResponse().getStatus());
assertEquals("Could not find a model with id 'unexisting'.", client.getResponse().getStatus().getDescription());
}
}
public void testDeleteModel() throws Exception {
Model model = null;
try {
Calendar now = Calendar.getInstance();
now.set(Calendar.MILLISECOND, 0);
ClockUtil.setCurrentTime(now.getTime());
model = repositoryService.newModel();
model.setCategory("Model category");
model.setKey("Model key");
model.setMetaInfo("Model metainfo");
model.setName("Model name");
model.setVersion(2);
repositoryService.saveModel(model);
ClientResource client = getAuthenticatedClient(RestUrls.createRelativeResourceUrl(
RestUrls.URL_MODEL, model.getId()));
Representation response = client.delete();
assertEquals(Status.SUCCESS_NO_CONTENT, client.getResponse().getStatus());
assertEquals(0, response.getSize());
// Check if the model is really gone
assertNull(repositoryService.createModelQuery().modelId(model.getId()).singleResult());
model = null;
} finally
{
if(model != null) {
try {
repositoryService.deleteModel(model.getId());
} catch(Throwable ignore) {
// Ignore, model might not be created
}
}
}
}
public void testDeleteUnexistingModel() throws Exception {
ClientResource client = getAuthenticatedClient(RestUrls.createRelativeResourceUrl(RestUrls.URL_MODEL, "unexisting"));
try {
client.delete();
fail("404 expected, but was: " + client.getResponse().getStatus());
} catch(ResourceException expected) {
assertEquals(Status.CLIENT_ERROR_NOT_FOUND, client.getResponse().getStatus());
assertEquals("Could not find a model with id 'unexisting'.", client.getResponse().getStatus().getDescription());
}
}
@Deployment(resources={"org/activiti/rest/api/repository/oneTaskProcess.bpmn20.xml"})
public void testUpdateModel() throws Exception {
Model model = null;
try {
Calendar createTime = Calendar.getInstance();
createTime.set(Calendar.MILLISECOND, 0);
ClockUtil.setCurrentTime(createTime.getTime());
model = repositoryService.newModel();
model.setCategory("Model category");
model.setKey("Model key");
model.setMetaInfo("Model metainfo");
model.setName("Model name");
model.setVersion(2);
repositoryService.saveModel(model);
Calendar updateTime = Calendar.getInstance();
updateTime.set(Calendar.MILLISECOND, 0);
updateTime.add(Calendar.HOUR, 1);
ClockUtil.setCurrentTime(updateTime.getTime());
// Create update request
ObjectNode requestNode = objectMapper.createObjectNode();
requestNode.put("name", "Updated name");
requestNode.put("category", "Updated category");
requestNode.put("key", "Updated key");
requestNode.put("metaInfo", "Updated metainfo");
requestNode.put("deploymentId", deploymentId);
requestNode.put("version", 3);
ClientResource client = getAuthenticatedClient(RestUrls.createRelativeResourceUrl(
RestUrls.URL_MODEL, model.getId()));
Representation response = client.put(requestNode);
// Check "OK" status
assertEquals(Status.SUCCESS_OK, client.getResponse().getStatus());
JsonNode responseNode = objectMapper.readTree(response.getStream());
assertNotNull(responseNode);
assertEquals("Updated name", responseNode.get("name").getTextValue());
assertEquals("Updated key", responseNode.get("key").getTextValue());
assertEquals("Updated category", responseNode.get("category").getTextValue());
assertEquals(3, responseNode.get("version").getIntValue());
assertEquals("Updated metainfo", responseNode.get("metaInfo").getTextValue());
assertEquals(deploymentId, responseNode.get("deploymentId").getTextValue());
assertEquals(model.getId(), responseNode.get("id").getTextValue());
assertEquals(createTime.getTime().getTime(), getDateFromISOString(responseNode.get("createTime").getTextValue()).getTime());
assertEquals(updateTime.getTime().getTime(), getDateFromISOString(responseNode.get("lastUpdateTime").getTextValue()).getTime());
assertTrue(responseNode.get("url").getTextValue().endsWith(RestUrls.createRelativeResourceUrl(RestUrls.URL_MODEL, model.getId())));
assertTrue(responseNode.get("deploymentUrl").getTextValue().endsWith(RestUrls.createRelativeResourceUrl(RestUrls.URL_DEPLOYMENT, deploymentId)));
} finally
{
try {
repositoryService.deleteModel(model.getId());
} catch(Throwable ignore) {
// Ignore, model might not be created
}
}
}
@Deployment(resources={"org/activiti/rest/api/repository/oneTaskProcess.bpmn20.xml"})
public void testUpdateModelOverrideWithNull() throws Exception {
Model model = null;
try {
Calendar createTime = Calendar.getInstance();
createTime.set(Calendar.MILLISECOND, 0);
ClockUtil.setCurrentTime(createTime.getTime());
model = repositoryService.newModel();
model.setCategory("Model category");
model.setKey("Model key");
model.setMetaInfo("Model metainfo");
model.setName("Model name");
model.setVersion(2);
repositoryService.saveModel(model);
Calendar updateTime = Calendar.getInstance();
updateTime.set(Calendar.MILLISECOND, 0);
ClockUtil.setCurrentTime(updateTime.getTime());
// Create update request
ObjectNode requestNode = objectMapper.createObjectNode();
requestNode.put("name", (String) null);
requestNode.put("category", (String) null);
requestNode.put("key", (String) null);
requestNode.put("metaInfo", (String) null);
requestNode.put("deploymentId", (String) null);
requestNode.put("version", (String) null);
ClientResource client = getAuthenticatedClient(RestUrls.createRelativeResourceUrl(
RestUrls.URL_MODEL, model.getId()));
Representation response = client.put(requestNode);
// Check "OK" status
assertEquals(Status.SUCCESS_OK, client.getResponse().getStatus());
JsonNode responseNode = objectMapper.readTree(response.getStream());
assertNotNull(responseNode);
assertNull(responseNode.get("name").getTextValue());
assertNull(responseNode.get("key").getTextValue());
assertNull(responseNode.get("category").getTextValue());
assertNull(responseNode.get("version").getTextValue());
assertNull(responseNode.get("metaInfo").getTextValue());
assertNull(responseNode.get("deploymentId").getTextValue());
assertEquals(model.getId(), responseNode.get("id").getTextValue());
assertEquals(createTime.getTime().getTime(), getDateFromISOString(responseNode.get("createTime").getTextValue()).getTime());
assertEquals(updateTime.getTime().getTime(), getDateFromISOString(responseNode.get("lastUpdateTime").getTextValue()).getTime());
assertTrue(responseNode.get("url").getTextValue().endsWith(RestUrls.createRelativeResourceUrl(RestUrls.URL_MODEL, model.getId())));
} finally
{
try {
repositoryService.deleteModel(model.getId());
} catch(Throwable ignore) {
// Ignore, model might not be created
}
}
}
@Deployment(resources={"org/activiti/rest/api/repository/oneTaskProcess.bpmn20.xml"})
public void testUpdateModelNoFields() throws Exception {
Model model = null;
try {
Calendar now = Calendar.getInstance();
now.set(Calendar.MILLISECOND, 0);
ClockUtil.setCurrentTime(now.getTime());
model = repositoryService.newModel();
model.setCategory("Model category");
model.setKey("Model key");
model.setMetaInfo("Model metainfo");
model.setName("Model name");
model.setVersion(2);
model.setDeploymentId(deploymentId);
repositoryService.saveModel(model);
// Use empty request-node, nothing should be changed after update
ObjectNode requestNode = objectMapper.createObjectNode();
ClientResource client = getAuthenticatedClient(RestUrls.createRelativeResourceUrl(
RestUrls.URL_MODEL, model.getId()));
Representation response = client.put(requestNode);
// Check "OK" status
assertEquals(Status.SUCCESS_OK, client.getResponse().getStatus());
JsonNode responseNode = objectMapper.readTree(response.getStream());
assertNotNull(responseNode);
assertEquals("Model name", responseNode.get("name").getTextValue());
assertEquals("Model key", responseNode.get("key").getTextValue());
assertEquals("Model category", responseNode.get("category").getTextValue());
assertEquals(2, responseNode.get("version").getIntValue());
assertEquals("Model metainfo", responseNode.get("metaInfo").getTextValue());
assertEquals(deploymentId, responseNode.get("deploymentId").getTextValue());
assertEquals(model.getId(), responseNode.get("id").getTextValue());
assertEquals(now.getTime().getTime(), getDateFromISOString(responseNode.get("createTime").getTextValue()).getTime());
assertEquals(now.getTime().getTime(), getDateFromISOString(responseNode.get("lastUpdateTime").getTextValue()).getTime());
assertTrue(responseNode.get("url").getTextValue().endsWith(RestUrls.createRelativeResourceUrl(RestUrls.URL_MODEL, model.getId())));
assertTrue(responseNode.get("deploymentUrl").getTextValue().endsWith(RestUrls.createRelativeResourceUrl(RestUrls.URL_DEPLOYMENT, deploymentId)));
} finally
{
try {
repositoryService.deleteModel(model.getId());
} catch(Throwable ignore) {
// Ignore, model might not be created
}
}
}
public void testUpdateUnexistingModel() throws Exception {
ClientResource client = getAuthenticatedClient(RestUrls.createRelativeResourceUrl(RestUrls.URL_MODEL, "unexisting"));
try {
client.put(objectMapper.createObjectNode());
fail("404 expected, but was: " + client.getResponse().getStatus());
} catch(ResourceException expected) {
assertEquals(Status.CLIENT_ERROR_NOT_FOUND, client.getResponse().getStatus());
assertEquals("Could not find a model with id 'unexisting'.", client.getResponse().getStatus().getDescription());
}
}
}
......@@ -1755,6 +1755,401 @@
</para>
</section>
</section>
<section>
<title>Models</title>
<section>
<title>Get a list of models</title>
<para>
<programlisting>GET repository/models</programlisting>
</para>
<para>
<table>
<title>URL query parameters</title>
<tgroup cols='3'>
<thead>
<row>
<entry>Parameter</entry>
<entry>Required</entry>
<entry>Value</entry>
<entry>Description</entry>
</row>
</thead>
<tbody>
<row>
<entry>id</entry>
<entry>No</entry>
<entry>String</entry>
<entry>Only return models with the given id.</entry>
</row>
<row>
<entry>category</entry>
<entry>No</entry>
<entry>String</entry>
<entry>Only return models with the given category.</entry>
</row>
<row>
<entry>categoryLike</entry>
<entry>No</entry>
<entry>String</entry>
<entry>Only return models with a category like the given value. Use the <literal>%</literal> character as wildcard.</entry>
</row>
<row>
<entry>categoryNotEquals</entry>
<entry>No</entry>
<entry>String</entry>
<entry>Only return models without the given category.</entry>
</row>
<row>
<entry>name</entry>
<entry>No</entry>
<entry>String</entry>
<entry>Only return models with the given name.</entry>
</row>
<row>
<entry>nameLike</entry>
<entry>No</entry>
<entry>String</entry>
<entry>Only return models with a name like the given value. Use the <literal>%</literal> character as wildcard.</entry>
</row>
<row>
<entry>key</entry>
<entry>No</entry>
<entry>String</entry>
<entry>Only return models with the given key.</entry>
</row>
<row>
<entry>deploymentId</entry>
<entry>No</entry>
<entry>String</entry>
<entry>Only return models which are deployed in the given deployment.</entry>
</row>
<row>
<entry>version</entry>
<entry>No</entry>
<entry>Integer</entry>
<entry>Only return models with the given version.</entry>
</row>
<row>
<entry>latestVersion</entry>
<entry>No</entry>
<entry>Boolean</entry>
<entry>If <literal>true</literal>, only return models which are the latest version. Best used in combination with <literal>key</literal>. If <literal>false</literal> is passed in as value, this is ignored and all versions are returned.</entry>
</row>
<row>
<entry>deployed</entry>
<entry>No</entry>
<entry>Boolean</entry>
<entry>If <literal>true</literal>, only deployed models are returned. If <literal>false</literal>, only undeployed models are returned (deploymentId is null).</entry>
</row>
<row>
<entry>sort</entry>
<entry>No</entry>
<entry>'id' (default), 'category', 'createTime', 'key', 'lastUpdateTime', 'name' and 'version'</entry>
<entry>Property to sort on, to be used toghether with the 'order'.</entry>
</row>
<row>
<entry namest="c1" nameend="c4"><para>The general <link linkend="restPagingAndSort">paging and sorting query-parameters</link> can be used for this URL.</para></entry>
</row>
</tbody>
</tgroup>
</table>
</para>
<para>
<table>
<title>Response codes</title>
<tgroup cols='2'>
<thead>
<row>
<entry>Response code</entry>
<entry>Description</entry>
</row>
</thead>
<tbody>
<row>
<entry>200</entry>
<entry>Indicates request was successful and the models are returned</entry>
</row>
<row>
<entry>400</entry>
<entry>Indicates an parameter was passed in the wrong format. The status-message contains additional information.</entry>
</row>
</tbody>
</tgroup>
</table>
</para>
<para>
<emphasis role="bold">Success response body:</emphasis>
<programlisting>
{
"data":[
{
"name":"Model name",
"key":"Model key",
"category":"Model category",
"version":2,
"metaInfo":"Model metainfo",
"deploymentId":"7",
"id":"10",
"url":"http://localhost:8182/repository/models/10",
"createTime":"2013-06-12T14:31:08.612+0000",
"lastUpdateTime":"2013-06-12T14:31:08.612+0000",
"deploymentUrl":"http://localhost:8182/repository/deployments/7"
},
...
],
"total":2,
"start":0,
"sort":"id",
"order":"asc",
"size":2
}</programlisting>
</para>
</section>
<section>
<title>Get a model</title>
<para>
<programlisting>GET repository/models/{modelId}</programlisting>
</para>
<para>
<table>
<title>URL parameters</title>
<tgroup cols='3'>
<thead>
<row>
<entry>Parameter</entry>
<entry>Required</entry>
<entry>Value</entry>
<entry>Description</entry>
</row>
</thead>
<tbody>
<row>
<entry>modelId</entry>
<entry>Yes</entry>
<entry>String</entry>
<entry>The id of the model to get.</entry>
</row>
</tbody>
</tgroup>
</table>
</para>
<para>
<table>
<title>Response codes</title>
<tgroup cols='2'>
<thead>
<row>
<entry>Response code</entry>
<entry>Description</entry>
</row>
</thead>
<tbody>
<row>
<entry>200</entry>
<entry>Indicates the model was found and returned.</entry>
</row>
<row>
<entry>404</entry>
<entry>Indicates the requested model was not found.</entry>
</row>
</tbody>
</tgroup>
</table>
</para>
<para>
<emphasis role="bold">Success response body:</emphasis>
<programlisting>
{
"id":"5",
"url":"http://localhost:8182/repository/models/5",
"name":"Model name",
"key":"Model key",
"category":"Model category",
"version":2,
"metaInfo":"Model metainfo",
"deploymentId":"2",
"deploymentUrl":"http://localhost:8182/repository/deployments/2",
"createTime":"2013-06-12T12:31:19.861+0000",
"lastUpdateTime":"2013-06-12T12:31:19.861+0000"
}</programlisting>
</para>
</section>
<section>
<title>Update a model</title>
<para>
<programlisting>PUT repository/models/{modelId}</programlisting>
</para>
<para>
<emphasis role="bold">Request body:</emphasis>
<programlisting>
{
"name":"Model name",
"key":"Model key",
"category":"Model category",
"version":2,
"metaInfo":"Model metainfo",
"deploymentId":"2"
}</programlisting>
All request values are optional. For example, you can only include the 'name' attribute in the request body JSON-object, only updating the name of the model, leaving all other fields unaffected. When an attribute is explicitly included and is set to null, the model-value will be updated to null. Example: <literal>{"metaInfo" : null}</literal> will clear the metaInfo of the model).
</para>
<para>
<table>
<title>Response codes</title>
<tgroup cols='2'>
<thead>
<row>
<entry>Response code</entry>
<entry>Description</entry>
</row>
</thead>
<tbody>
<row>
<entry>200</entry>
<entry>Indicates the model was found and updated.</entry>
</row>
<row>
<entry>404</entry>
<entry>Indicates the requested model was not found.</entry>
</row>
</tbody>
</tgroup>
</table>
</para>
<para>
<emphasis role="bold">Success response body:</emphasis>
<programlisting>
{
"id":"5",
"url":"http://localhost:8182/repository/models/5",
"name":"Model name",
"key":"Model key",
"category":"Model category",
"version":2,
"metaInfo":"Model metainfo",
"deploymentId":"2",
"deploymentUrl":"http://localhost:8182/repository/deployments/2",
"createTime":"2013-06-12T12:31:19.861+0000",
"lastUpdateTime":"2013-06-12T12:31:19.861+0000"
}</programlisting>
</para>
</section>
<section>
<title>Create a model</title>
<para>
<programlisting>POST repository/models</programlisting>
</para>
<para>
<emphasis role="bold">Request body:</emphasis>
<programlisting>
{
"name":"Model name",
"key":"Model key",
"category":"Model category",
"version":1,
"metaInfo":"Model metainfo",
"deploymentId":"2"
}</programlisting>
All request values are optional. For example, you can only include the 'name' attribute in the request body JSON-object, only setting the name of the model, leaving all other fields null.
</para>
<para>
<table>
<title>Response codes</title>
<tgroup cols='2'>
<thead>
<row>
<entry>Response code</entry>
<entry>Description</entry>
</row>
</thead>
<tbody>
<row>
<entry>201</entry>
<entry>Indicates the model was created.</entry>
</row>
</tbody>
</tgroup>
</table>
</para>
<para>
<emphasis role="bold">Success response body:</emphasis>
<programlisting>
{
"id":"5",
"url":"http://localhost:8182/repository/models/5",
"name":"Model name",
"key":"Model key",
"category":"Model category",
"version":1,
"metaInfo":"Model metainfo",
"deploymentId":"2",
"deploymentUrl":"http://localhost:8182/repository/deployments/2",
"createTime":"2013-06-12T12:31:19.861+0000",
"lastUpdateTime":"2013-06-12T12:31:19.861+0000"
}</programlisting>
</para>
</section>
<section>
<title>Delete a model</title>
<para>
<programlisting>DELETE repository/models/{modelId}</programlisting>
</para>
<para>
<table>
<title>URL parameters</title>
<tgroup cols='3'>
<thead>
<row>
<entry>Parameter</entry>
<entry>Required</entry>
<entry>Value</entry>
<entry>Description</entry>
</row>
</thead>
<tbody>
<row>
<entry>modelId</entry>
<entry>Yes</entry>
<entry>String</entry>
<entry>The id of the model to delete.</entry>
</row>
</tbody>
</tgroup>
</table>
</para>
<para>
<table>
<title>Response codes</title>
<tgroup cols='2'>
<thead>
<row>
<entry>Response code</entry>
<entry>Description</entry>
</row>
</thead>
<tbody>
<row>
<entry>204</entry>
<entry>Indicates the model was found and has been deleted. Response-body is intentionally empty.</entry>
</row>
<row>
<entry>404</entry>
<entry>Indicates the requested model was not found.</entry>
</row>
</tbody>
</tgroup>
</table>
</para>
</section>
</section>
<section>
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册