package jnpf.bizcommon.onlyoffice.service.impl; import jnpf.bizcommon.onlyoffice.entity.OnlyOfficeConfigParam; import jnpf.bizcommon.onlyoffice.entity.OnlyOfficeProperties; import jnpf.exception.DataException; import lombok.Getter; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; import org.springframework.util.ObjectUtils; import java.net.URI; import java.net.URISyntaxException; import java.util.LinkedHashMap; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Pattern; @Component @RequiredArgsConstructor public class OnlyOfficePluginResolver { private static final Pattern PATH_COMPONENT_PATTERN = Pattern.compile("[A-Za-z0-9._-]+"); private final OnlyOfficeProperties properties; public List resolve(OnlyOfficeConfigParam param) { if (ObjectUtils.isEmpty(param.getBizScene())) { return java.util.Collections.emptyList(); } OnlyOfficeProperties.PluginSceneMapping mapping = properties.getPlugins().getScenes().stream() .filter(item -> param.getBizScene().equals(item.getBizScene())) .findFirst() .orElseThrow(() -> new DataException("未配置的 OnlyOffice 插件场景:" + param.getBizScene())); if (!mapping.getBizModule().equals(param.getBizModule())) { throw new DataException("OnlyOffice 插件场景与业务模块不匹配"); } if (ObjectUtils.isEmpty(param.getBizDataId())) { throw new DataException("启用 OnlyOffice 插件时 bizDataId 不能为空"); } if (ObjectUtils.isEmpty(mapping.getPluginCodes())) { throw new DataException("OnlyOffice 插件场景未配置插件"); } String autostartPluginCode = mapping.getAutostartPluginCode(); if (!ObjectUtils.isEmpty(autostartPluginCode) && !mapping.getPluginCodes().contains(autostartPluginCode)) { throw new DataException("OnlyOffice 自动启动插件未包含在当前场景中"); } List resolved = new ArrayList<>(); Set guids = new HashSet<>(); for (String pluginCode : mapping.getPluginCodes()) { if (ObjectUtils.isEmpty(pluginCode) || !PATH_COMPONENT_PATTERN.matcher(pluginCode).matches()) { throw new DataException("OnlyOffice 插件编码配置无效"); } OnlyOfficeProperties.PluginDefinition definition = properties.getPlugins().getDefinitions().get(pluginCode); if (definition == null) { throw new DataException("OnlyOffice 插件定义不存在:" + pluginCode); } String guid = definition.getGuid(); if (ObjectUtils.isEmpty(guid) || !guids.add(guid)) { throw new DataException("OnlyOffice 插件 GUID 配置无效或重复"); } String version = definition.getVersion(); String release = definition.getRelease(); if (!validPathComponent(version) || !validPathComponent(release)) { throw new DataException("OnlyOffice 插件版本或 release 配置无效"); } String baseUrl = normalizedUrl(definition.getPublicBaseUrl(), "public-base-url"); String apiBaseUrl = normalizedUrl(definition.getApiBaseUrl(), "api-base-url"); resolved.add(new ResolvedPlugin(pluginCode, guid, version, baseUrl + "/" + release + "/" + pluginCode + "/config.json", apiBaseUrl, mapping.getBizScene(), mapping.getBizModule(), pluginCode.equals(autostartPluginCode), mapping.getDisabledPluginGuids())); } return resolved; } private boolean validPathComponent(String value) { return !ObjectUtils.isEmpty(value) && PATH_COMPONENT_PATTERN.matcher(value).matches(); } private String normalizedUrl(String value, String name) { if (ObjectUtils.isEmpty(value)) { throw new DataException("OnlyOffice 插件 " + name + " 未配置"); } String normalized = value.replaceAll("/+$", ""); try { URI uri = new URI(normalized); if (!("http".equals(uri.getScheme()) || "https".equals(uri.getScheme())) || ObjectUtils.isEmpty(uri.getHost()) || uri.getQuery() != null || uri.getFragment() != null) { throw new DataException("OnlyOffice 插件 " + name + " 配置无效"); } } catch (URISyntaxException e) { throw new DataException("OnlyOffice 插件 " + name + " 配置无效"); } return normalized; } @Getter public static class ResolvedPlugin { private final String manifestUrl; private final String pluginCode; private final String guid; private final String version; private final String apiBaseUrl; private final String bizScene; private final String bizModule; private final boolean autostart; private final List disabledPluginGuids; private ResolvedPlugin(String pluginCode, String guid, String version, String manifestUrl, String apiBaseUrl, String bizScene, String bizModule, boolean autostart, List disabledPluginGuids) { this.pluginCode = pluginCode; this.guid = guid; this.version = version; this.manifestUrl = manifestUrl; this.apiBaseUrl = apiBaseUrl; this.bizScene = bizScene; this.bizModule = bizModule; this.autostart = autostart; this.disabledPluginGuids = new ArrayList<>(disabledPluginGuids); } public Map runtimeContext(String documentId, String bizDataId) { Map context = new LinkedHashMap<>(); context.put("apiBaseUrl", apiBaseUrl); context.put("documentId", documentId); context.put("bizDataId", bizDataId); context.put("bizModule", bizModule); context.put("bizScene", bizScene); context.put("pluginVersion", version); return context; } } }