一、特殊名词介绍
无
二、背景、场景介绍
在集群场景或非集群但多报表系统共用一套模板和相关配置文件的场景中。决策系统提供了FTP和SFTP两种文件服务器用于实现将系统运行中的文件性质的资源进行统一的管理。
包括 assets、reportlets、resources、schedule、dashboards、treasures、backup 这些文件夹下的资源都会从文件服务器指向的资源地址进行读取。而ResourceRepositoryFactoryProvider接口就是用于,用户有其他类型的统一文件服务器的场景中,允许用户进行文件服务器的扩展,比如OSS、HDFS之类的。
三、接口介绍
package com.fr.io.fun;
import com.fr.io.base.provider.RepositoryFactoryProvider;
import com.fr.io.config.RepositoryConfig;
import com.fr.stable.fun.mark.Mutable;
import com.fr.stable.script.CalculatorKey;
/**
* 仓库工厂插件接口
* <p>
* Created by rinoux on 2018/3/13.
*/
public interface ResourceRepositoryFactoryProvider<T extends RepositoryConfig> extends Mutable {
CalculatorKey KEY = CalculatorKey.createKey(ResourceRepositoryFactoryProvider.class.getName());
String MARK_STRING = "ResourceRepositoryFactoryProvider";
RepositoryFactoryProvider<T> getFactory();
}
package com.fr.io.config;
import com.fr.common.annotations.Open;
import com.fr.config.utils.UniqueKey;
import com.fr.io.base.provider.Updatable;
import com.fr.third.fasterxml.jackson.annotation.JsonIgnoreProperties;
/**
* Created by rinoux on 2018/5/15.
*/
@JsonIgnoreProperties({"id", "data", "classInfo", "nameSpace"})
@Open
public abstract class RepositoryConfig extends UniqueKey implements Updatable {
private String identity;
public RepositoryConfig() {}
public RepositoryConfig(String identity) {
this.identity = identity;
}
public String getIdentity() {
return identity;
}
public void setIdentity(String identity) {
this.identity = identity;
}
@Override
public Object clone() throws CloneNotSupportedException {
RepositoryConfig cloned = (RepositoryConfig) super.clone();
cloned.setIdentity(this.identity);
return cloned;
}
}
package com.fr.io.base.provider;
import com.fr.common.annotations.Open;
import com.fr.io.config.RepositoryConfigManagerProvider;
import com.fr.io.config.RepositoryConfig;
import com.fr.io.context.info.RepositoryProfile;
import com.fr.io.repository.ResourceRepository;
import java.io.Serializable;
/**
* Created by rinoux on 2018/3/20.
*/
@Open
public interface RepositoryFactoryProvider<T extends RepositoryConfig> extends Serializable {
/**
* 仓库标识
*
* @return 标识
*/
String getIdentity();
/**
* 获取配置管理
*
* @return 配置管理器
*/
RepositoryConfigManagerProvider<T> getConfigManager();
/**
* profile的类
*
* @return
*/
Class<? extends RepositoryProfile<T>> getProfileClass();
/**
* 配置类型
*
* @return
*/
Class<T> getConfigClass();
/**
* 验证配置是否正确
*
* @param config 配置
* @return 是否正确
*/
boolean verifyConfig(T config);
/**
* 生产仓库
*
* @param repoName 仓库名称
* @param workRoot 工作路径
* @param config 所需配置
* @return 仓库
*/
ResourceRepository produce(String repoName, String workRoot, T config);
/**
* 生产仓库
*
* @param repoName 仓库名称
* @param workRoot 工作路径
* @return 仓库
*/
ResourceRepository produce(String repoName, String workRoot);
}
package com.fr.io.base.provider;
/**
* Created by rinoux on 2018/3/8.
*/
public interface RepositoryInfoProvider {
/**
* 实际仓库名称
*
* @return 名称
*/
String getRepoName();
/**
* 设置仓库名称
*
* @param repoName 名称
*/
void setRepoName(String repoName);
/**
* 仓库的类型标识(FTP,OSS之类的)
*
* @return getIdentity
*/
String getIdentity();
/**
* 获取工作路径
*
* @return workRoot
*/
String getWorkRoot();
/**
* 文件大小和实际磁盘占用大小一致
*
*
* @return 是否一致
*/
boolean isAccurateDiskSize();
}
package com.fr.workspace.resource;
import com.fr.stable.Filter;
import com.fr.workspace.base.WorkspaceAPI;
import com.fr.workspace.base.WorkspaceConstants;
import org.jetbrains.annotations.Nullable;
import java.io.InputStream;
/**
* Created by juhaoyu on 2018/6/14.
* 工作空间资源读写
*/
@WorkspaceAPI(description = "Fine-Core_Workspace_Description_Of_Work_Resource")
public interface WorkResource {
/**
* 读取资源(相对工作目录的资源路径)
*/
byte[] readFully(String path);
/**
* 打开资源流
*
* @param path 路径
* @return 不存在返回null
*/
@Nullable
InputStream openStream(String path);
/**
* 写资源
*/
void write(String path, byte[] data);
/**
* 创建空文件
* 已存在的文件不会清空
*/
boolean createFile(String path);
/**
* 创建目录
*/
boolean createDirectory(String path);
/**
* 删除文件或目录
*/
boolean delete(String path);
/**
* 重命名文件或目录
*/
boolean rename(String from, String to);
/**
* 判断文件或目录是否存在
*/
boolean exist(String path);
/**
* 列出路径下的所有name
*
* @param dir
* @return
*/
String[] list(String dir);
/**
* 列出路径下的所有符合条件的name
*
* @param dir 路径
* @param filter 过滤器
* @return
*/
String[] list(String dir, Filter<String> filter);
/**
* 是否是文件夹
*
* @param path
* @return
*/
boolean isDirectory(String path);
/**
* 最后修改时间
*
* @param path 路径
* @return mills
*/
long lastModified(String path);
/**
* 文件大小
*
* @param path 文件路径
* @return 大小
*/
long length(String path);
/**
* 融合了{@link this#write(String, byte[])}, {@link this#delete(String)} 和 {@link this#rename(String, String)} 三个操作
*
* @param tmpPath tmp路径
* @param path path路径
* @param content 文件内容
* @return 保存是否成功
*/
default void save(String tmpPath, String path, byte[] content) {
// 输出临时文件到服务器
write(tmpPath, content);
// 删除源文件
delete(path);
// 重命名文件
rename(tmpPath, path);
}
}
package com.fr.io.repository;
import com.fr.io.base.provider.RepositoryInfoProvider;
import com.fr.stable.Filter;
import com.fr.workspace.resource.ResourceIOException;
import com.fr.workspace.resource.WorkResource;
import java.io.InputStream;
import java.net.URL;
/**
* 资源仓库.
*/
public interface ResourceRepository extends RepositoryInfoProvider, WorkResource {
/**
* 存储的分隔符
*
* @return 分隔符
*/
String getSeparator();
/**
* 获取文件的entry
*
* @param path 路径
* @return FineFileEntry 不存在返回null
*/
FineFileEntry getEntry(String path);
/**
* 列出路径下所有的FineFileEntry
*
* @param dir 路径
* @return 所有FineFileEntry,不存在或者非目录返回空数组
*/
FineFileEntry[] listEntry(String dir);
/**
* 获取url
*
* @param path 路径
* @return url
*/
URL getResource(String path);
/**
* 根据路径读取文件流
* <p>
* InputStream的关闭应该由client来负责
*
* @param file 路径
* @return 数据字节数组 如果文件不存在返回null
* @throws ResourceIOException read过程异常时抛出异常
*/
InputStream read(String file) throws ResourceIOException;
/**
* 列出该路径下的所有filter接受的内容
* <p>
* filter为null时默认设置为DefaultFilter,表示接受所有类型
* <p>
* 返回的List为dir下所有filter接受的文件和文件夹的名称name,而非全路径path
* <p>
* 非递归
*
* @param dir 路径
* @param filter 过滤器
* @return 所有文件的名称 路径不存在时返回空的数组
*/
@Override
String[] list(String dir, Filter<String> filter);
/**
* 列出该路径下的所有文件和文件夹
* <p>
* 返回的List为dir下所有filter接受的文件和文件夹的名称name,而非全路径path
* <p>
* 非递归
*
* @param dir 路径
* @return 所有文件的名称
*/
@Override
String[] list(String dir);
/**
* 向文件写入数据
* <p>
* 如果文件不存在则新建文件
* <p>
* data使用后需要在write中关闭
*
* @param file 文件path
* @param data 数据
* @throws ResourceIOException 写过程中的异常抛出
*/
void write(String file, InputStream data) throws ResourceIOException;
/**
* 追加写文件
* <p>
* 向一个已经存在的文件末尾追加新的内容
*
* @param file 名称
* @param data 数据流
* @throws ResourceIOException 追加写的过程中发生异常抛出
*/
void appendWrite(String file, InputStream data) throws ResourceIOException;
/**
* 追加写文件
* <p>
* 向一个已经存在的文件末尾追加新的内容
*
* @param file 名称
* @param data 数据流
* @throws ResourceIOException 追加写的过程中发生异常抛出
*/
void appendWrite(String file, byte[] data) throws ResourceIOException;
/**
* 复制
* <p>
* origPath和desPath均为全路径
* <p>
* 若desPath已经存在返回false
*
* @param origPath 源
* @param desPath 目标
* @return 是否copy成功
* @throws ResourceIOException 重命名过程中出现的异常抛出
*/
boolean copy(String origPath, String desPath) throws ResourceIOException;
/**
* 是否为目录
* <p>
* 对于对象存储,key以"/"结尾的判断为目录
*
* @param path 名称
* @return 是否是目录
*/
@Override
boolean isDirectory(String path);
/**
* 关闭
* <p>
* 做一些关闭的操作
*/
void shutDown();
/**
* 认证
*
* @return 默认为<code>true</code>
*/
default boolean authentication(){
return true;
}
}
package com.fr.io.context.info;
import com.fr.cluster.ClusterBridge;
import com.fr.common.annotations.Open;
import com.fr.config.Identifier;
import com.fr.config.holder.Conf;
import com.fr.config.holder.factory.Holders;
import com.fr.config.holder.impl.MapConf;
import com.fr.config.utils.UniqueKey;
import com.fr.io.base.exception.RepositoryException;
import com.fr.io.base.provider.RepositoryFactoryProvider;
import com.fr.io.base.provider.Updatable;
import com.fr.io.config.RepositoryConfig;
import com.fr.io.context.ResourceModuleContext;
import com.fr.stable.StringUtils;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
/**
* Created by rinoux on 2018/3/26.
*/
@SuppressWarnings("all")
@Open
public abstract class RepositoryProfile<T extends RepositoryConfig> extends UniqueKey implements Serializable, Updatable {
/**
* 仓库类型
*/
@Identifier("identity")
private Conf<String> identity = Holders.simple(StringUtils.EMPTY);
/**
* 仓库名称
*/
@Identifier("repoName")
private Conf<String> repoName = Holders.simple(StringUtils.EMPTY);
/**
* 是否集群共享
*/
@Identifier("shared")
private Conf<Boolean> shared = Holders.simple(Boolean.FALSE);
/**
* 各个机器上的工作路径,如果是共享就共用一个,非共享则各自有各自的workroot,即[id, workroot]
*/
@Identifier("workroots")
private MapConf<Map<String, String>> workroots = Holders.map(new HashMap<String, String>(), String.class, String.class);
public RepositoryProfile() {
}
public RepositoryProfile(String identity, String repoName, String workRoot) {
init(identity, repoName, workRoot, false);
}
public RepositoryProfile(String identity, String repoName, String workRoot, T config) {
init(identity, repoName, workRoot, false);
}
public RepositoryProfile(String identity, String repoName, String workRoot, boolean shared) {
init(identity, repoName, workRoot, shared);
}
public RepositoryProfile(String identity, String repoName, String workRoot, boolean shared, T config) {
init(identity, repoName, workRoot, shared);
}
private void init(String identity, String repoName, String workRoot, boolean shared) {
this.identity.set(identity);
this.repoName.set(repoName);
this.shared.set(shared);
this.setWorkRoot(workRoot);
}
public String getIdentity() {
return identity.get();
}
public void setIdentity(String identity) {
this.identity.set(identity);
}
public String getRepoName() {
return repoName.get();
}
public void setRepoName(String repoName) {
this.repoName.set(repoName);
}
public String getWorkRoot() {
String currentNodeId = ClusterBridge.getView().getCurrent().getID();
if (workroots.containsKey(currentNodeId)) {
return (String) workroots.get(currentNodeId);
}
else if (isShared()) {
Iterator<String> iterator = workroots.get().values().iterator();
while (iterator.hasNext()) {
return iterator.next();
}
}
return null;
}
public void setWorkRoot(String workRoot) {
String currentNodeId = ClusterBridge.getView().getCurrent().getID();
if (isShared()) {
// 共享时改所有的 workroots
Set<String> ids = workroots.get().keySet();
for (String id : ids) {
workroots.put(id, workRoot);
}
}
// 改自己的
this.workroots.put(currentNodeId, workRoot);
}
public boolean isShared() {
return shared.get();
}
public void setShared(boolean shared) {
this.shared.set(shared);
}
public abstract T getConfig();
public abstract void setConfig(T config);
@Override
public RepositoryProfile<T> clone() throws CloneNotSupportedException {
RepositoryProfile<T> cloned = (RepositoryProfile<T>) super.clone();
cloned.identity = (Conf<String>) this.identity.clone();
cloned.repoName = (Conf<String>) this.repoName.clone();
cloned.shared = (Conf<Boolean>) this.shared.clone();
cloned.workroots = (MapConf<Map<String, String>>) this.workroots.clone();
return cloned;
}
/**
* 安装并返回生产的安装组件
*
* @return 仓库安装组件
* @throws RepositoryException
*/
public InstalledComponent<T> install() {
return ResourceModuleContext.install(this);
}
private boolean validate() {
if (StringUtils.isEmpty(getIdentity())) {
return false;
}
if (StringUtils.isEmpty(getRepoName())) {
return false;
}
// 不共享但是没有本机的workroot
if (!isShared() && StringUtils.isEmpty(getWorkRoot())) {
return false;
}
// 共享但是没有workroot配置
if (isShared() && workroots.get().isEmpty()) {
return false;
}
return true;
}
/**
* 是否适合本机安装
*
* @return
*/
public boolean suitable() {
if (ResourceModuleContext.getFactory(getIdentity()) == null) {
return false;
}
return validate();
}
@Override
public final void update(String srcRepo) {
InstalledComponent c = ResourceModuleContext.getInstalledComponent(srcRepo);
this.setWorkRoot(c.getWorkRoot());
this.setShared(c.isShared());
if (getConfig() != null) {
this.getConfig().update(srcRepo);
}
}
/**
* 仅生成安装组件(不安装)
*
* @return 仓库安装组件
*/
public final InstalledComponent<T> generateComponent() {
RepositoryFactoryProvider<T> fac = ResourceModuleContext.getFactory(getIdentity());
return new InstalledComponent<T>(fac, getRepoName(), getWorkRoot(), isShared());
}
}
可通过三组常见引入JS和CSS的插件接口对比引入以下JS接口
!(function () {
BI.config("dec.constant.intelligence.cluster.file.server", function (items) {
items.push({
value: "DEMO", //跟后台的RepositoryConfig#identity值一致
id: "decision-intelligence-cluster-file-demo",// 定一个前端唯一的ID值【下拉框选项】
text: "DEMO REPOSITORY",//该类型文件服务器的显示值【下拉框选项】
cardType: "dec.intelligence.cluster.file.demo"//配置文件服务器的组件
});
return items;
});
//下面是定义配置的组件getValue/validation 是该组件必须要实现的方法
var LABEL_WIDTH = 116, EDITOR_WIDTH = 300;
var DEMO = BI.inherit(BI.Widget, {
...
render: function () {
var self = this, o = this.options;
return {
type: "bi.vertical",
tgap: 15,
items: [
{
type: "dec.label.editor.item",
textWidth: LABEL_WIDTH,
editorWidth: EDITOR_WIDTH,
watermark: BI.i18nText("Dec-Please_Input"),
text: "ROOT",
value: this.model.root,//初始化显示配置的值
ref: function (_ref) {
self.rootRow = _ref;
},
listeners: [{
eventName: BI.Editor.EVENT_CHANGE,
action: function () {
self.store.setRoot(this.getValue());
}
}]
}]
};
},
getValue: function () {//getValue返回的值对应后台的config接口的实现。变量名跟后台config的GetConifg和SetConfig注解的value一致
return {
root: this.model.root//保存配置的值
};
},
validation: function () {//异常判断
var valid = true, root = this.model.root;
if (BI.isEmpty(root)) {
this.rootRow.showError(BI.i18nText("Dec-Error_Null"));
valid = false;
}
return valid;
}
});
...
})();
四、支持版本
五、插件注册
<extra-core>
<ResourceRepositoryFactoryProvider class="your class name"/>
</extra-core>
六、原理说明
文件服务器本身逻辑较为底层,涉及的相关引入和初始化及调用逻辑较为复杂,故本文不做单独介绍。
七、特殊限制说明
该接口的本质就是通过抽象的定义了一组文件操作接口方法,来适配各类文件存储媒介。
要实现的接口很多,但最终其实都是为调用ResourceRepository接口中操作文件的各种方法展开的
ResourceRepositoryFactoryProvider 是插件注册的入口,提供一个文件服务器的工厂给决策平台
RepositoryConfig是最终的自定义的文件存储媒介的可变配置对象。
RepositoryProfile则是前置的决策平台内部的一个文件服务器的配置对象,它包含了RepositoryConfig。
八、常用链接
demo地址:demo-resource-repository-factory-provider
三组常见引入JS和CSS的插件接口对比
九、开源案例
免责声明:所有文档中的开源示例,均为开发者自行开发并提供。仅用于参考和学习使用,开发者和官方均无义务对开源案例所涉及的所有成果进行教学和指导。若作为商用一切后果责任由使用者自行承担。