Browse Source

支持上传接口。

mxd 4 years ago
parent
commit
31d9ede386

+ 5 - 0
pom.xml

@@ -92,6 +92,11 @@
             <artifactId>commons-io</artifactId>
             <version>2.6</version>
         </dependency>
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-compress</artifactId>
+            <version>1.20</version>
+        </dependency>
     </dependencies>
     <profiles>
         <profile>

+ 17 - 5
src/main/java/org/ssssssss/magicapi/adapter/resource/DatabaseResource.java

@@ -18,14 +18,26 @@ public class DatabaseResource extends KeyValueResource {
 
 	private Map<String, String> cachedContent = new ConcurrentHashMap<>();
 
-	public DatabaseResource(JdbcTemplate template, String tableName, String separator, String path, boolean readonly, KeyValueResource parent) {
-		super(separator, path, readonly, parent);
+	public DatabaseResource(JdbcTemplate template, String tableName) {
+		this(template, tableName,  false);
+	}
+
+	public DatabaseResource(JdbcTemplate template, String tableName, boolean readonly) {
+		this(template, tableName, "/magic-api", readonly);
+	}
+
+	public DatabaseResource(JdbcTemplate template, String tableName, String path,boolean readonly) {
+		this(template, tableName, path, readonly, null);
+	}
+
+	public DatabaseResource(JdbcTemplate template, String tableName, String path, boolean readonly, KeyValueResource parent) {
+		super("/", path, readonly, parent);
 		this.template = template;
 		this.tableName = tableName;
 	}
 
-	public DatabaseResource(JdbcTemplate template, String tableName, String separator, String path, boolean readonly, Map<String, String> cachedContent, KeyValueResource parent) {
-		this(template, tableName, separator, path, readonly, parent);
+	public DatabaseResource(JdbcTemplate template, String tableName, String path, boolean readonly, Map<String, String> cachedContent, KeyValueResource parent) {
+		this(template, tableName, path, readonly, parent);
 		this.cachedContent = cachedContent;
 	}
 
@@ -105,7 +117,7 @@ public class DatabaseResource extends KeyValueResource {
 
 	@Override
 	public Function<String, Resource> mappedFunction() {
-		return it -> new DatabaseResource(template, tableName, separator, it, readonly, this.cachedContent, this);
+		return it -> new DatabaseResource(template, tableName, it, readonly, this.cachedContent, this);
 	}
 
 	@Override

+ 3 - 0
src/main/java/org/ssssssss/magicapi/adapter/resource/KeyValueResource.java

@@ -40,6 +40,9 @@ public abstract class KeyValueResource implements Resource {
 
 	@Override
 	public final boolean renameTo(Resource resource) {
+		if(resource.name().equalsIgnoreCase(this.name())){
+			return true;
+		}
 		if (!(resource instanceof KeyValueResource)) {
 			throw new IllegalArgumentException("无法将" + this.getAbsolutePath() + "重命名为:" + resource.getAbsolutePath());
 		}

+ 8 - 8
src/main/java/org/ssssssss/magicapi/adapter/resource/RedisResource.java

@@ -20,15 +20,15 @@ public class RedisResource extends KeyValueResource {
 
 	private static final Logger logger = LoggerFactory.getLogger(RedisResource.class);
 
-	private final Map<String,String> cachedContent = new ConcurrentHashMap<>();
+	private final Map<String, String> cachedContent = new ConcurrentHashMap<>();
 
-	public RedisResource(StringRedisTemplate redisTemplate, String path, String separator, boolean readonly, RedisResource parent) {
-		super(separator, path, readonly, parent);
+	public RedisResource(StringRedisTemplate redisTemplate, String path, boolean readonly, RedisResource parent) {
+		super(":", path, readonly, parent);
 		this.redisTemplate = redisTemplate;
 	}
 
-	public RedisResource(StringRedisTemplate redisTemplate, String path, String separator, boolean readonly) {
-		this(redisTemplate,path,separator,readonly,null);
+	public RedisResource(StringRedisTemplate redisTemplate, String path, boolean readonly) {
+		this(redisTemplate, path, readonly, null);
 	}
 
 	@Override
@@ -46,7 +46,7 @@ public class RedisResource extends KeyValueResource {
 	@Override
 	public byte[] read() {
 		String value = this.cachedContent.get(path);
-		if(value == null){
+		if (value == null) {
 			value = redisTemplate.opsForValue().get(path);
 		}
 		return value == null ? new byte[0] : value.getBytes(StandardCharsets.UTF_8);
@@ -68,7 +68,7 @@ public class RedisResource extends KeyValueResource {
 
 	@Override
 	public boolean exists() {
-		if(this.cachedContent.containsKey(this.path)){
+		if (this.cachedContent.containsKey(this.path)) {
 			return true;
 		}
 		return Boolean.TRUE.equals(this.redisTemplate.hasKey(this.path));
@@ -86,7 +86,7 @@ public class RedisResource extends KeyValueResource {
 
 	@Override
 	protected Function<String, Resource> mappedFunction() {
-		return (it) -> new RedisResource(this.redisTemplate, it, this.separator, readonly, this);
+		return (it) -> new RedisResource(this.redisTemplate, it, readonly, this);
 	}
 
 	@Override

+ 122 - 0
src/main/java/org/ssssssss/magicapi/adapter/resource/ZipResource.java

@@ -0,0 +1,122 @@
+package org.ssssssss.magicapi.adapter.resource;
+
+import org.apache.commons.compress.archivers.ArchiveEntry;
+import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
+import org.ssssssss.magicapi.adapter.Resource;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public class ZipResource implements Resource {
+
+	private final Map<String, byte[]> cachedContent;
+
+	private String path = "";
+
+	private Resource parent;
+
+	public ZipResource(InputStream is) throws IOException {
+		cachedContent = new HashMap<>();
+		try (ZipArchiveInputStream zis = new ZipArchiveInputStream (is)) {
+			ArchiveEntry entry;
+			byte[] buf = new byte[4096];
+			int len = -1;
+			while ((entry = zis.getNextEntry()) != null) {
+				ByteArrayOutputStream os = new ByteArrayOutputStream();
+				while ((len = zis.read(buf, 0, buf.length)) != -1) {
+					os.write(buf, 0, len);
+				}
+				cachedContent.put(entry.getName(), os.toByteArray());
+			}
+		}
+	}
+
+	ZipResource(String name, Map<String, byte[]> cachedContent,Resource parent) {
+		this.path = name;
+		this.cachedContent = cachedContent;
+		this.parent = parent;
+	}
+
+	@Override
+	public boolean readonly() {
+		return true;
+	}
+
+	@Override
+	public boolean exists() {
+		return this.cachedContent.containsKey(this.path);
+	}
+
+	@Override
+	public byte[] read() {
+		return cachedContent.getOrDefault(this.path, new byte[0]);
+	}
+
+	@Override
+	public Resource getResource(String name) {
+		return new ZipResource(this.path + name, this.cachedContent,this);
+	}
+
+	@Override
+	public Resource getDirectory(String name) {
+		return new ZipResource(this.path + name + "/", this.cachedContent,this);
+	}
+
+	@Override
+	public boolean isDirectory() {
+		return this.path.endsWith("/");
+	}
+
+	@Override
+	public String name() {
+		String name = this.path;
+		if (isDirectory()) {
+			name = this.path.substring(0, name.length() - 1);
+		}
+		int index = name.lastIndexOf("/");
+		return index > -1 ? name.substring(index + 1) : name;
+	}
+
+	@Override
+	public List<Resource> resources() {
+		throw new UnsupportedOperationException();
+	}
+
+	@Override
+	public Resource parent() {
+		return this.parent;
+	}
+
+	@Override
+	public List<Resource> dirs() {
+		int len = this.path.length();
+		return this.cachedContent.keySet().stream()
+				.filter(it -> it.endsWith("/")  && it.startsWith(this.path) && it.indexOf("/",len + 1) == it.length() - 1)
+				.map(it -> this.getDirectory(it.substring(len,it.length() - 1)))
+				.collect(Collectors.toList());
+	}
+
+
+	@Override
+	public List<Resource> files(String suffix) {
+		if(isDirectory()){
+			int len = this.path.length();
+			return this.cachedContent.keySet().stream()
+					.filter(it -> it.startsWith(this.path) && it.endsWith(suffix) && it.indexOf("/",len) == -1)
+					.map(it -> this.getResource(it.substring(len)))
+					.collect(Collectors.toList());
+		}
+		return Collections.emptyList();
+	}
+
+	@Override
+	public String getAbsolutePath() {
+		return this.path;
+	}
+}

+ 5 - 0
src/main/java/org/ssssssss/magicapi/config/MagicFunctionManager.java

@@ -16,6 +16,7 @@ import org.ssssssss.script.MagicScriptContext;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.Executors;
 import java.util.concurrent.TimeUnit;
@@ -84,6 +85,10 @@ public class MagicFunctionManager {
 		return functionInfo != null && !Objects.equals(info.getId(), functionInfo.getId());
 	}
 
+	public boolean hasRegister(Set<String> paths) {
+		return paths.stream().anyMatch(mappings::containsKey);
+	}
+
 	/**
 	 * 函数移动
 	 */

+ 6 - 2
src/main/java/org/ssssssss/magicapi/config/MappingHandlerMapping.java

@@ -87,7 +87,7 @@ public class MappingHandlerMapping {
 	 */
 	private final List<ApiInfo> apiInfos = Collections.synchronizedList(new ArrayList<>());
 
-	public MappingHandlerMapping(String prefix,boolean allowOverride) throws NoSuchMethodException {
+	public MappingHandlerMapping(String prefix, boolean allowOverride) throws NoSuchMethodException {
 		this.prefix = prefix;
 		this.allowOverride = allowOverride;
 	}
@@ -231,7 +231,7 @@ public class MappingHandlerMapping {
 			return nameEquals || !groupServiceProvider.exists(group);
 		}
 		// 检测名字是否冲突
-		if((!parentIdEquals || !nameEquals) && groupServiceProvider.exists(group)){
+		if ((!parentIdEquals || !nameEquals) && groupServiceProvider.exists(group)) {
 			return false;
 		}
 		// 新的接口分组路径
@@ -240,6 +240,10 @@ public class MappingHandlerMapping {
 		return !hasConflict(oldTree, newPath + "/" + Objects.toString(group.getPath(), ""));
 	}
 
+	public boolean hasRegister(Set<String> paths) {
+		return paths.stream().anyMatch(mappings::containsKey);
+	}
+
 	/**
 	 * 删除分组
 	 */

+ 1 - 0
src/main/java/org/ssssssss/magicapi/controller/MagicController.java

@@ -38,6 +38,7 @@ public class MagicController implements JsonCodeConstants {
 	}
 
 	@ExceptionHandler(Exception.class)
+	@ResponseBody
 	public Object exceptionHandler(Exception e) {
 		logger.error("magic-api调用接口出错", e);
 		return new JsonBean<>(-1, e.getMessage());

+ 106 - 8
src/main/java/org/ssssssss/magicapi/controller/MagicWorkbenchController.java

@@ -4,20 +4,27 @@ import org.apache.commons.lang3.StringUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
 import org.springframework.http.ResponseEntity;
+import org.springframework.util.MimeType;
 import org.springframework.util.ResourceUtils;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.multipart.MultipartFile;
 import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
 import org.ssssssss.magicapi.adapter.Resource;
+import org.ssssssss.magicapi.adapter.resource.ZipResource;
 import org.ssssssss.magicapi.config.MagicConfiguration;
 import org.ssssssss.magicapi.config.Valid;
 import org.ssssssss.magicapi.interceptor.RequestInterceptor;
 import org.ssssssss.magicapi.logging.MagicLoggerContext;
-import org.ssssssss.magicapi.model.JsonBean;
-import org.ssssssss.magicapi.model.Options;
+import org.ssssssss.magicapi.model.*;
 import org.ssssssss.magicapi.modules.ResponseModule;
+import org.ssssssss.magicapi.provider.GroupServiceProvider;
+import org.ssssssss.magicapi.provider.StoreServiceProvider;
+import org.ssssssss.magicapi.utils.JsonUtils;
 import org.ssssssss.magicapi.utils.MD5Utils;
+import org.ssssssss.magicapi.utils.PathUtils;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
@@ -72,18 +79,19 @@ public class MagicWorkbenchController extends MagicController {
 		return new JsonBean<>(Stream.of(Options.values()).map(item -> Collections.singletonMap(item.getValue(), item.getName())).collect(Collectors.toList()));
 	}
 
-	@RequestMapping(value = "/config-js", produces = "application/javascript")
+	@RequestMapping(value = "/config-js")
 	@ResponseBody
-	public Object configjs() {
+	public ResponseEntity<byte[]> configjs() {
+		ResponseEntity.BodyBuilder responseBuilder = ResponseEntity.ok().contentType(MediaType.parseMediaType("application/javascript"));
 		if (configuration.getEditorConfig() != null) {
 			try {
 				File file = ResourceUtils.getFile(configuration.getEditorConfig());
-				return Files.readAllBytes(Paths.get(file.toURI()));
+				return responseBuilder.body(Files.readAllBytes(Paths.get(file.toURI())));
 			} catch (IOException e) {
 				logger.warn("读取编辑器配置文件{}失败", configuration.getEditorConfig());
 			}
 		}
-		return "var MAGIC_EDITOR_CONFIG = {}";
+		return responseBuilder.body("var MAGIC_EDITOR_CONFIG = {}".getBytes());
 	}
 
 	@RequestMapping("/download")
@@ -99,9 +107,99 @@ public class MagicWorkbenchController extends MagicController {
 		}
 	}
 
+	@RequestMapping("/upload")
+	@Valid(readonly = false, authorization = RequestInterceptor.Authorization.UPLOAD)
+	@ResponseBody
+	public JsonBean<Boolean> upload(MultipartFile file, String mode) throws IOException {
+		notNull(file, FILE_IS_REQUIRED);
+		ZipResource root = new ZipResource(file.getInputStream());
+		Set<String> apiPaths = new HashSet<>();
+		Set<String> functionPaths = new HashSet<>();
+		Set<Group> groups = new HashSet<>();
+		Set<ApiInfo> apiInfos = new HashSet<>();
+		Set<FunctionInfo> functionInfos = new HashSet<>();
+		// 检查上传资源中是否有冲突
+		isTrue(readPaths(groups, apiPaths, functionPaths, apiInfos, functionInfos, "/", root), UPLOAD_PATH_CONFLICT);
+		// 判断是否是强制上传
+		if (!"force".equals(mode)) {
+			// 检测与已注册的接口和函数是否有冲突
+			isTrue(!configuration.getMappingHandlerMapping().hasRegister(apiPaths), UPLOAD_PATH_CONFLICT.format("接口"));
+			isTrue(!configuration.getMagicFunctionManager().hasRegister(apiPaths), UPLOAD_PATH_CONFLICT.format("函数"));
+		}
+		Resource item = root.getResource("group.json");
+		GroupServiceProvider groupServiceProvider = configuration.getGroupServiceProvider();
+		if (item.exists()) {
+			Group group = groupServiceProvider.readGroup(item);
+			// 检查分组是否存在
+			isTrue("0".equals(group.getParentId()) || groupServiceProvider.getGroupResource(group.getParentId()).exists(), GROUP_NOT_FOUND);
+			groups.removeIf(it -> it.getId().equalsIgnoreCase(group.getId()));
+		}
+		for (Group group : groups) {
+			if (groupServiceProvider.getGroupResource(group.getId()).exists()) {
+				groupServiceProvider.update(group);
+			} else {
+				groupServiceProvider.insert(group);
+			}
+		}
+		Resource backups = configuration.getWorkspace().getDirectory("backups");
+		// 保存
+		write(configuration.getMagicApiService(),backups,apiInfos);
+		write(configuration.getFunctionServiceProvider(),backups,functionInfos);
+		// 重新注册
+		configuration.getMappingHandlerMapping().registerAllMapping();
+		configuration.getMagicFunctionManager().registerAllFunction();
+		return new JsonBean<>(SUCCESS, true);
+	}
+
+	private <T extends MagicEntity> void write(StoreServiceProvider<T> provider, Resource backups, Set<T> infos) {
+		for (T info : infos) {
+			Resource resource = configuration.getGroupServiceProvider().getGroupResource(info.getGroupId());
+			byte[] content = provider.serialize(info);
+			resource.write(content);
+			Resource directory = backups.getDirectory(info.getId());
+			if (!directory.exists()) {
+				directory.mkdir();
+			}
+			directory.getResource(System.currentTimeMillis() + ".ms").write(content);
+			resource.write(content);
+		}
+	}
+
+	private boolean readPaths(Set<Group> groups, Set<String> apiPaths, Set<String> functionPaths, Set<ApiInfo> apiInfos, Set<FunctionInfo> functionInfos, String parentPath, Resource root) {
+		Resource resource = root.getResource("group.json");
+		String path = "";
+		if (resource.exists()) {
+			Group group = JsonUtils.readValue(resource.read(), Group.class);
+			groups.add(group);
+			path = Objects.toString(group.getPath(), "");
+			boolean isApi = "1".equals(group.getType());
+			for (Resource file : root.files(".ms")) {
+				boolean conflict;
+				if (isApi) {
+					ApiInfo info = configuration.getMagicApiService().deserialize(file.read());
+					apiInfos.add(info);
+					conflict = !apiPaths.add(Objects.toString(info.getMethod(), "GET") + ":" + PathUtils.replaceSlash(parentPath + "/" + path + "/" + info.getPath()));
+				} else {
+					FunctionInfo info = configuration.getFunctionServiceProvider().deserialize(file.read());
+					functionInfos.add(info);
+					conflict = !functionPaths.add(PathUtils.replaceSlash(parentPath + "/" + path + "/" + info.getPath()));
+				}
+				if (conflict) {
+					return false;
+				}
+			}
+		}
+		for (Resource directory : root.dirs()) {
+			if (!readPaths(groups, apiPaths, functionPaths, apiInfos, functionInfos, PathUtils.replaceSlash(parentPath + "/" + path), directory)) {
+				return false;
+			}
+		}
+		return true;
+	}
+
 	private ResponseEntity<?> download(Resource resource, String filename) throws IOException {
 		ByteArrayOutputStream os = new ByteArrayOutputStream();
-		resource.export(os,"backups");
-		return ResponseModule.download(os.toByteArray(),filename);
+		resource.export(os, "backups");
+		return ResponseModule.download(os.toByteArray(), filename);
 	}
 }

+ 4 - 0
src/main/java/org/ssssssss/magicapi/model/JsonCodeConstants.java

@@ -50,6 +50,10 @@ public interface JsonCodeConstants {
 
 	JsonCode HEADER_INVALID = new JsonCode(0, "header验证失败");
 
+	JsonCode FILE_IS_REQUIRED = new JsonCode(0, "请上传文件");
+
+	JsonCode UPLOAD_PATH_CONFLICT = new JsonCode(0, "上传后%s路径会有冲突,请检查");
+
 	JsonCode DEBUG_SESSION_NOT_FOUND = new JsonCode(0, "debug session not found!");
 
 	JsonCode API_NOT_FOUND = new JsonCode(1001, "api not found");

+ 2 - 0
src/main/java/org/ssssssss/magicapi/provider/GroupServiceProvider.java

@@ -34,6 +34,8 @@ public interface GroupServiceProvider {
 	 */
 	boolean containsApiGroup(String groupId);
 
+	Group readGroup(Resource resource);
+
 	/**
 	 * 接口分组列表
 	 */

+ 20 - 12
src/main/java/org/ssssssss/magicapi/provider/StoreServiceProvider.java

@@ -1,14 +1,13 @@
 package org.ssssssss.magicapi.provider;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.ssssssss.magicapi.adapter.Resource;
 import org.ssssssss.magicapi.model.MagicEntity;
 import org.ssssssss.magicapi.utils.JsonUtils;
 
 import java.nio.charset.StandardCharsets;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.UUID;
+import java.util.*;
 import java.util.stream.Collectors;
 
 public abstract class StoreServiceProvider<T extends MagicEntity> {
@@ -27,15 +26,17 @@ public abstract class StoreServiceProvider<T extends MagicEntity> {
 
 	protected Class<T> clazz;
 
+	private static Logger logger = LoggerFactory.getLogger(StoreServiceProvider.class);
+
 	public StoreServiceProvider(Class<T> clazz, Resource workspace, GroupServiceProvider groupServiceProvider) {
 		this.clazz = clazz;
 		this.workspace = workspace;
 		this.groupServiceProvider = groupServiceProvider;
-		if(!this.workspace.exists()){
+		if (!this.workspace.exists()) {
 			this.workspace.mkdir();
 		}
 		this.backupResource = this.workspace.parent().getDirectory("backups");
-		if(!this.backupResource.exists()){
+		if (!this.backupResource.exists()) {
 			this.backupResource.mkdir();
 		}
 	}
@@ -62,9 +63,13 @@ public abstract class StoreServiceProvider<T extends MagicEntity> {
 	 */
 	public boolean backup(T info) {
 		Resource directory = this.backupResource.getDirectory(info.getId());
-		if(!directory.readonly() && (directory.exists() || directory.mkdir())){
+		if (!directory.readonly() && (directory.exists() || directory.mkdir())) {
 			Resource resource = directory.getResource(String.format("%s.ms", System.currentTimeMillis()));
-			return resource.write(serialize(info));
+			try {
+				return resource.write(serialize(info));
+			} catch (Exception e) {
+				logger.warn("保存历史记录失败,{}", e.getMessage());
+			}
 		}
 		return false;
 	}
@@ -78,7 +83,10 @@ public abstract class StoreServiceProvider<T extends MagicEntity> {
 	public List<Long> backupList(String id) {
 		Resource directory = this.backupResource.getDirectory(id);
 		List<Resource> resources = directory.files(".ms");
-		return resources.stream().map(it -> Long.valueOf(it.name().replace(".ms",""))).collect(Collectors.toList());
+		return resources.stream()
+				.map(it -> Long.valueOf(it.name().replace(".ms", "")))
+				.sorted(Comparator.reverseOrder())
+				.collect(Collectors.toList());
 	}
 
 	/**
@@ -89,9 +97,9 @@ public abstract class StoreServiceProvider<T extends MagicEntity> {
 	 */
 	public T backupInfo(String id, Long timestamp) {
 		Resource directory = this.backupResource.getDirectory(id);
-		if(directory.exists()){
+		if (directory.exists()) {
 			Resource resource = directory.getResource(String.format("%s.ms", timestamp));
-			if(resource.exists()){
+			if (resource.exists()) {
 				return deserialize(resource.read());
 			}
 		}
@@ -201,7 +209,7 @@ public abstract class StoreServiceProvider<T extends MagicEntity> {
 	/**
 	 * 根据组ID删除
 	 */
-	public boolean deleteGroup(String rootId,List<String> groupIds) {
+	public boolean deleteGroup(String rootId, List<String> groupIds) {
 		if (!groupServiceProvider.getGroupResource(rootId).delete()) {
 			return false;
 		}

+ 9 - 1
src/main/java/org/ssssssss/magicapi/provider/impl/DefaultGroupServiceProvider.java

@@ -1,5 +1,6 @@
 package org.ssssssss.magicapi.provider.impl;
 
+import org.apache.commons.lang3.StringUtils;
 import org.ssssssss.magicapi.adapter.Resource;
 import org.ssssssss.magicapi.model.Group;
 import org.ssssssss.magicapi.model.TreeNode;
@@ -28,7 +29,9 @@ public class DefaultGroupServiceProvider implements GroupServiceProvider {
 
 	@Override
 	public boolean insert(Group group) {
-		group.setId(UUID.randomUUID().toString().replace("-", ""));
+		if(StringUtils.isBlank(group.getId())){
+			group.setId(UUID.randomUUID().toString().replace("-", ""));
+		}
 		Resource directory = this.getGroupResource(group.getParentId());
 		directory = directory == null ? this.getGroupResource(group.getType(), group.getName()) : directory.getDirectory(group.getName());
 		if (!directory.exists() && directory.mkdir()) {
@@ -81,6 +84,11 @@ public class DefaultGroupServiceProvider implements GroupServiceProvider {
 		return "0".equals(groupId) || cacheApiTree.containsKey(groupId);
 	}
 
+	@Override
+	public Group readGroup(Resource resource) {
+		return JsonUtils.readValue(resource.read(),Group.class);
+	}
+
 	@Override
 	public TreeNode<Group> apiGroupTree() {
 		List<Group> groups = groupList("1");