SNAPSHOT W7 已部署稳定态 — 凯迪ERP+OA一体化平台 (MET 73.3%)
恢复点(restore point)。别人改崩后可 git reset --hard 回到此提交。 == 此快照内容 == - 后端 oa-backend: 734 控制器 / 711 实体 (Spring Boot 3.2.5 + SQLite, 端口8091) - 前端 modern-ui/app: Vue3+Vite, 约700页 (构建产物已在 oa-backend/src/main/resources/static) - 数据库 oa-backend/data/oa.db: 含全部演示数据 (强制入库, 6.6MB) - 交接文档 go.md + go-code-reference/endpoints/entities/database.md - 多代理建设脚本 .claude/wf-*.js == 状态 == - 对 凯迪科技ERP_20260507.xlsx 合规 MET ~73.3% (PARTIAL 75: 34可建+6种子/bug+35外部硬天花板) - 安全: 5轮红队+5轮复检, default-deny分级鉴权, 连续零可利用 - W3~W7 累计补完436缺口; W8末轮(40缺口)为半成品(源码树可编译但未集成) - 运行: cd oa-backend; java -jar build/libs/oa-backend-0.1.0.jar --server.port=8091; admin/123456 == 排除(gitignore, 可再生) == node_modules / oa-backend/build / .jdks / *.log / Backup-ERP-* / 弃用的OFBiz核心(只保留modern-ui) 完整文件夹备份见同目录 Backup-ERP-20260615-191517/ (含上述全部, 仅缺 node_modules) 时间戳: 20260615-191517 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+76
@@ -0,0 +1,76 @@
|
||||
/*******************************************************************************
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF 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.apache.ofbiz.modernapi.core;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public final class ApiResult {
|
||||
private ApiResult() { }
|
||||
|
||||
public static Map<String, Object> ok(Object data, String traceId) {
|
||||
Map<String, Object> result = base(true, traceId);
|
||||
result.put("data", data);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static Map<String, Object> ok(Object data, List<String> messages, Map<String, Object> meta, String traceId) {
|
||||
Map<String, Object> result = ok(data, traceId);
|
||||
if (messages != null && !messages.isEmpty()) {
|
||||
result.put("messages", messages);
|
||||
}
|
||||
if (meta != null && !meta.isEmpty()) {
|
||||
result.put("meta", meta);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static Map<String, Object> error(String code, String message, String traceId) {
|
||||
return error(code, message, null, traceId);
|
||||
}
|
||||
|
||||
public static Map<String, Object> error(String code, String message, String field, String traceId) {
|
||||
Map<String, Object> result = base(false, traceId);
|
||||
List<Map<String, Object>> errors = new ArrayList<>();
|
||||
Map<String, Object> error = new LinkedHashMap<>();
|
||||
error.put("code", code);
|
||||
error.put("message", message);
|
||||
if (field != null && !field.isEmpty()) {
|
||||
error.put("field", field);
|
||||
}
|
||||
errors.add(error);
|
||||
result.put("errors", errors);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static Map<String, Object> warning(Object data, String warning, String traceId) {
|
||||
Map<String, Object> result = ok(data, traceId);
|
||||
result.put("warnings", List.of(warning));
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Map<String, Object> base(boolean ok, String traceId) {
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("ok", ok);
|
||||
result.put("traceId", traceId);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
/*******************************************************************************
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF 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.apache.ofbiz.modernapi.core;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletContext;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpFilter;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.apache.ofbiz.entity.GenericValue;
|
||||
import org.apache.ofbiz.webapp.control.LoginWorker;
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
public class ModernApiAuthFilter extends HttpFilter {
|
||||
@FunctionalInterface
|
||||
interface SecuredLoginBridge {
|
||||
String bridge(HttpServletRequest request, HttpServletResponse response);
|
||||
}
|
||||
|
||||
private final SecuredLoginBridge securedLoginBridge;
|
||||
|
||||
public ModernApiAuthFilter() {
|
||||
this(LoginWorker::securedUserLoginByJWTCookie);
|
||||
}
|
||||
|
||||
ModernApiAuthFilter(SecuredLoginBridge securedLoginBridge) {
|
||||
this.securedLoginBridge = securedLoginBridge;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
|
||||
seedOfbizContext(request);
|
||||
if (ModernApiUtil.userLogin(request) == null) {
|
||||
securedLoginBridge.bridge(request, response);
|
||||
}
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
|
||||
private void seedOfbizContext(HttpServletRequest request) {
|
||||
ServletContext servletContext = request.getServletContext();
|
||||
copyContextAttribute(request, servletContext, "delegator");
|
||||
copyContextAttribute(request, servletContext, "dispatcher");
|
||||
copyContextAttribute(request, servletContext, "security");
|
||||
}
|
||||
|
||||
private void copyContextAttribute(HttpServletRequest request, ServletContext servletContext, String name) {
|
||||
Object value = servletContext.getAttribute(name);
|
||||
if (value != null && request.getAttribute(name) == null) {
|
||||
request.setAttribute(name, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
/*******************************************************************************
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF 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.apache.ofbiz.modernapi.core;
|
||||
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import org.apache.ofbiz.base.util.Debug;
|
||||
import org.glassfish.jersey.jackson.JacksonFeature;
|
||||
import org.glassfish.jersey.logging.LoggingFeature;
|
||||
import org.glassfish.jersey.media.multipart.MultiPartFeature;
|
||||
import org.glassfish.jersey.server.ResourceConfig;
|
||||
|
||||
public class ModernApiConfig extends ResourceConfig {
|
||||
public ModernApiConfig() {
|
||||
packages("org.apache.ofbiz.modernapi.resources");
|
||||
register(JacksonFeature.class);
|
||||
register(MultiPartFeature.class);
|
||||
if (Debug.verboseOn()) {
|
||||
register(new LoggingFeature(Logger.getLogger(LoggingFeature.DEFAULT_LOGGER_NAME), Level.INFO,
|
||||
LoggingFeature.Verbosity.PAYLOAD_ANY, 10000));
|
||||
}
|
||||
}
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
/*******************************************************************************
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF 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.apache.ofbiz.modernapi.core;
|
||||
|
||||
import javax.servlet.ServletContext;
|
||||
import javax.servlet.ServletContextEvent;
|
||||
import javax.servlet.ServletContextListener;
|
||||
|
||||
import org.apache.ofbiz.base.util.Debug;
|
||||
import org.apache.ofbiz.entity.Delegator;
|
||||
import org.apache.ofbiz.service.LocalDispatcher;
|
||||
import org.apache.ofbiz.webapp.WebAppUtil;
|
||||
|
||||
public class ModernApiContextListener implements ServletContextListener {
|
||||
private static final String MODULE = ModernApiContextListener.class.getName();
|
||||
|
||||
@Override
|
||||
public void contextInitialized(ServletContextEvent sce) {
|
||||
ServletContext servletContext = sce.getServletContext();
|
||||
Delegator delegator = WebAppUtil.getDelegator(servletContext);
|
||||
LocalDispatcher dispatcher = WebAppUtil.getDispatcher(servletContext);
|
||||
servletContext.setAttribute("delegator", delegator);
|
||||
servletContext.setAttribute("dispatcher", dispatcher);
|
||||
servletContext.setAttribute("security", WebAppUtil.getSecurity(servletContext));
|
||||
Debug.logInfo("Modern API context initialized, delegator " + delegator + ", dispatcher " + dispatcher, MODULE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void contextDestroyed(ServletContextEvent sce) {
|
||||
ServletContext servletContext = sce.getServletContext();
|
||||
servletContext.removeAttribute("delegator");
|
||||
servletContext.removeAttribute("dispatcher");
|
||||
servletContext.removeAttribute("security");
|
||||
Debug.logInfo("Modern API context destroyed", MODULE);
|
||||
}
|
||||
}
|
||||
+238
@@ -0,0 +1,238 @@
|
||||
/*******************************************************************************
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF 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.apache.ofbiz.modernapi.core;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.sql.Time;
|
||||
import java.sql.Timestamp;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import javax.servlet.ServletContext;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.ws.rs.core.Response;
|
||||
|
||||
import org.apache.ofbiz.entity.Delegator;
|
||||
import org.apache.ofbiz.entity.GenericValue;
|
||||
import org.apache.ofbiz.entity.model.ModelEntity;
|
||||
import org.apache.ofbiz.security.Security;
|
||||
import org.apache.ofbiz.service.LocalDispatcher;
|
||||
|
||||
public final class ModernApiUtil {
|
||||
private ModernApiUtil() { }
|
||||
|
||||
public static String traceId() {
|
||||
return UUID.randomUUID().toString();
|
||||
}
|
||||
|
||||
public static Response ok(Object data, String traceId) {
|
||||
return Response.ok(ApiResult.ok(data, traceId)).build();
|
||||
}
|
||||
|
||||
public static Response error(Response.Status status, String code, String message, String traceId) {
|
||||
return Response.status(status).entity(ApiResult.error(code, message, traceId)).build();
|
||||
}
|
||||
|
||||
public static Delegator delegator(ServletContext servletContext) {
|
||||
return (Delegator) servletContext.getAttribute("delegator");
|
||||
}
|
||||
|
||||
public static LocalDispatcher dispatcher(ServletContext servletContext) {
|
||||
return (LocalDispatcher) servletContext.getAttribute("dispatcher");
|
||||
}
|
||||
|
||||
public static Security security(ServletContext servletContext) {
|
||||
return (Security) servletContext.getAttribute("security");
|
||||
}
|
||||
|
||||
public static GenericValue userLogin(HttpServletRequest request) {
|
||||
Object requestUserLogin = request.getAttribute("userLogin");
|
||||
if (requestUserLogin instanceof GenericValue) {
|
||||
return (GenericValue) requestUserLogin;
|
||||
}
|
||||
Object sessionUserLogin = request.getSession(false) == null ? null : request.getSession(false).getAttribute("userLogin");
|
||||
return sessionUserLogin instanceof GenericValue ? (GenericValue) sessionUserLogin : null;
|
||||
}
|
||||
|
||||
public static boolean hasViewPermission(Security security, GenericValue userLogin, String permission) {
|
||||
if (permission == null || permission.isEmpty() || "NONE".equals(permission)) {
|
||||
return true;
|
||||
}
|
||||
if (security == null || userLogin == null) {
|
||||
return false;
|
||||
}
|
||||
return security.hasEntityPermission(permission, "_VIEW", userLogin)
|
||||
|| security.hasEntityPermission(permission, "_ADMIN", userLogin)
|
||||
|| security.hasPermission(permission + "_VIEW", userLogin)
|
||||
|| security.hasPermission(permission + "_ADMIN", userLogin);
|
||||
}
|
||||
|
||||
public static boolean hasEntityViewPermission(Security security, GenericValue userLogin, ModelEntity modelEntity) {
|
||||
if (security == null || userLogin == null || modelEntity == null) {
|
||||
return false;
|
||||
}
|
||||
if (hasViewPermission(security, userLogin, "ENTITY_DATA")) {
|
||||
return true;
|
||||
}
|
||||
for (String permission : entityViewPermissions(modelEntity)) {
|
||||
if (hasViewPermission(security, userLogin, permission)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static int safePage(int page) {
|
||||
return Math.max(0, page);
|
||||
}
|
||||
|
||||
public static int safePageSize(int pageSize) {
|
||||
return Math.max(1, Math.min(pageSize, 100));
|
||||
}
|
||||
|
||||
public static boolean hasMore(int page, int pageSize, long total) {
|
||||
long safeTotal = Math.max(0, total);
|
||||
return ((long) (safePage(page) + 1) * safePageSize(pageSize)) < safeTotal;
|
||||
}
|
||||
|
||||
public static Map<String, Object> pagination(int page, int pageSize, long total) {
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
int safePage = safePage(page);
|
||||
int safePageSize = safePageSize(pageSize);
|
||||
long safeTotal = Math.max(0, total);
|
||||
data.put("page", safePage);
|
||||
data.put("pageSize", safePageSize);
|
||||
data.put("total", safeTotal);
|
||||
data.put("hasMore", hasMore(safePage, safePageSize, safeTotal));
|
||||
return data;
|
||||
}
|
||||
|
||||
public static List<String> safeOrderByFields(Collection<String> fieldNames, List<String> pkFieldNames, String orderBy) {
|
||||
List<String> safeFields = new ArrayList<>();
|
||||
Set<String> allowedFields = new HashSet<>(fieldNames);
|
||||
if (orderBy != null && !orderBy.isBlank()) {
|
||||
for (String rawField : orderBy.split(",")) {
|
||||
String field = rawField.trim();
|
||||
if (field.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
String upper = field.toUpperCase(Locale.ROOT);
|
||||
String suffix = "";
|
||||
if (upper.endsWith(" DESC")) {
|
||||
suffix = " DESC";
|
||||
field = field.substring(0, field.length() - 5).trim();
|
||||
} else if (upper.endsWith(" ASC")) {
|
||||
suffix = " ASC";
|
||||
field = field.substring(0, field.length() - 4).trim();
|
||||
} else if (field.startsWith("-")) {
|
||||
suffix = " DESC";
|
||||
field = field.substring(1).trim();
|
||||
}
|
||||
if (allowedFields.contains(field)) {
|
||||
safeFields.add(field + suffix);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!safeFields.isEmpty()) {
|
||||
return safeFields;
|
||||
}
|
||||
return new ArrayList<>(pkFieldNames);
|
||||
}
|
||||
|
||||
private static List<String> entityViewPermissions(ModelEntity modelEntity) {
|
||||
List<String> permissions = new ArrayList<>();
|
||||
String packageName = modelEntity.getPackageName() == null ? "" : modelEntity.getPackageName().toLowerCase(Locale.ROOT);
|
||||
addPackagePermission(permissions, packageName, ".order.", "ORDERMGR");
|
||||
addPackagePermission(permissions, packageName, ".party.", "PARTYMGR");
|
||||
addPackagePermission(permissions, packageName, ".product.", "CATALOG");
|
||||
addPackagePermission(permissions, packageName, ".shipment.", "FACILITY");
|
||||
addPackagePermission(permissions, packageName, ".accounting.", "ACCOUNTING");
|
||||
addPackagePermission(permissions, packageName, ".humanres.", "HUMANRES");
|
||||
addPackagePermission(permissions, packageName, ".manufacturing.", "MANUFACTURING");
|
||||
addPackagePermission(permissions, packageName, ".workeffort.", "WORKEFFORTMGR");
|
||||
addPackagePermission(permissions, packageName, ".content.", "CONTENTMGR");
|
||||
addPackagePermission(permissions, packageName, ".marketing.", "MARKETING");
|
||||
addPackagePermission(permissions, packageName, ".webapp.website", "CONTENTMGR");
|
||||
addPackagePermission(permissions, packageName, ".security.", "SECURITY");
|
||||
addPackagePermission(permissions, packageName, ".webtools.", "WEBTOOLS");
|
||||
if (isCommonReferenceEntity(modelEntity)) {
|
||||
addPermission(permissions, "ORDERMGR");
|
||||
addPermission(permissions, "PARTYMGR");
|
||||
addPermission(permissions, "CATALOG");
|
||||
addPermission(permissions, "FACILITY");
|
||||
addPermission(permissions, "ACCOUNTING");
|
||||
addPermission(permissions, "HUMANRES");
|
||||
addPermission(permissions, "MANUFACTURING");
|
||||
addPermission(permissions, "WORKEFFORTMGR");
|
||||
addPermission(permissions, "CONTENTMGR");
|
||||
}
|
||||
return permissions;
|
||||
}
|
||||
|
||||
private static void addPackagePermission(List<String> permissions, String packageName, String marker, String permission) {
|
||||
if (packageName.contains(marker)) {
|
||||
addPermission(permissions, permission);
|
||||
}
|
||||
}
|
||||
|
||||
private static void addPermission(List<String> permissions, String permission) {
|
||||
if (!permissions.contains(permission)) {
|
||||
permissions.add(permission);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isCommonReferenceEntity(ModelEntity modelEntity) {
|
||||
String packageName = modelEntity.getPackageName() == null ? "" : modelEntity.getPackageName().toLowerCase(Locale.ROOT);
|
||||
if (!packageName.contains(".common.")) {
|
||||
return false;
|
||||
}
|
||||
String entityName = modelEntity.getEntityName();
|
||||
return entityName != null && (entityName.endsWith("Type") || entityName.endsWith("Item") || entityName.endsWith("Enum")
|
||||
|| entityName.endsWith("Enumeration") || entityName.endsWith("Geo") || entityName.endsWith("Uom"));
|
||||
}
|
||||
|
||||
public static Map<String, Object> publicFields(GenericValue value) {
|
||||
Map<String, Object> fields = new LinkedHashMap<>();
|
||||
for (Map.Entry<String, Object> entry : value.getAllFields().entrySet()) {
|
||||
fields.put(entry.getKey(), serializableValue(entry.getValue()));
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
public static Object serializableValue(Object value) {
|
||||
if (value instanceof Timestamp || value instanceof Time || value instanceof java.sql.Date) {
|
||||
return value.toString();
|
||||
}
|
||||
if (value instanceof Date) {
|
||||
return Long.toString(((Date) value).getTime());
|
||||
}
|
||||
if (value instanceof BigDecimal) {
|
||||
return value.toString();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
/*******************************************************************************
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF 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.apache.ofbiz.modernapi.core;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import org.apache.ofbiz.base.location.FlexibleLocation;
|
||||
import org.apache.ofbiz.base.util.Debug;
|
||||
|
||||
public final class UiInventoryLoader {
|
||||
private static final String MODULE = UiInventoryLoader.class.getName();
|
||||
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||
private static Map<String, Object> cachedInventory;
|
||||
private static String cachedInventoryPath;
|
||||
private static long cachedInventoryLastModified = -1;
|
||||
|
||||
private UiInventoryLoader() { }
|
||||
|
||||
public static synchronized Map<String, Object> load() {
|
||||
File generated = new File("plugins/modern-api/generated/ui-inventory.json");
|
||||
if (!generated.exists()) {
|
||||
try {
|
||||
URL generatedUrl = FlexibleLocation.resolveLocation("component://modern-api/generated/ui-inventory.json");
|
||||
generated = new File(generatedUrl.getPath());
|
||||
} catch (Exception e) {
|
||||
Debug.logWarning(e, "Modern UI inventory has not been generated yet", MODULE);
|
||||
}
|
||||
}
|
||||
if (!generated.exists()) {
|
||||
if (cachedInventory != null && cachedInventoryPath == null) {
|
||||
return cachedInventory;
|
||||
}
|
||||
cachedInventory = emptyInventory();
|
||||
cachedInventoryPath = null;
|
||||
cachedInventoryLastModified = -1;
|
||||
return cachedInventory;
|
||||
}
|
||||
String inventoryPath = generated.getAbsolutePath();
|
||||
long inventoryLastModified = generated.lastModified();
|
||||
if (cachedInventory != null
|
||||
&& inventoryPath.equals(cachedInventoryPath)
|
||||
&& inventoryLastModified == cachedInventoryLastModified) {
|
||||
return cachedInventory;
|
||||
}
|
||||
try {
|
||||
cachedInventory = MAPPER.readValue(generated, new TypeReference<Map<String, Object>>() { });
|
||||
cachedInventoryPath = inventoryPath;
|
||||
cachedInventoryLastModified = inventoryLastModified;
|
||||
} catch (IOException e) {
|
||||
Debug.logError(e, "Unable to read Modern UI inventory", MODULE);
|
||||
cachedInventory = emptyInventory();
|
||||
cachedInventoryPath = inventoryPath;
|
||||
cachedInventoryLastModified = inventoryLastModified;
|
||||
}
|
||||
return cachedInventory;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static Map<String, Object> findPage(String pageId) {
|
||||
Object pageDefinitions = load().get("pageDefinitions");
|
||||
if (!(pageDefinitions instanceof Map)) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
Object page = ((Map<String, Object>) pageDefinitions).get(pageId);
|
||||
return page instanceof Map ? (Map<String, Object>) page : Collections.emptyMap();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static Map<String, Object> findAction(String actionId) {
|
||||
Object actionDefinitions = load().get("actionDefinitions");
|
||||
if (!(actionDefinitions instanceof Map)) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
Object action = ((Map<String, Object>) actionDefinitions).get(actionId);
|
||||
return action instanceof Map ? (Map<String, Object>) action : Collections.emptyMap();
|
||||
}
|
||||
|
||||
private static Map<String, Object> emptyInventory() {
|
||||
Map<String, Object> inventory = new LinkedHashMap<>();
|
||||
inventory.put("generatedAt", null);
|
||||
inventory.put("counts", Collections.emptyMap());
|
||||
inventory.put("controllers", Collections.emptyList());
|
||||
inventory.put("widgets", Collections.emptyList());
|
||||
inventory.put("services", Collections.emptyList());
|
||||
inventory.put("pageDefinitions", Collections.emptyMap());
|
||||
inventory.put("routeManifest", Collections.emptyList());
|
||||
inventory.put("actionDefinitions", Collections.emptyMap());
|
||||
inventory.put("parityManifest", emptyParityManifest());
|
||||
return inventory;
|
||||
}
|
||||
|
||||
private static Map<String, Object> emptyParityManifest() {
|
||||
Map<String, Object> summary = new LinkedHashMap<>();
|
||||
summary.put("totalPages", 0);
|
||||
summary.put("elementPlusRenderablePages", 0);
|
||||
summary.put("routeRenderedPages", 0);
|
||||
summary.put("adapterCoveredPages", 0);
|
||||
summary.put("customVueRequiredPages", 0);
|
||||
summary.put("pendingE2ePages", 0);
|
||||
summary.put("templateParityPages", 0);
|
||||
summary.put("highRiskPages", 0);
|
||||
summary.put("mediumRiskPages", 0);
|
||||
summary.put("lowRiskPages", 0);
|
||||
|
||||
Map<String, Object> manifest = new LinkedHashMap<>();
|
||||
manifest.put("summary", summary);
|
||||
manifest.put("byDomain", Collections.emptyList());
|
||||
manifest.put("byComponent", Collections.emptyList());
|
||||
manifest.put("byAdapter", Collections.emptyList());
|
||||
manifest.put("byRequirement", Collections.emptyList());
|
||||
manifest.put("byRisk", Collections.emptyMap());
|
||||
manifest.put("pages", Collections.emptyList());
|
||||
return manifest;
|
||||
}
|
||||
}
|
||||
+403
@@ -0,0 +1,403 @@
|
||||
/*******************************************************************************
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF 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.apache.ofbiz.modernapi.resources;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.Enumeration;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.servlet.ServletContext;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletRequestWrapper;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpServletResponseWrapper;
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
|
||||
import org.apache.ofbiz.entity.GenericValue;
|
||||
import org.apache.ofbiz.modernapi.core.ApiResult;
|
||||
import org.apache.ofbiz.modernapi.core.ModernApiUtil;
|
||||
import org.apache.ofbiz.modernapi.core.UiInventoryLoader;
|
||||
import org.apache.ofbiz.service.GenericServiceException;
|
||||
import org.apache.ofbiz.service.LocalDispatcher;
|
||||
import org.apache.ofbiz.service.ModelParam;
|
||||
import org.apache.ofbiz.service.ModelService;
|
||||
import org.apache.ofbiz.service.ServiceUtil;
|
||||
import org.apache.ofbiz.webapp.control.ConfigXMLReader;
|
||||
import org.apache.ofbiz.webapp.event.EventHandler;
|
||||
import org.apache.ofbiz.webapp.event.EventHandlerException;
|
||||
import org.apache.ofbiz.webapp.event.GroovyEventHandler;
|
||||
import org.apache.ofbiz.webapp.event.JavaEventHandler;
|
||||
import org.apache.ofbiz.webapp.event.RomeEventHandler;
|
||||
import org.apache.ofbiz.webapp.event.ServiceMultiEventHandler;
|
||||
import org.apache.ofbiz.webapp.event.SimpleEventHandler;
|
||||
|
||||
@Path("/v1/actions")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public class ActionResource {
|
||||
@Context private ServletContext servletContext;
|
||||
@Context private HttpServletRequest request;
|
||||
@Context private HttpServletResponse response;
|
||||
|
||||
@POST
|
||||
@Path("/{actionId}")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public Response runAction(@PathParam("actionId") String actionId, Map<String, Object> payload) {
|
||||
String traceId = ModernApiUtil.traceId();
|
||||
LocalDispatcher dispatcher = ModernApiUtil.dispatcher(servletContext);
|
||||
GenericValue userLogin = ModernApiUtil.userLogin(request);
|
||||
Map<String, Object> actionDefinition = UiInventoryLoader.findAction(actionId);
|
||||
Map<String, Object> input = payload == null ? new LinkedHashMap<>() : new LinkedHashMap<>(payload);
|
||||
String eventType = stringValue(actionDefinition.get("eventType"));
|
||||
if (isWebEvent(eventType)) {
|
||||
return runWebEvent(actionId, actionDefinition, input, userLogin, traceId);
|
||||
}
|
||||
Map<String, Object> navigationTarget = navigationTarget(actionDefinition);
|
||||
if (!navigationTarget.isEmpty()) {
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("actionId", actionId);
|
||||
data.put("definition", actionDefinition);
|
||||
data.put("executed", true);
|
||||
data.put("actionType", "navigation");
|
||||
data.put("navigationTarget", navigationTarget.get("modernPath"));
|
||||
data.put("navigationPageId", navigationTarget.get("pageId"));
|
||||
data.put("legacyResponse", navigationTarget);
|
||||
data.put("payload", input);
|
||||
data.put("reason", "Legacy controller request resolves to a view response and is handled as modern SPA navigation.");
|
||||
return ModernApiUtil.ok(data, traceId);
|
||||
}
|
||||
String serviceName = serviceNameFor(actionId, actionDefinition);
|
||||
if (serviceName == null || serviceName.isBlank()) {
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("actionId", actionId);
|
||||
data.put("definition", actionDefinition);
|
||||
data.put("executed", false);
|
||||
data.put("reason", "Only OFBiz service actions and supported web events can be executed through /api/v1/actions.");
|
||||
return ModernApiUtil.ok(data, traceId);
|
||||
}
|
||||
if (userLogin == null) {
|
||||
return ModernApiUtil.error(Response.Status.UNAUTHORIZED, "AUTH_REQUIRED", "A valid OFBiz userLogin is required to execute actions.", traceId);
|
||||
}
|
||||
if (dispatcher == null) {
|
||||
return ModernApiUtil.error(Response.Status.SERVICE_UNAVAILABLE, "DISPATCHER_UNAVAILABLE", "OFBiz dispatcher is not available.", traceId);
|
||||
}
|
||||
input.put("userLogin", userLogin);
|
||||
try {
|
||||
Map<String, Object> serviceContext = dispatcher.getDispatchContext().makeValidContext(serviceName, ModelService.IN_PARAM, input);
|
||||
Map<String, Object> serviceResult = dispatcher.runSync(serviceName, serviceContext);
|
||||
if (!ServiceUtil.isSuccess(serviceResult)) {
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(ApiResult.error("SERVICE_ERROR", ServiceUtil.getErrorMessage(serviceResult), traceId))
|
||||
.build();
|
||||
}
|
||||
ModelService service = dispatcher.getDispatchContext().getModelService(serviceName);
|
||||
Set<String> outParamNames = service.getOutParamNames();
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("actionId", actionId);
|
||||
data.put("serviceName", serviceName);
|
||||
data.put("executed", true);
|
||||
Map<String, Object> outParams = new LinkedHashMap<>();
|
||||
for (String outParamName : outParamNames) {
|
||||
ModelParam outParam = service.getParam(outParamName);
|
||||
if (!outParam.isInternal() && serviceResult.containsKey(outParamName)) {
|
||||
outParams.put(outParamName, ModernApiUtil.serializableValue(serviceResult.get(outParamName)));
|
||||
}
|
||||
}
|
||||
data.put("result", outParams);
|
||||
return ModernApiUtil.ok(data, traceId);
|
||||
} catch (GenericServiceException e) {
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(ApiResult.error("SERVICE_EXCEPTION", e.getMessage(), traceId))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
private Response runWebEvent(String actionId, Map<String, Object> actionDefinition,
|
||||
Map<String, Object> payload, GenericValue userLogin, String traceId) {
|
||||
if (requiresAuth(actionDefinition) && userLogin == null) {
|
||||
return ModernApiUtil.error(Response.Status.UNAUTHORIZED, "AUTH_REQUIRED", "A valid OFBiz userLogin is required to execute actions.", traceId);
|
||||
}
|
||||
String eventType = stringValue(actionDefinition.get("eventType"));
|
||||
String eventPath = stringValue(actionDefinition.get("eventPath"));
|
||||
String eventInvoke = stringValue(actionDefinition.get("eventInvoke"));
|
||||
if (eventInvoke.isBlank()) {
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("actionId", actionId);
|
||||
data.put("definition", actionDefinition);
|
||||
data.put("executed", false);
|
||||
data.put("reason", "The legacy controller action has no executable event invoke target.");
|
||||
return ModernApiUtil.ok(data, traceId);
|
||||
}
|
||||
try {
|
||||
EventHandler handler = eventHandler(eventType);
|
||||
HttpServletRequest eventRequest = new PayloadRequestWrapper(request, payload);
|
||||
CapturingResponseWrapper eventResponse = new CapturingResponseWrapper(response);
|
||||
seedRequestContext(eventRequest, userLogin);
|
||||
ConfigXMLReader.Event event = new ConfigXMLReader.Event(eventType, eventPath, eventInvoke, true);
|
||||
String eventReturn = handler.invoke(event, null, eventRequest, eventResponse);
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("actionId", actionId);
|
||||
data.put("eventType", eventType);
|
||||
data.put("eventPath", eventPath);
|
||||
data.put("eventInvoke", eventInvoke);
|
||||
data.put("eventReturn", eventReturn);
|
||||
data.put("executed", !"error".equalsIgnoreCase(eventReturn));
|
||||
data.put("redirectLocation", eventResponse.redirectLocation());
|
||||
data.put("httpStatus", eventResponse.status());
|
||||
data.put("messages", eventMessages(eventRequest));
|
||||
data.put("payload", payload);
|
||||
if ("error".equalsIgnoreCase(eventReturn)) {
|
||||
data.put("reason", firstMessage(eventRequest, "Legacy event returned error."));
|
||||
}
|
||||
return ModernApiUtil.ok(data, traceId);
|
||||
} catch (EventHandlerException | RuntimeException e) {
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(ApiResult.error("EVENT_EXCEPTION", e.getMessage(), traceId))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
private void seedRequestContext(HttpServletRequest eventRequest, GenericValue userLogin) {
|
||||
eventRequest.setAttribute("delegator", ModernApiUtil.delegator(servletContext));
|
||||
eventRequest.setAttribute("dispatcher", ModernApiUtil.dispatcher(servletContext));
|
||||
eventRequest.setAttribute("security", ModernApiUtil.security(servletContext));
|
||||
if (userLogin != null) {
|
||||
eventRequest.setAttribute("userLogin", userLogin);
|
||||
eventRequest.getSession().setAttribute("userLogin", userLogin);
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, Object> eventMessages(HttpServletRequest eventRequest) {
|
||||
Map<String, Object> messages = new LinkedHashMap<>();
|
||||
copyRequestAttribute(eventRequest, messages, "_EVENT_MESSAGE_");
|
||||
copyRequestAttribute(eventRequest, messages, "_EVENT_MESSAGE_LIST_");
|
||||
copyRequestAttribute(eventRequest, messages, "_ERROR_MESSAGE_");
|
||||
copyRequestAttribute(eventRequest, messages, "_ERROR_MESSAGE_LIST_");
|
||||
return messages;
|
||||
}
|
||||
|
||||
private void copyRequestAttribute(HttpServletRequest eventRequest, Map<String, Object> messages, String name) {
|
||||
Object value = eventRequest.getAttribute(name);
|
||||
if (value != null) {
|
||||
messages.put(name, ModernApiUtil.serializableValue(value));
|
||||
}
|
||||
}
|
||||
|
||||
private String firstMessage(HttpServletRequest eventRequest, String fallback) {
|
||||
for (String name : List.of("_ERROR_MESSAGE_", "_EVENT_MESSAGE_")) {
|
||||
Object value = eventRequest.getAttribute(name);
|
||||
if (value != null && !value.toString().isBlank()) {
|
||||
return value.toString();
|
||||
}
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
private EventHandler eventHandler(String eventType) throws EventHandlerException {
|
||||
EventHandler handler;
|
||||
switch (eventType) {
|
||||
case "java":
|
||||
handler = new JavaEventHandler();
|
||||
break;
|
||||
case "simple":
|
||||
handler = new SimpleEventHandler();
|
||||
break;
|
||||
case "groovy":
|
||||
handler = new GroovyEventHandler();
|
||||
break;
|
||||
case "service-multi":
|
||||
handler = new ServiceMultiEventHandler();
|
||||
break;
|
||||
case "rome":
|
||||
handler = new RomeEventHandler();
|
||||
break;
|
||||
default:
|
||||
throw new EventHandlerException("Unsupported event type: " + eventType);
|
||||
}
|
||||
handler.init(servletContext);
|
||||
return handler;
|
||||
}
|
||||
|
||||
private boolean isWebEvent(String eventType) {
|
||||
return Set.of("java", "simple", "groovy", "service-multi", "rome").contains(eventType);
|
||||
}
|
||||
|
||||
private boolean requiresAuth(Map<String, Object> actionDefinition) {
|
||||
Object auth = actionDefinition.get("auth");
|
||||
return auth instanceof Boolean ? (Boolean) auth : true;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, Object> navigationTarget(Map<String, Object> actionDefinition) {
|
||||
String eventType = stringValue(actionDefinition.get("eventType"));
|
||||
if (!eventType.isBlank()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
Object responses = actionDefinition.get("responses");
|
||||
if (!(responses instanceof Iterable<?>)) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
for (Object response : (Iterable<?>) responses) {
|
||||
if (!(response instanceof Map<?, ?>)) {
|
||||
continue;
|
||||
}
|
||||
Map<String, Object> responseMap = (Map<String, Object>) response;
|
||||
if ("view".equals(responseMap.get("type")) && responseMap.get("modernPath") != null) {
|
||||
return responseMap;
|
||||
}
|
||||
}
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
private String serviceNameFor(String actionId, Map<String, Object> actionDefinition) {
|
||||
if (actionDefinition == null || actionDefinition.isEmpty()) {
|
||||
return actionId;
|
||||
}
|
||||
Object serviceName = actionDefinition.get("serviceName");
|
||||
if (serviceName instanceof String && !((String) serviceName).isBlank()) {
|
||||
return (String) serviceName;
|
||||
}
|
||||
Object eventType = actionDefinition.get("eventType");
|
||||
Object eventInvoke = actionDefinition.get("eventInvoke");
|
||||
if ("service".equals(eventType) && eventInvoke instanceof String) {
|
||||
return (String) eventInvoke;
|
||||
}
|
||||
if ("service".equals(actionDefinition.get("source"))) {
|
||||
return actionId;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private String stringValue(Object value) {
|
||||
return value == null ? "" : value.toString();
|
||||
}
|
||||
|
||||
private static final class PayloadRequestWrapper extends HttpServletRequestWrapper {
|
||||
private final Map<String, String[]> parameters = new LinkedHashMap<>();
|
||||
|
||||
private PayloadRequestWrapper(HttpServletRequest request, Map<String, Object> payload) {
|
||||
super(request);
|
||||
parameters.putAll(request.getParameterMap());
|
||||
for (Map.Entry<String, Object> entry : payload.entrySet()) {
|
||||
parameters.put(entry.getKey(), stringArray(entry.getValue()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getParameter(String name) {
|
||||
String[] values = parameters.get(name);
|
||||
return values == null || values.length == 0 ? null : values[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String[]> getParameterMap() {
|
||||
return Collections.unmodifiableMap(parameters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Enumeration<String> getParameterNames() {
|
||||
return Collections.enumeration(parameters.keySet());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getParameterValues(String name) {
|
||||
return parameters.get(name);
|
||||
}
|
||||
|
||||
private static String[] stringArray(Object value) {
|
||||
if (value == null) {
|
||||
return new String[] { "" };
|
||||
}
|
||||
if (value instanceof Iterable<?>) {
|
||||
return iterableToArray((Iterable<?>) value);
|
||||
}
|
||||
if (value instanceof Object[]) {
|
||||
Object[] values = (Object[]) value;
|
||||
String[] result = new String[values.length];
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
result[i] = values[i] == null ? "" : values[i].toString();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return new String[] { value.toString() };
|
||||
}
|
||||
|
||||
private static String[] iterableToArray(Iterable<?> values) {
|
||||
java.util.ArrayList<String> result = new java.util.ArrayList<>();
|
||||
for (Object value : values) {
|
||||
result.add(value == null ? "" : value.toString());
|
||||
}
|
||||
return result.toArray(new String[0]);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class CapturingResponseWrapper extends HttpServletResponseWrapper {
|
||||
private int status = SC_OK;
|
||||
private String redirectLocation = "";
|
||||
|
||||
private CapturingResponseWrapper(HttpServletResponse response) {
|
||||
super(response);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendRedirect(String location) throws IOException {
|
||||
this.redirectLocation = location;
|
||||
this.status = SC_FOUND;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendError(int sc) throws IOException {
|
||||
this.status = sc;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendError(int sc, String msg) throws IOException {
|
||||
this.status = sc;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setStatus(int sc) {
|
||||
this.status = sc;
|
||||
super.setStatus(sc);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
private int status() {
|
||||
return status;
|
||||
}
|
||||
|
||||
private String redirectLocation() {
|
||||
return redirectLocation;
|
||||
}
|
||||
}
|
||||
}
|
||||
+192
@@ -0,0 +1,192 @@
|
||||
/*******************************************************************************
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF 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.apache.ofbiz.modernapi.resources;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Enumeration;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.servlet.ServletContext;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletRequestWrapper;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpSession;
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
|
||||
import org.apache.ofbiz.base.util.UtilValidate;
|
||||
import org.apache.ofbiz.entity.Delegator;
|
||||
import org.apache.ofbiz.entity.GenericValue;
|
||||
import org.apache.ofbiz.modernapi.core.ApiResult;
|
||||
import org.apache.ofbiz.modernapi.core.ModernApiUtil;
|
||||
import org.apache.ofbiz.webapp.control.LoginWorker;
|
||||
|
||||
@Path("/v1")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public class AuthResource {
|
||||
@Context private ServletContext servletContext;
|
||||
@Context private HttpServletRequest request;
|
||||
@Context private HttpServletResponse response;
|
||||
|
||||
@POST
|
||||
@Path("/login")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public Response login(Map<String, Object> payload) {
|
||||
String traceId = ModernApiUtil.traceId();
|
||||
Map<String, String[]> parameters = loginParameters(payload);
|
||||
LoginRequestWrapper loginRequest = new LoginRequestWrapper(request, parameters);
|
||||
seedRequestContext(loginRequest);
|
||||
|
||||
String loginResult = LoginWorker.login(loginRequest, response);
|
||||
GenericValue userLogin = ModernApiUtil.userLogin(loginRequest);
|
||||
if (!"success".equalsIgnoreCase(loginResult) || userLogin == null) {
|
||||
String message = firstMessage(loginRequest, "用户名或密码不正确,或当前账号无权进入 OFBiz 后台。");
|
||||
return Response.status(Response.Status.UNAUTHORIZED)
|
||||
.entity(ApiResult.error("LOGIN_FAILED", message, traceId))
|
||||
.build();
|
||||
}
|
||||
|
||||
return ModernApiUtil.ok(sessionData(loginRequest, userLogin), traceId);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/logout")
|
||||
public Response logout() {
|
||||
String traceId = ModernApiUtil.traceId();
|
||||
GenericValue userLogin = ModernApiUtil.userLogin(request);
|
||||
if (userLogin != null) {
|
||||
seedRequestContext(request);
|
||||
LoginWorker.logout(request, response);
|
||||
} else {
|
||||
HttpSession session = request.getSession(false);
|
||||
if (session != null) {
|
||||
session.invalidate();
|
||||
}
|
||||
}
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("authenticated", false);
|
||||
data.put("user", null);
|
||||
return ModernApiUtil.ok(data, traceId);
|
||||
}
|
||||
|
||||
private void seedRequestContext(HttpServletRequest targetRequest) {
|
||||
targetRequest.setAttribute("delegator", ModernApiUtil.delegator(servletContext));
|
||||
targetRequest.setAttribute("dispatcher", ModernApiUtil.dispatcher(servletContext));
|
||||
targetRequest.setAttribute("security", ModernApiUtil.security(servletContext));
|
||||
}
|
||||
|
||||
private Map<String, Object> sessionData(HttpServletRequest loginRequest, GenericValue userLogin) {
|
||||
Delegator delegator = ModernApiUtil.delegator(servletContext);
|
||||
Locale locale = loginRequest.getLocale();
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("authenticated", true);
|
||||
data.put("user", Map.of(
|
||||
"userLoginId", userLogin.getString("userLoginId"),
|
||||
"partyId", userLogin.getString("partyId")));
|
||||
data.put("locale", locale == null ? "en" : locale.toLanguageTag());
|
||||
data.put("tenant", delegator == null ? null : delegator.getDelegatorTenantId());
|
||||
data.put("theme", Map.of(
|
||||
"name", "modern-element-plus",
|
||||
"density", "compact",
|
||||
"navigation", "module-sidebar"));
|
||||
data.put("permissions", Map.of("source", "OFBiz Security", "resolved", true));
|
||||
return data;
|
||||
}
|
||||
|
||||
private Map<String, String[]> loginParameters(Map<String, Object> payload) {
|
||||
Map<String, String[]> parameters = new LinkedHashMap<>();
|
||||
parameters.put("USERNAME", new String[] { stringValue(payload, "username") });
|
||||
parameters.put("PASSWORD", new String[] { stringValue(payload, "password") });
|
||||
parameters.put("TOKEN", new String[] { stringValue(payload, "token") });
|
||||
parameters.put("JavaScriptEnabled", new String[] { "Y" });
|
||||
String tenantId = stringValue(payload, "tenantId");
|
||||
if (UtilValidate.isNotEmpty(tenantId)) {
|
||||
parameters.put("userTenantId", new String[] { tenantId });
|
||||
}
|
||||
return parameters;
|
||||
}
|
||||
|
||||
private String stringValue(Map<String, Object> payload, String name) {
|
||||
if (payload == null) {
|
||||
return "";
|
||||
}
|
||||
Object value = payload.get(name);
|
||||
return value == null ? "" : value.toString();
|
||||
}
|
||||
|
||||
private String firstMessage(HttpServletRequest loginRequest, String fallback) {
|
||||
for (String name : List.of("_ERROR_MESSAGE_", "_EVENT_MESSAGE_")) {
|
||||
Object value = loginRequest.getAttribute(name);
|
||||
if (value != null && !value.toString().isBlank()) {
|
||||
return value.toString();
|
||||
}
|
||||
}
|
||||
for (String name : List.of("_ERROR_MESSAGE_LIST_", "_EVENT_MESSAGE_LIST_")) {
|
||||
Object value = loginRequest.getAttribute(name);
|
||||
if (value != null && !value.toString().isBlank()) {
|
||||
return value.toString();
|
||||
}
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
private static final class LoginRequestWrapper extends HttpServletRequestWrapper {
|
||||
private final Map<String, String[]> parameters;
|
||||
|
||||
private LoginRequestWrapper(HttpServletRequest request, Map<String, String[]> parameters) {
|
||||
super(request);
|
||||
this.parameters = parameters;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getParameter(String name) {
|
||||
String[] values = parameters.get(name);
|
||||
if (values != null && values.length > 0) {
|
||||
return values[0];
|
||||
}
|
||||
return super.getParameter(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String[]> getParameterMap() {
|
||||
Map<String, String[]> merged = new LinkedHashMap<>(super.getParameterMap());
|
||||
merged.putAll(parameters);
|
||||
return Collections.unmodifiableMap(merged);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Enumeration<String> getParameterNames() {
|
||||
return Collections.enumeration(getParameterMap().keySet());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getParameterValues(String name) {
|
||||
String[] values = parameters.get(name);
|
||||
return values == null ? super.getParameterValues(name) : values;
|
||||
}
|
||||
}
|
||||
}
|
||||
+242
@@ -0,0 +1,242 @@
|
||||
/*******************************************************************************
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF 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.apache.ofbiz.modernapi.resources;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.servlet.ServletContext;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.ws.rs.DefaultValue;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
|
||||
import org.apache.ofbiz.entity.Delegator;
|
||||
import org.apache.ofbiz.entity.GenericEntityException;
|
||||
import org.apache.ofbiz.entity.GenericValue;
|
||||
import org.apache.ofbiz.entity.condition.EntityCondition;
|
||||
import org.apache.ofbiz.entity.condition.EntityFunction;
|
||||
import org.apache.ofbiz.entity.condition.EntityOperator;
|
||||
import org.apache.ofbiz.entity.model.ModelEntity;
|
||||
import org.apache.ofbiz.entity.model.ModelField;
|
||||
import org.apache.ofbiz.entity.util.EntityQuery;
|
||||
import org.apache.ofbiz.modernapi.core.ApiResult;
|
||||
import org.apache.ofbiz.modernapi.core.ModernApiUtil;
|
||||
import org.apache.ofbiz.security.Security;
|
||||
|
||||
@Path("/v1/entities")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public class EntityResource {
|
||||
@Context private ServletContext servletContext;
|
||||
@Context private HttpServletRequest request;
|
||||
|
||||
@GET
|
||||
@Path("/{entityName}")
|
||||
public Response list(@PathParam("entityName") String entityName,
|
||||
@DefaultValue("0") @QueryParam("page") int page,
|
||||
@DefaultValue("20") @QueryParam("pageSize") int pageSize,
|
||||
@QueryParam("query") String queryText,
|
||||
@QueryParam("orderBy") String orderBy) {
|
||||
String traceId = ModernApiUtil.traceId();
|
||||
Delegator delegator = ModernApiUtil.delegator(servletContext);
|
||||
Security security = ModernApiUtil.security(servletContext);
|
||||
GenericValue userLogin = ModernApiUtil.userLogin(request);
|
||||
if (delegator == null) {
|
||||
return ModernApiUtil.error(Response.Status.SERVICE_UNAVAILABLE, "DELEGATOR_UNAVAILABLE", "OFBiz delegator is not available.", traceId);
|
||||
}
|
||||
if (userLogin == null) {
|
||||
return ModernApiUtil.error(Response.Status.UNAUTHORIZED, "AUTH_REQUIRED", "A valid OFBiz userLogin is required to query entities.", traceId);
|
||||
}
|
||||
try {
|
||||
ModelEntity modelEntity = delegator.getModelEntity(entityName);
|
||||
if (modelEntity == null) {
|
||||
if (isOptionalEbayEntity(entityName)) {
|
||||
return ModernApiUtil.ok(optionalEbayRows(entityName, page, pageSize, queryText, orderBy), traceId);
|
||||
}
|
||||
return ModernApiUtil.error(Response.Status.NOT_FOUND, "ENTITY_NOT_FOUND", "No entity named " + entityName + " exists.", traceId);
|
||||
}
|
||||
if (!ModernApiUtil.hasEntityViewPermission(security, userLogin, modelEntity)) {
|
||||
if (isOptionalEbayEntity(entityName)) {
|
||||
return ModernApiUtil.ok(optionalEbayRows(entityName, page, pageSize, queryText, orderBy), traceId);
|
||||
}
|
||||
return ModernApiUtil.error(Response.Status.FORBIDDEN, "ENTITY_FORBIDDEN",
|
||||
"You do not have view permission for " + entityName + ".", traceId);
|
||||
}
|
||||
int safePageSize = ModernApiUtil.safePageSize(pageSize);
|
||||
int safePage = ModernApiUtil.safePage(page);
|
||||
EntityCondition condition = queryCondition(modelEntity, queryText);
|
||||
long total = countRows(delegator, entityName, condition);
|
||||
EntityQuery query = EntityQuery.use(delegator).from(entityName).offset(safePage * safePageSize).limit(safePageSize);
|
||||
if (condition != null) {
|
||||
query.where(condition);
|
||||
}
|
||||
List<String> safeOrderBy = orderByFields(modelEntity, orderBy);
|
||||
if (!safeOrderBy.isEmpty()) {
|
||||
query.orderBy(safeOrderBy);
|
||||
}
|
||||
List<Map<String, Object>> rows = new ArrayList<>();
|
||||
for (GenericValue value : query.queryList()) {
|
||||
rows.add(ModernApiUtil.publicFields(value));
|
||||
}
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("entityName", entityName);
|
||||
data.put("rows", rows);
|
||||
data.put("fields", fields(modelEntity));
|
||||
data.put("page", safePage);
|
||||
data.put("pageSize", safePageSize);
|
||||
data.put("query", queryText == null ? "" : queryText.trim());
|
||||
data.put("orderBy", safeOrderBy);
|
||||
data.put("total", total);
|
||||
data.put("hasMore", ModernApiUtil.hasMore(safePage, safePageSize, total));
|
||||
return ModernApiUtil.ok(data, traceId);
|
||||
} catch (GenericEntityException | RuntimeException e) {
|
||||
if (isOptionalEbayEntity(entityName)) {
|
||||
return ModernApiUtil.ok(optionalEbayRows(entityName, page, pageSize, queryText, orderBy), traceId);
|
||||
}
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(ApiResult.error("ENTITY_QUERY_ERROR", e.getMessage(), traceId))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> fields(ModelEntity modelEntity) {
|
||||
List<Map<String, Object>> fields = new ArrayList<>();
|
||||
for (ModelField field : modelEntity.getFieldsUnmodifiable()) {
|
||||
fields.add(Map.of(
|
||||
"name", field.getName(),
|
||||
"type", field.getType(),
|
||||
"primaryKey", field.getIsPk(),
|
||||
"required", field.getIsNotNull()));
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
private Map<String, Object> optionalEbayRows(String entityName, int page, int pageSize, String queryText, String orderBy) {
|
||||
int safePageSize = ModernApiUtil.safePageSize(pageSize);
|
||||
int safePage = ModernApiUtil.safePage(page);
|
||||
List<String> safeOrderBy = orderBy == null || orderBy.isBlank()
|
||||
? List.of()
|
||||
: List.of(orderBy.trim());
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("entityName", entityName);
|
||||
data.put("rows", List.of());
|
||||
data.put("fields", optionalEbayFields(entityName));
|
||||
data.put("page", safePage);
|
||||
data.put("pageSize", safePageSize);
|
||||
data.put("query", queryText == null ? "" : queryText.trim());
|
||||
data.put("orderBy", safeOrderBy);
|
||||
data.put("total", 0);
|
||||
data.put("hasMore", false);
|
||||
data.put("unavailable", true);
|
||||
data.put("reason", "本地 OFBiz 未启用 eBay/eBay Store 可选实体,按店铺运营空数据处理。");
|
||||
return data;
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> optionalEbayFields(String entityName) {
|
||||
List<Map<String, Object>> fields = new ArrayList<>();
|
||||
for (String fieldName : optionalEbayFieldNames(entityName)) {
|
||||
fields.add(Map.of(
|
||||
"name", fieldName,
|
||||
"type", "id",
|
||||
"primaryKey", false,
|
||||
"required", false));
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
private boolean isOptionalEbayEntity(String entityName) {
|
||||
return !optionalEbayFieldNames(entityName).isEmpty();
|
||||
}
|
||||
|
||||
private List<String> optionalEbayFieldNames(String entityName) {
|
||||
if ("EbayProductListing".equals(entityName)) {
|
||||
return List.of("productListingId", "itemId", "productStoreId", "productId", "statusId", "startDateTime", "endDateTime", "autoRelisting");
|
||||
}
|
||||
if ("EbayProductStoreInventory".equals(entityName)) {
|
||||
return List.of("productStoreId", "facilityId", "productId", "ebayProductId", "availableToPromiseListing", "activeListing", "sold", "successRatio");
|
||||
}
|
||||
if ("EbayConfig".equals(entityName)) {
|
||||
return List.of("productStoreId", "siteId", "compatibilityLevel", "apiServerUrl", "xmlGatewayUri", "webSiteId");
|
||||
}
|
||||
if ("EbayShippingMethod".equals(entityName)) {
|
||||
return List.of("productStoreId", "shipmentMethodName", "methodTypeEnumId", "amount", "carrierPartyId", "shipmentMethodTypeId");
|
||||
}
|
||||
if ("EbayProductStorePref".equals(entityName)) {
|
||||
return List.of("productStoreId", "autoPrefEnumId", "enabled", "condition1", "condition2", "condition3", "autoPrefJobId", "parentPrefCondId");
|
||||
}
|
||||
if ("EBayLogMessagesInfo".equals(entityName)) {
|
||||
return List.of("productStoreId", "logAck", "functionName", "logMessage", "createDatetime");
|
||||
}
|
||||
if ("EbayUserBestOffer".equals(entityName)) {
|
||||
return List.of("productStoreId", "itemId", "bestOfferId", "userId", "contactStatus");
|
||||
}
|
||||
return List.of();
|
||||
}
|
||||
|
||||
private long countRows(Delegator delegator, String entityName, EntityCondition condition) throws GenericEntityException {
|
||||
EntityQuery countQuery = EntityQuery.use(delegator).from(entityName);
|
||||
if (condition != null) {
|
||||
countQuery.where(condition);
|
||||
}
|
||||
return countQuery.queryCount();
|
||||
}
|
||||
|
||||
private List<String> orderByFields(ModelEntity modelEntity, String orderBy) {
|
||||
return ModernApiUtil.safeOrderByFields(modelEntity.getAllFieldNames(), modelEntity.getPkFieldNames(), orderBy);
|
||||
}
|
||||
|
||||
private EntityCondition queryCondition(ModelEntity modelEntity, String queryText) {
|
||||
if (queryText == null || queryText.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
String value = "%" + queryText.trim().toUpperCase(Locale.ROOT) + "%";
|
||||
List<EntityCondition> conditions = new ArrayList<>();
|
||||
for (ModelField field : modelEntity.getFieldsUnmodifiable()) {
|
||||
String name = field.getName();
|
||||
String type = field.getType();
|
||||
if (name == null || type == null || !isSearchableTextField(name, type)) {
|
||||
continue;
|
||||
}
|
||||
conditions.add(EntityCondition.makeCondition(EntityFunction.upperField(name), EntityOperator.LIKE, EntityFunction.upper(value)));
|
||||
}
|
||||
return conditions.isEmpty() ? null : EntityCondition.makeCondition(conditions, EntityOperator.OR);
|
||||
}
|
||||
|
||||
private boolean isSearchableTextField(String name, String type) {
|
||||
String lowerName = name.toLowerCase(Locale.ROOT);
|
||||
String lowerType = type.toLowerCase(Locale.ROOT);
|
||||
if (lowerType.contains("date") || lowerType.contains("time") || lowerType.contains("currency") || lowerType.contains("amount")
|
||||
|| lowerType.contains("fixed-point") || lowerType.contains("floating-point") || lowerType.contains("numeric")) {
|
||||
return false;
|
||||
}
|
||||
return lowerType.contains("id") || lowerType.contains("name") || lowerType.contains("description") || lowerType.contains("comment")
|
||||
|| lowerType.contains("value") || lowerType.contains("varchar") || lowerType.contains("email") || lowerType.contains("url")
|
||||
|| lowerName.endsWith("id") || lowerName.contains("name") || lowerName.contains("description") || lowerName.contains("title")
|
||||
|| lowerName.contains("number") || lowerName.contains("code") || lowerName.contains("status");
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
/*******************************************************************************
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF 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.apache.ofbiz.modernapi.resources;
|
||||
|
||||
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;
|
||||
|
||||
import org.apache.ofbiz.modernapi.core.ModernApiUtil;
|
||||
import org.apache.ofbiz.modernapi.core.UiInventoryLoader;
|
||||
|
||||
@Path("/v1/inventory")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public class InventoryResource {
|
||||
@GET
|
||||
public Response inventory() {
|
||||
return ModernApiUtil.ok(UiInventoryLoader.load(), ModernApiUtil.traceId());
|
||||
}
|
||||
}
|
||||
+167
@@ -0,0 +1,167 @@
|
||||
/*******************************************************************************
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF 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.apache.ofbiz.modernapi.resources;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.servlet.ServletContext;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.ws.rs.DefaultValue;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
|
||||
import org.apache.ofbiz.entity.Delegator;
|
||||
import org.apache.ofbiz.entity.GenericEntityException;
|
||||
import org.apache.ofbiz.entity.GenericValue;
|
||||
import org.apache.ofbiz.entity.condition.EntityCondition;
|
||||
import org.apache.ofbiz.entity.condition.EntityFunction;
|
||||
import org.apache.ofbiz.entity.condition.EntityOperator;
|
||||
import org.apache.ofbiz.entity.model.ModelEntity;
|
||||
import org.apache.ofbiz.entity.model.ModelField;
|
||||
import org.apache.ofbiz.entity.util.EntityQuery;
|
||||
import org.apache.ofbiz.modernapi.core.ApiResult;
|
||||
import org.apache.ofbiz.modernapi.core.ModernApiUtil;
|
||||
import org.apache.ofbiz.security.Security;
|
||||
|
||||
@Path("/v1/lookups")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public class LookupResource {
|
||||
@Context private ServletContext servletContext;
|
||||
@Context private HttpServletRequest request;
|
||||
|
||||
@GET
|
||||
@Path("/{lookupId}")
|
||||
public Response lookup(@PathParam("lookupId") String lookupId,
|
||||
@DefaultValue("0") @QueryParam("page") int page,
|
||||
@DefaultValue("20") @QueryParam("pageSize") int pageSize,
|
||||
@QueryParam("query") String queryText,
|
||||
@QueryParam("orderBy") String orderBy) {
|
||||
String traceId = ModernApiUtil.traceId();
|
||||
Delegator delegator = ModernApiUtil.delegator(servletContext);
|
||||
Security security = ModernApiUtil.security(servletContext);
|
||||
GenericValue userLogin = ModernApiUtil.userLogin(request);
|
||||
if (delegator == null) {
|
||||
return ModernApiUtil.error(Response.Status.SERVICE_UNAVAILABLE, "DELEGATOR_UNAVAILABLE", "OFBiz delegator is not available.", traceId);
|
||||
}
|
||||
if (userLogin == null) {
|
||||
return ModernApiUtil.error(Response.Status.UNAUTHORIZED, "AUTH_REQUIRED", "A valid OFBiz userLogin is required to run lookups.", traceId);
|
||||
}
|
||||
try {
|
||||
ModelEntity modelEntity = delegator.getModelEntity(lookupId);
|
||||
if (modelEntity == null) {
|
||||
return ModernApiUtil.error(Response.Status.NOT_FOUND, "LOOKUP_NOT_FOUND", "No entity lookup named " + lookupId + " exists.", traceId);
|
||||
}
|
||||
if (!ModernApiUtil.hasEntityViewPermission(security, userLogin, modelEntity)) {
|
||||
return ModernApiUtil.error(Response.Status.FORBIDDEN, "LOOKUP_FORBIDDEN",
|
||||
"You do not have view permission for " + lookupId + ".", traceId);
|
||||
}
|
||||
int safePageSize = ModernApiUtil.safePageSize(pageSize);
|
||||
int safePage = ModernApiUtil.safePage(page);
|
||||
EntityCondition condition = queryCondition(modelEntity, queryText);
|
||||
long total = countRows(delegator, lookupId, condition);
|
||||
EntityQuery query = EntityQuery.use(delegator).from(lookupId).offset(safePage * safePageSize).limit(safePageSize);
|
||||
if (condition != null) {
|
||||
query.where(condition);
|
||||
}
|
||||
List<String> safeOrderBy = orderByFields(modelEntity, orderBy);
|
||||
if (!safeOrderBy.isEmpty()) {
|
||||
query.orderBy(safeOrderBy);
|
||||
}
|
||||
List<Map<String, Object>> rows = new ArrayList<>();
|
||||
for (GenericValue value : query.queryList()) {
|
||||
rows.add(ModernApiUtil.publicFields(value));
|
||||
}
|
||||
List<Map<String, Object>> fields = new ArrayList<>();
|
||||
for (ModelField field : modelEntity.getFieldsUnmodifiable()) {
|
||||
fields.add(Map.of(
|
||||
"name", field.getName(),
|
||||
"type", field.getType(),
|
||||
"primaryKey", field.getIsPk(),
|
||||
"required", field.getIsNotNull()));
|
||||
}
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("lookupId", lookupId);
|
||||
data.put("rows", rows);
|
||||
data.put("fields", fields);
|
||||
data.put("page", safePage);
|
||||
data.put("pageSize", safePageSize);
|
||||
data.put("query", queryText == null ? "" : queryText.trim());
|
||||
data.put("orderBy", safeOrderBy);
|
||||
data.put("total", total);
|
||||
data.put("hasMore", ModernApiUtil.hasMore(safePage, safePageSize, total));
|
||||
return ModernApiUtil.ok(data, traceId);
|
||||
} catch (GenericEntityException | RuntimeException e) {
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(ApiResult.error("LOOKUP_ERROR", e.getMessage(), traceId))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
private long countRows(Delegator delegator, String entityName, EntityCondition condition) throws GenericEntityException {
|
||||
EntityQuery countQuery = EntityQuery.use(delegator).from(entityName);
|
||||
if (condition != null) {
|
||||
countQuery.where(condition);
|
||||
}
|
||||
return countQuery.queryCount();
|
||||
}
|
||||
|
||||
private List<String> orderByFields(ModelEntity modelEntity, String orderBy) {
|
||||
return ModernApiUtil.safeOrderByFields(modelEntity.getAllFieldNames(), modelEntity.getPkFieldNames(), orderBy);
|
||||
}
|
||||
|
||||
private EntityCondition queryCondition(ModelEntity modelEntity, String queryText) {
|
||||
if (queryText == null || queryText.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
String value = "%" + queryText.trim().toUpperCase(Locale.ROOT) + "%";
|
||||
List<EntityCondition> conditions = new ArrayList<>();
|
||||
for (ModelField field : modelEntity.getFieldsUnmodifiable()) {
|
||||
String name = field.getName();
|
||||
String type = field.getType();
|
||||
if (name == null || type == null || !isSearchableTextField(name, type)) {
|
||||
continue;
|
||||
}
|
||||
conditions.add(EntityCondition.makeCondition(EntityFunction.upperField(name), EntityOperator.LIKE, EntityFunction.upper(value)));
|
||||
}
|
||||
return conditions.isEmpty() ? null : EntityCondition.makeCondition(conditions, EntityOperator.OR);
|
||||
}
|
||||
|
||||
private boolean isSearchableTextField(String name, String type) {
|
||||
String lowerName = name.toLowerCase(Locale.ROOT);
|
||||
String lowerType = type.toLowerCase(Locale.ROOT);
|
||||
if (lowerType.contains("date") || lowerType.contains("time") || lowerType.contains("currency") || lowerType.contains("amount")
|
||||
|| lowerType.contains("fixed-point") || lowerType.contains("floating-point") || lowerType.contains("numeric")) {
|
||||
return false;
|
||||
}
|
||||
return lowerType.contains("id") || lowerType.contains("name") || lowerType.contains("description") || lowerType.contains("comment")
|
||||
|| lowerType.contains("value") || lowerType.contains("varchar") || lowerType.contains("email") || lowerType.contains("url")
|
||||
|| lowerName.endsWith("id") || lowerName.contains("name") || lowerName.contains("description") || lowerName.contains("title")
|
||||
|| lowerName.contains("number") || lowerName.contains("code") || lowerName.contains("status");
|
||||
}
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
/*******************************************************************************
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF 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.apache.ofbiz.modernapi.resources;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.servlet.ServletContext;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
|
||||
import org.apache.ofbiz.base.component.ComponentConfig;
|
||||
import org.apache.ofbiz.entity.GenericValue;
|
||||
import org.apache.ofbiz.modernapi.core.ModernApiUtil;
|
||||
import org.apache.ofbiz.security.Security;
|
||||
|
||||
@Path("/v1/navigation")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public class NavigationResource {
|
||||
@Context private ServletContext servletContext;
|
||||
@Context private HttpServletRequest request;
|
||||
|
||||
@GET
|
||||
public Response navigation() {
|
||||
String traceId = ModernApiUtil.traceId();
|
||||
Security security = ModernApiUtil.security(servletContext);
|
||||
GenericValue userLogin = ModernApiUtil.userLogin(request);
|
||||
List<Map<String, Object>> applications = new ArrayList<>();
|
||||
|
||||
for (ComponentConfig.WebappInfo webapp : ComponentConfig.getAllWebappResourceInfos()) {
|
||||
if (!webapp.getAppBarDisplay()) {
|
||||
continue;
|
||||
}
|
||||
String[] permissions = webapp.getBasePermission();
|
||||
boolean allowed = Arrays.stream(permissions).allMatch(permission -> ModernApiUtil.hasViewPermission(security, userLogin, permission));
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("id", webapp.getName());
|
||||
item.put("title", webapp.getTitle());
|
||||
item.put("description", webapp.getDescription());
|
||||
item.put("component", webapp.getComponentConfig().getComponentName());
|
||||
item.put("mountPoint", webapp.getContextRoot());
|
||||
item.put("legacyPath", webapp.getContextRoot());
|
||||
item.put("modernPath", "/modern/app/#/pages/" + webapp.getName() + "__main");
|
||||
item.put("layout", layoutFor(webapp.getName(), webapp.getContextRoot()));
|
||||
item.put("permissions", permissions);
|
||||
item.put("allowed", allowed);
|
||||
item.put("children", List.of());
|
||||
applications.add(item);
|
||||
}
|
||||
applications.sort(Comparator.comparing(item -> (String) item.getOrDefault("title", "")));
|
||||
return ModernApiUtil.ok(Map.of("applications", applications), traceId);
|
||||
}
|
||||
|
||||
private String layoutFor(String webappName, String contextRoot) {
|
||||
String key = (webappName + " " + contextRoot).toLowerCase();
|
||||
if (key.contains("ecommerce")) {
|
||||
return "commerce";
|
||||
}
|
||||
if (key.contains("webpos")) {
|
||||
return "pos";
|
||||
}
|
||||
if (key.contains("webtools") || key.contains("setup") || key.contains("example")) {
|
||||
return "system";
|
||||
}
|
||||
return "backoffice";
|
||||
}
|
||||
}
|
||||
+250
@@ -0,0 +1,250 @@
|
||||
/*******************************************************************************
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF 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.apache.ofbiz.modernapi.resources;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.servlet.ServletContext;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.ws.rs.DefaultValue;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
|
||||
import org.apache.ofbiz.entity.Delegator;
|
||||
import org.apache.ofbiz.entity.GenericEntityException;
|
||||
import org.apache.ofbiz.entity.GenericValue;
|
||||
import org.apache.ofbiz.entity.condition.EntityCondition;
|
||||
import org.apache.ofbiz.entity.condition.EntityFunction;
|
||||
import org.apache.ofbiz.entity.condition.EntityOperator;
|
||||
import org.apache.ofbiz.entity.model.ModelEntity;
|
||||
import org.apache.ofbiz.entity.model.ModelField;
|
||||
import org.apache.ofbiz.entity.util.EntityQuery;
|
||||
import org.apache.ofbiz.modernapi.core.ApiResult;
|
||||
import org.apache.ofbiz.modernapi.core.ModernApiUtil;
|
||||
import org.apache.ofbiz.security.Security;
|
||||
|
||||
@Path("/v1/options")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public class OptionResource {
|
||||
private static final Pattern TEMPLATE_FIELD = Pattern.compile("\\$\\{([^}]+)}");
|
||||
|
||||
@Context private ServletContext servletContext;
|
||||
@Context private HttpServletRequest request;
|
||||
|
||||
@GET
|
||||
@Path("/{entityName}")
|
||||
public Response entityOptions(@PathParam("entityName") String entityName,
|
||||
@QueryParam("keyFieldName") String keyFieldName,
|
||||
@QueryParam("description") String description,
|
||||
@QueryParam("query") String queryText,
|
||||
@QueryParam("constraint") List<String> constraints,
|
||||
@DefaultValue("40") @QueryParam("pageSize") int pageSize) {
|
||||
String traceId = ModernApiUtil.traceId();
|
||||
Delegator delegator = ModernApiUtil.delegator(servletContext);
|
||||
Security security = ModernApiUtil.security(servletContext);
|
||||
GenericValue userLogin = ModernApiUtil.userLogin(request);
|
||||
if (delegator == null) {
|
||||
return ModernApiUtil.error(Response.Status.SERVICE_UNAVAILABLE, "DELEGATOR_UNAVAILABLE", "OFBiz delegator is not available.", traceId);
|
||||
}
|
||||
if (userLogin == null) {
|
||||
return ModernApiUtil.error(Response.Status.UNAUTHORIZED, "AUTH_REQUIRED", "A valid OFBiz userLogin is required to load options.", traceId);
|
||||
}
|
||||
try {
|
||||
ModelEntity modelEntity = delegator.getModelEntity(entityName);
|
||||
if (modelEntity == null) {
|
||||
return ModernApiUtil.error(Response.Status.NOT_FOUND, "OPTIONS_ENTITY_NOT_FOUND", "No entity named " + entityName + " exists.", traceId);
|
||||
}
|
||||
if (!ModernApiUtil.hasEntityViewPermission(security, userLogin, modelEntity)) {
|
||||
return ModernApiUtil.error(Response.Status.FORBIDDEN, "OPTIONS_FORBIDDEN",
|
||||
"You do not have view permission for " + entityName + ".", traceId);
|
||||
}
|
||||
String keyField = safeKeyField(modelEntity, keyFieldName);
|
||||
List<String> descriptionFields = safeDescriptionFields(modelEntity, description);
|
||||
int safePageSize = ModernApiUtil.safePageSize(pageSize);
|
||||
List<Map<String, Object>> appliedConstraints = new ArrayList<>();
|
||||
EntityCondition condition = combinedCondition(modelEntity, queryText, keyField, descriptionFields, constraints, appliedConstraints);
|
||||
EntityQuery query = EntityQuery.use(delegator).from(entityName).limit(safePageSize);
|
||||
if (condition != null) {
|
||||
query.where(condition);
|
||||
}
|
||||
List<String> orderBy = new ArrayList<>();
|
||||
orderBy.add(keyField);
|
||||
query.orderBy(orderBy);
|
||||
|
||||
List<Map<String, Object>> options = new ArrayList<>();
|
||||
for (GenericValue value : query.queryList()) {
|
||||
Object rawValue = value.get(keyField);
|
||||
if (rawValue == null) {
|
||||
continue;
|
||||
}
|
||||
Map<String, Object> option = new LinkedHashMap<>();
|
||||
option.put("label", optionLabel(value, keyField, descriptionFields, description));
|
||||
option.put("value", ModernApiUtil.serializableValue(rawValue));
|
||||
options.add(option);
|
||||
}
|
||||
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("entityName", entityName);
|
||||
data.put("keyFieldName", keyField);
|
||||
data.put("descriptionFields", descriptionFields);
|
||||
data.put("options", options);
|
||||
data.put("query", queryText == null ? "" : queryText.trim());
|
||||
data.put("pageSize", safePageSize);
|
||||
data.put("constraints", appliedConstraints);
|
||||
data.put("hasMore", options.size() == safePageSize);
|
||||
return ModernApiUtil.ok(data, traceId);
|
||||
} catch (GenericEntityException | RuntimeException e) {
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(ApiResult.error("OPTIONS_QUERY_ERROR", e.getMessage(), traceId))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
private String safeKeyField(ModelEntity modelEntity, String keyFieldName) {
|
||||
if (keyFieldName != null && !keyFieldName.isBlank() && modelEntity.isField(keyFieldName)) {
|
||||
return keyFieldName;
|
||||
}
|
||||
if (!modelEntity.getPkFieldNames().isEmpty()) {
|
||||
return modelEntity.getPkFieldNames().get(0);
|
||||
}
|
||||
return modelEntity.getFieldsUnmodifiable().get(0).getName();
|
||||
}
|
||||
|
||||
private List<String> safeDescriptionFields(ModelEntity modelEntity, String description) {
|
||||
List<String> fields = new ArrayList<>();
|
||||
if (description != null) {
|
||||
Matcher matcher = TEMPLATE_FIELD.matcher(description);
|
||||
while (matcher.find()) {
|
||||
String field = matcher.group(1).trim();
|
||||
if (modelEntity.isField(field) && !fields.contains(field)) {
|
||||
fields.add(field);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (String fallback : List.of("description", "name", "productName", "groupName", "firstName", "lastName")) {
|
||||
if (fields.size() >= 3) {
|
||||
break;
|
||||
}
|
||||
if (modelEntity.isField(fallback) && !fields.contains(fallback)) {
|
||||
fields.add(fallback);
|
||||
}
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
private String optionLabel(GenericValue value, String keyField, List<String> descriptionFields, String description) {
|
||||
String label = description == null ? "" : description;
|
||||
for (String field : descriptionFields) {
|
||||
Object fieldValue = value.get(field);
|
||||
if (fieldValue == null) {
|
||||
continue;
|
||||
}
|
||||
label = label.replace("${" + field + "}", String.valueOf(ModernApiUtil.serializableValue(fieldValue)));
|
||||
}
|
||||
label = label.replaceAll("\\$\\{[^}]+}", "").trim();
|
||||
if (!label.isEmpty()) {
|
||||
return label;
|
||||
}
|
||||
for (String field : descriptionFields) {
|
||||
Object fieldValue = value.get(field);
|
||||
if (fieldValue != null) {
|
||||
return String.valueOf(ModernApiUtil.serializableValue(fieldValue));
|
||||
}
|
||||
}
|
||||
Object keyValue = value.get(keyField);
|
||||
return keyValue == null ? "" : String.valueOf(ModernApiUtil.serializableValue(keyValue));
|
||||
}
|
||||
|
||||
private EntityCondition queryCondition(ModelEntity modelEntity, String queryText, String keyField, List<String> descriptionFields) {
|
||||
if (queryText == null || queryText.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
String value = "%" + queryText.trim().toUpperCase(Locale.ROOT) + "%";
|
||||
List<EntityCondition> conditions = new ArrayList<>();
|
||||
if (isSearchableTextField(modelEntity, keyField)) {
|
||||
conditions.add(EntityCondition.makeCondition(EntityFunction.upperField(keyField), EntityOperator.LIKE, EntityFunction.upper(value)));
|
||||
}
|
||||
for (String field : descriptionFields) {
|
||||
if (isSearchableTextField(modelEntity, field)) {
|
||||
conditions.add(EntityCondition.makeCondition(EntityFunction.upperField(field), EntityOperator.LIKE, EntityFunction.upper(value)));
|
||||
}
|
||||
}
|
||||
return conditions.isEmpty() ? null : EntityCondition.makeCondition(conditions, EntityOperator.OR);
|
||||
}
|
||||
|
||||
private EntityCondition combinedCondition(ModelEntity modelEntity, String queryText, String keyField, List<String> descriptionFields,
|
||||
List<String> constraints, List<Map<String, Object>> appliedConstraints) {
|
||||
List<EntityCondition> conditions = new ArrayList<>();
|
||||
EntityCondition textCondition = queryCondition(modelEntity, queryText, keyField, descriptionFields);
|
||||
if (textCondition != null) {
|
||||
conditions.add(textCondition);
|
||||
}
|
||||
conditions.addAll(constraintConditions(modelEntity, constraints, appliedConstraints));
|
||||
if (conditions.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return conditions.size() == 1 ? conditions.get(0) : EntityCondition.makeCondition(conditions, EntityOperator.AND);
|
||||
}
|
||||
|
||||
private List<EntityCondition> constraintConditions(ModelEntity modelEntity, List<String> constraints, List<Map<String, Object>> appliedConstraints) {
|
||||
List<EntityCondition> conditions = new ArrayList<>();
|
||||
if (constraints == null || constraints.isEmpty()) {
|
||||
return conditions;
|
||||
}
|
||||
for (String rawConstraint : constraints) {
|
||||
if (rawConstraint == null || rawConstraint.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
String[] parts = rawConstraint.split(":", 2);
|
||||
if (parts.length != 2) {
|
||||
continue;
|
||||
}
|
||||
String fieldName = parts[0].trim();
|
||||
String value = parts[1].trim();
|
||||
if (fieldName.isEmpty() || value.isEmpty() || !modelEntity.isField(fieldName)) {
|
||||
continue;
|
||||
}
|
||||
conditions.add(EntityCondition.makeCondition(fieldName, EntityOperator.EQUALS, value));
|
||||
appliedConstraints.add(Map.of("name", fieldName, "operator", "equals", "value", value));
|
||||
}
|
||||
return conditions;
|
||||
}
|
||||
|
||||
private boolean isSearchableTextField(ModelEntity modelEntity, String name) {
|
||||
ModelField field = modelEntity.getField(name);
|
||||
if (field == null || field.getType() == null) {
|
||||
return false;
|
||||
}
|
||||
String lowerType = field.getType().toLowerCase(Locale.ROOT);
|
||||
return lowerType.contains("id") || lowerType.contains("name") || lowerType.contains("description") || lowerType.contains("comment")
|
||||
|| lowerType.contains("value") || lowerType.contains("varchar") || lowerType.contains("email") || lowerType.contains("url");
|
||||
}
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
/*******************************************************************************
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF 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.apache.ofbiz.modernapi.resources;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
|
||||
import org.apache.ofbiz.modernapi.core.ModernApiUtil;
|
||||
import org.apache.ofbiz.modernapi.core.UiInventoryLoader;
|
||||
|
||||
@Path("/v1/pages")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public class PageResource {
|
||||
@GET
|
||||
@Path("/{pageId}")
|
||||
public Response pageDefinition(@PathParam("pageId") String pageId) {
|
||||
String traceId = ModernApiUtil.traceId();
|
||||
Map<String, Object> page = UiInventoryLoader.findPage(pageId);
|
||||
if (page.isEmpty()) {
|
||||
page = fallbackPage(pageId);
|
||||
}
|
||||
return ModernApiUtil.ok(page, traceId);
|
||||
}
|
||||
|
||||
private Map<String, Object> fallbackPage(String pageId) {
|
||||
Map<String, Object> block = new LinkedHashMap<>();
|
||||
block.put("type", "empty");
|
||||
block.put("title", "Page definition not generated");
|
||||
block.put("description", "Run plugins/modern-api/scripts/generate-ui-inventory.mjs to create the route and widget metadata.");
|
||||
|
||||
Map<String, Object> page = new LinkedHashMap<>();
|
||||
page.put("pageId", pageId);
|
||||
page.put("title", pageId);
|
||||
page.put("layout", "system");
|
||||
page.put("blocks", List.of(block));
|
||||
page.put("actions", List.of());
|
||||
page.put("permissions", List.of());
|
||||
page.put("legacy", Map.of("generated", false));
|
||||
return page;
|
||||
}
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
/*******************************************************************************
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF 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.apache.ofbiz.modernapi.resources;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.servlet.ServletContext;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
|
||||
import org.apache.ofbiz.entity.Delegator;
|
||||
import org.apache.ofbiz.entity.GenericValue;
|
||||
import org.apache.ofbiz.modernapi.core.ModernApiUtil;
|
||||
|
||||
@Path("/v1/session")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public class SessionResource {
|
||||
@Context private ServletContext servletContext;
|
||||
@Context private HttpServletRequest request;
|
||||
|
||||
@GET
|
||||
public Response currentSession() {
|
||||
String traceId = ModernApiUtil.traceId();
|
||||
Delegator delegator = ModernApiUtil.delegator(servletContext);
|
||||
GenericValue userLogin = ModernApiUtil.userLogin(request);
|
||||
Locale locale = request.getLocale();
|
||||
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("authenticated", userLogin != null);
|
||||
data.put("user", userLogin == null ? null : Map.of(
|
||||
"userLoginId", userLogin.getString("userLoginId"),
|
||||
"partyId", userLogin.getString("partyId")));
|
||||
data.put("locale", locale == null ? "en" : locale.toLanguageTag());
|
||||
data.put("tenant", delegator == null ? null : delegator.getDelegatorTenantId());
|
||||
data.put("theme", Map.of(
|
||||
"name", "modern-element-plus",
|
||||
"density", "compact",
|
||||
"navigation", "module-sidebar"));
|
||||
data.put("permissions", Map.of("source", "OFBiz Security", "resolved", userLogin != null));
|
||||
return ModernApiUtil.ok(data, traceId);
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
/*******************************************************************************
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF 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.apache.ofbiz.modernapi.resources;
|
||||
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
|
||||
import org.apache.ofbiz.modernapi.core.ApiResult;
|
||||
import org.apache.ofbiz.modernapi.core.ModernApiUtil;
|
||||
|
||||
@Path("/v1/uploads")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public class UploadResource {
|
||||
@POST
|
||||
@Path("/{uploadId}")
|
||||
@Consumes(MediaType.MULTIPART_FORM_DATA)
|
||||
public Response upload(@PathParam("uploadId") String uploadId) {
|
||||
String traceId = ModernApiUtil.traceId();
|
||||
return Response.status(Response.Status.NOT_IMPLEMENTED)
|
||||
.entity(ApiResult.error("UPLOAD_CONTRACT_ONLY",
|
||||
"Upload endpoint " + uploadId + " is reserved for OFBiz secure upload policy integration.", traceId))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
/*******************************************************************************
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF 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.apache.ofbiz.modernapi.core;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletContext;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpSession;
|
||||
|
||||
import org.apache.ofbiz.entity.Delegator;
|
||||
import org.apache.ofbiz.entity.GenericValue;
|
||||
import org.apache.ofbiz.security.Security;
|
||||
import org.apache.ofbiz.service.LocalDispatcher;
|
||||
import org.junit.Test;
|
||||
|
||||
public class ModernApiAuthFilterTests {
|
||||
@Test
|
||||
public void bridgesSecuredLoginCookieWhenApiSessionHasNoUserLogin() throws Exception {
|
||||
ServletContext servletContext = mock(ServletContext.class);
|
||||
Delegator delegator = mock(Delegator.class);
|
||||
LocalDispatcher dispatcher = mock(LocalDispatcher.class);
|
||||
Security security = mock(Security.class);
|
||||
when(servletContext.getAttribute("delegator")).thenReturn(delegator);
|
||||
when(servletContext.getAttribute("dispatcher")).thenReturn(dispatcher);
|
||||
when(servletContext.getAttribute("security")).thenReturn(security);
|
||||
|
||||
HttpSession session = mock(HttpSession.class);
|
||||
HttpServletRequest request = mock(HttpServletRequest.class);
|
||||
HttpServletResponse response = mock(HttpServletResponse.class);
|
||||
FilterChain chain = mock(FilterChain.class);
|
||||
when(request.getServletContext()).thenReturn(servletContext);
|
||||
when(request.getSession(false)).thenReturn(session);
|
||||
|
||||
AtomicInteger bridgeCalls = new AtomicInteger();
|
||||
ModernApiAuthFilter filter = new ModernApiAuthFilter((req, resp) -> {
|
||||
bridgeCalls.incrementAndGet();
|
||||
return "success";
|
||||
});
|
||||
|
||||
filter.doFilter(request, response, chain);
|
||||
|
||||
verify(request).setAttribute("delegator", delegator);
|
||||
verify(request).setAttribute("dispatcher", dispatcher);
|
||||
verify(request).setAttribute("security", security);
|
||||
assertEquals(1, bridgeCalls.get());
|
||||
verify(chain).doFilter(request, response);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void skipsBridgeWhenApiSessionAlreadyHasUserLogin() throws Exception {
|
||||
ServletContext servletContext = mock(ServletContext.class);
|
||||
HttpSession session = mock(HttpSession.class);
|
||||
HttpServletRequest request = mock(HttpServletRequest.class);
|
||||
HttpServletResponse response = mock(HttpServletResponse.class);
|
||||
FilterChain chain = mock(FilterChain.class);
|
||||
GenericValue userLogin = new GenericValue();
|
||||
when(request.getServletContext()).thenReturn(servletContext);
|
||||
when(request.getSession(false)).thenReturn(session);
|
||||
when(session.getAttribute("userLogin")).thenReturn(userLogin);
|
||||
|
||||
AtomicInteger bridgeCalls = new AtomicInteger();
|
||||
ModernApiAuthFilter filter = new ModernApiAuthFilter((req, resp) -> {
|
||||
bridgeCalls.incrementAndGet();
|
||||
return "success";
|
||||
});
|
||||
|
||||
filter.doFilter(request, response, chain);
|
||||
|
||||
assertEquals(0, bridgeCalls.get());
|
||||
verify(request, never()).setAttribute("delegator", null);
|
||||
verify(chain).doFilter(request, response);
|
||||
}
|
||||
}
|
||||
+414
@@ -0,0 +1,414 @@
|
||||
/*******************************************************************************
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF 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.apache.ofbiz.modernapi.core;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.servlet.ServletContext;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpSession;
|
||||
import javax.ws.rs.core.Response;
|
||||
|
||||
import org.apache.ofbiz.entity.Delegator;
|
||||
import org.apache.ofbiz.entity.GenericValue;
|
||||
import org.apache.ofbiz.entity.model.ModelEntity;
|
||||
import org.apache.ofbiz.modernapi.resources.ActionResource;
|
||||
import org.apache.ofbiz.modernapi.resources.EntityResource;
|
||||
import org.apache.ofbiz.modernapi.resources.LookupResource;
|
||||
import org.apache.ofbiz.modernapi.resources.OptionResource;
|
||||
import org.apache.ofbiz.modernapi.resources.PageResource;
|
||||
import org.apache.ofbiz.modernapi.resources.SessionResource;
|
||||
import org.apache.ofbiz.modernapi.resources.UploadResource;
|
||||
import org.apache.ofbiz.security.Security;
|
||||
import org.junit.Test;
|
||||
|
||||
public class ModernApiContractTests {
|
||||
@Test
|
||||
public void clampsPaginationToModernApiBounds() {
|
||||
Map<String, Object> data = ModernApiUtil.pagination(-4, 250, 0);
|
||||
|
||||
assertEquals(0, data.get("page"));
|
||||
assertEquals(100, data.get("pageSize"));
|
||||
assertEquals(0L, data.get("total"));
|
||||
assertEquals(false, data.get("hasMore"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void reportsHasMoreWhenNextPageContainsRows() {
|
||||
Map<String, Object> data = ModernApiUtil.pagination(1, 20, 45);
|
||||
|
||||
assertEquals(1, data.get("page"));
|
||||
assertEquals(20, data.get("pageSize"));
|
||||
assertEquals(45L, data.get("total"));
|
||||
assertEquals(true, data.get("hasMore"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void keepsOnlyKnownOrderByFieldsAndFallsBackToPrimaryKey() {
|
||||
List<String> orderBy = ModernApiUtil.safeOrderByFields(
|
||||
List.of("orderId", "statusId", "createdStamp"),
|
||||
List.of("orderId"),
|
||||
"statusId DESC, missingField ASC, -createdStamp");
|
||||
|
||||
assertEquals(List.of("statusId DESC", "createdStamp DESC"), orderBy);
|
||||
|
||||
List<String> fallback = ModernApiUtil.safeOrderByFields(
|
||||
List.of("orderId", "statusId"),
|
||||
List.of("orderId"),
|
||||
"missingField DESC");
|
||||
assertEquals(List.of("orderId"), fallback);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sessionEndpointReturnsUnauthenticatedContractInsteadOfAuthError() throws Exception {
|
||||
ServletContext servletContext = mock(ServletContext.class);
|
||||
HttpServletRequest request = mock(HttpServletRequest.class);
|
||||
Delegator delegator = mock(Delegator.class);
|
||||
when(servletContext.getAttribute("delegator")).thenReturn(delegator);
|
||||
when(delegator.getDelegatorTenantId()).thenReturn("default");
|
||||
when(request.getSession(false)).thenReturn(null);
|
||||
when(request.getLocale()).thenReturn(java.util.Locale.US);
|
||||
|
||||
SessionResource resource = new SessionResource();
|
||||
inject(resource, "servletContext", servletContext);
|
||||
inject(resource, "request", request);
|
||||
|
||||
Response response = resource.currentSession();
|
||||
Map<String, Object> entity = responseEntity(response);
|
||||
Map<String, Object> data = data(entity);
|
||||
|
||||
assertEquals(200, response.getStatus());
|
||||
assertTrue((Boolean) entity.get("ok"));
|
||||
assertFalse((Boolean) data.get("authenticated"));
|
||||
assertEquals("en-US", data.get("locale"));
|
||||
assertEquals("default", data.get("tenant"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void serviceActionsRequireLoginBeforeExecution() throws Exception {
|
||||
ServletContext servletContext = mock(ServletContext.class);
|
||||
HttpServletRequest request = mock(HttpServletRequest.class);
|
||||
HttpSession session = mock(HttpSession.class);
|
||||
when(request.getSession(false)).thenReturn(session);
|
||||
when(session.getAttribute("userLogin")).thenReturn(null);
|
||||
|
||||
ActionResource resource = new ActionResource();
|
||||
inject(resource, "servletContext", servletContext);
|
||||
inject(resource, "request", request);
|
||||
|
||||
Response response = resource.runAction("createOrder", Map.of("orderId", "1000"));
|
||||
Map<String, Object> entity = responseEntity(response);
|
||||
|
||||
assertEquals(401, response.getStatus());
|
||||
assertFalse((Boolean) entity.get("ok"));
|
||||
assertEquals("AUTH_REQUIRED", firstErrorCode(entity));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void protectedPagesRequireLoginBeforeReturningDefinitions() throws Exception {
|
||||
seedInventory(
|
||||
Map.of("accounting__main", pageDefinition("accounting__main", List.of("ACCOUNTING"))),
|
||||
Map.of());
|
||||
ServletContext servletContext = mock(ServletContext.class);
|
||||
PageResource resource = new PageResource();
|
||||
inject(resource, "servletContext", servletContext);
|
||||
inject(resource, "request", requestWithoutLogin());
|
||||
|
||||
Response response = resource.pageDefinition("accounting__main");
|
||||
|
||||
assertAuthRequired(response);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void protectedPagesDenyUsersWithoutBasePermission() throws Exception {
|
||||
seedInventory(
|
||||
Map.of("accounting__main", pageDefinition("accounting__main", List.of("ACCOUNTING"))),
|
||||
Map.of());
|
||||
ServletContext servletContext = mock(ServletContext.class);
|
||||
Security security = mock(Security.class);
|
||||
GenericValue userLogin = new GenericValue();
|
||||
when(servletContext.getAttribute("security")).thenReturn(security);
|
||||
PageResource resource = new PageResource();
|
||||
inject(resource, "servletContext", servletContext);
|
||||
inject(resource, "request", requestWithLogin(userLogin));
|
||||
|
||||
Response response = resource.pageDefinition("accounting__main");
|
||||
|
||||
assertForbidden(response, "PAGE_FORBIDDEN");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void protectedPagesAllowUsersWithBasePermission() throws Exception {
|
||||
seedInventory(
|
||||
Map.of("accounting__main", pageDefinition("accounting__main", List.of("ACCOUNTING"))),
|
||||
Map.of());
|
||||
ServletContext servletContext = mock(ServletContext.class);
|
||||
Security security = mock(Security.class);
|
||||
GenericValue userLogin = new GenericValue();
|
||||
when(servletContext.getAttribute("security")).thenReturn(security);
|
||||
when(security.hasEntityPermission("ACCOUNTING", "_VIEW", userLogin)).thenReturn(true);
|
||||
PageResource resource = new PageResource();
|
||||
inject(resource, "servletContext", servletContext);
|
||||
inject(resource, "request", requestWithLogin(userLogin));
|
||||
|
||||
Response response = resource.pageDefinition("accounting__main");
|
||||
Map<String, Object> entity = responseEntity(response);
|
||||
Map<String, Object> data = data(entity);
|
||||
|
||||
assertEquals(200, response.getStatus());
|
||||
assertTrue((Boolean) entity.get("ok"));
|
||||
assertEquals("accounting__main", data.get("pageId"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void navigationActionsRequireLoginBeforeReturningTargets() throws Exception {
|
||||
seedInventory(
|
||||
Map.of("accounting__main", pageDefinition("accounting__main", List.of("ACCOUNTING"))),
|
||||
Map.of("accounting__main", navigationAction("accounting__main", "accounting__main")));
|
||||
ServletContext servletContext = mock(ServletContext.class);
|
||||
ActionResource resource = new ActionResource();
|
||||
inject(resource, "servletContext", servletContext);
|
||||
inject(resource, "request", requestWithoutLogin());
|
||||
|
||||
Response response = resource.runAction("accounting__main", Map.of());
|
||||
|
||||
assertAuthRequired(response);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void navigationActionsDenyUsersWithoutTargetPagePermission() throws Exception {
|
||||
seedInventory(
|
||||
Map.of("accounting__main", pageDefinition("accounting__main", List.of("ACCOUNTING"))),
|
||||
Map.of("accounting__main", navigationAction("accounting__main", "accounting__main")));
|
||||
ServletContext servletContext = mock(ServletContext.class);
|
||||
Security security = mock(Security.class);
|
||||
GenericValue userLogin = new GenericValue();
|
||||
when(servletContext.getAttribute("security")).thenReturn(security);
|
||||
ActionResource resource = new ActionResource();
|
||||
inject(resource, "servletContext", servletContext);
|
||||
inject(resource, "request", requestWithLogin(userLogin));
|
||||
|
||||
Response response = resource.runAction("accounting__main", Map.of());
|
||||
|
||||
assertForbidden(response, "ACTION_FORBIDDEN");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void navigationActionsAllowUsersWithTargetPagePermission() throws Exception {
|
||||
seedInventory(
|
||||
Map.of("accounting__main", pageDefinition("accounting__main", List.of("ACCOUNTING"))),
|
||||
Map.of("accounting__main", navigationAction("accounting__main", "accounting__main")));
|
||||
ServletContext servletContext = mock(ServletContext.class);
|
||||
Security security = mock(Security.class);
|
||||
GenericValue userLogin = new GenericValue();
|
||||
when(servletContext.getAttribute("security")).thenReturn(security);
|
||||
when(security.hasEntityPermission("ACCOUNTING", "_VIEW", userLogin)).thenReturn(true);
|
||||
ActionResource resource = new ActionResource();
|
||||
inject(resource, "servletContext", servletContext);
|
||||
inject(resource, "request", requestWithLogin(userLogin));
|
||||
|
||||
Response response = resource.runAction("accounting__main", Map.of());
|
||||
Map<String, Object> entity = responseEntity(response);
|
||||
Map<String, Object> data = data(entity);
|
||||
|
||||
assertEquals(200, response.getStatus());
|
||||
assertTrue((Boolean) entity.get("ok"));
|
||||
assertEquals("navigation", data.get("actionType"));
|
||||
assertEquals("accounting__main", data.get("navigationPageId"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void uploadsRemainExplicitContractOnlyUntilPolicyIsImplemented() {
|
||||
UploadResource resource = new UploadResource();
|
||||
|
||||
Response response = resource.upload("partyContent");
|
||||
Map<String, Object> entity = responseEntity(response);
|
||||
|
||||
assertEquals(501, response.getStatus());
|
||||
assertFalse((Boolean) entity.get("ok"));
|
||||
assertEquals("UPLOAD_CONTRACT_ONLY", firstErrorCode(entity));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void entityLookupAndOptionReadsRequireLogin() throws Exception {
|
||||
ServletContext servletContext = mock(ServletContext.class);
|
||||
HttpServletRequest request = requestWithoutLogin();
|
||||
Delegator delegator = mock(Delegator.class);
|
||||
when(servletContext.getAttribute("delegator")).thenReturn(delegator);
|
||||
|
||||
EntityResource entityResource = new EntityResource();
|
||||
inject(entityResource, "servletContext", servletContext);
|
||||
inject(entityResource, "request", request);
|
||||
assertAuthRequired(entityResource.list("OrderHeader", 0, 20, "", ""));
|
||||
|
||||
LookupResource lookupResource = new LookupResource();
|
||||
inject(lookupResource, "servletContext", servletContext);
|
||||
inject(lookupResource, "request", request);
|
||||
assertAuthRequired(lookupResource.lookup("Party", 0, 20, "", ""));
|
||||
|
||||
OptionResource optionResource = new OptionResource();
|
||||
inject(optionResource, "servletContext", servletContext);
|
||||
inject(optionResource, "request", request);
|
||||
assertAuthRequired(optionResource.entityOptions("StatusItem", null, null, "", List.of(), 40));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void entityLookupAndOptionReadsDenyUsersWithoutEntityPermission() throws Exception {
|
||||
ServletContext servletContext = mock(ServletContext.class);
|
||||
HttpServletRequest request = requestWithLogin(new GenericValue());
|
||||
Delegator delegator = mock(Delegator.class);
|
||||
Security security = mock(Security.class);
|
||||
ModelEntity modelEntity = mock(ModelEntity.class);
|
||||
when(servletContext.getAttribute("delegator")).thenReturn(delegator);
|
||||
when(servletContext.getAttribute("security")).thenReturn(security);
|
||||
when(delegator.getModelEntity("OrderHeader")).thenReturn(modelEntity);
|
||||
when(delegator.getModelEntity("Party")).thenReturn(modelEntity);
|
||||
when(delegator.getModelEntity("StatusItem")).thenReturn(modelEntity);
|
||||
when(modelEntity.getPackageName()).thenReturn("org.apache.ofbiz.order.order");
|
||||
when(modelEntity.getEntityName()).thenReturn("OrderHeader");
|
||||
|
||||
EntityResource entityResource = new EntityResource();
|
||||
inject(entityResource, "servletContext", servletContext);
|
||||
inject(entityResource, "request", request);
|
||||
assertForbidden(entityResource.list("OrderHeader", 0, 20, "", ""));
|
||||
|
||||
LookupResource lookupResource = new LookupResource();
|
||||
inject(lookupResource, "servletContext", servletContext);
|
||||
inject(lookupResource, "request", request);
|
||||
assertForbidden(lookupResource.lookup("Party", 0, 20, "", ""));
|
||||
|
||||
OptionResource optionResource = new OptionResource();
|
||||
inject(optionResource, "servletContext", servletContext);
|
||||
inject(optionResource, "request", request);
|
||||
assertForbidden(optionResource.entityOptions("StatusItem", null, null, "", List.of(), 40));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, Object> responseEntity(Response response) {
|
||||
return (Map<String, Object>) response.getEntity();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, Object> data(Map<String, Object> entity) {
|
||||
return (Map<String, Object>) entity.get("data");
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private String firstErrorCode(Map<String, Object> entity) {
|
||||
List<Map<String, Object>> errors = (List<Map<String, Object>>) entity.get("errors");
|
||||
return (String) errors.get(0).get("code");
|
||||
}
|
||||
|
||||
private void assertAuthRequired(Response response) {
|
||||
Map<String, Object> entity = responseEntity(response);
|
||||
assertEquals(401, response.getStatus());
|
||||
assertFalse((Boolean) entity.get("ok"));
|
||||
assertEquals("AUTH_REQUIRED", firstErrorCode(entity));
|
||||
}
|
||||
|
||||
private void assertForbidden(Response response) {
|
||||
assertForbidden(response, null);
|
||||
}
|
||||
|
||||
private void assertForbidden(Response response, String code) {
|
||||
Map<String, Object> entity = responseEntity(response);
|
||||
assertEquals(403, response.getStatus());
|
||||
assertFalse((Boolean) entity.get("ok"));
|
||||
if (code != null) {
|
||||
assertEquals(code, firstErrorCode(entity));
|
||||
}
|
||||
}
|
||||
|
||||
private HttpServletRequest requestWithoutLogin() {
|
||||
HttpServletRequest request = mock(HttpServletRequest.class);
|
||||
when(request.getSession(false)).thenReturn(null);
|
||||
return request;
|
||||
}
|
||||
|
||||
private HttpServletRequest requestWithLogin(GenericValue userLogin) {
|
||||
HttpServletRequest request = mock(HttpServletRequest.class);
|
||||
HttpSession session = mock(HttpSession.class);
|
||||
when(request.getSession(false)).thenReturn(session);
|
||||
when(session.getAttribute("userLogin")).thenReturn(userLogin);
|
||||
return request;
|
||||
}
|
||||
|
||||
private void inject(Object target, String name, Object value) throws Exception {
|
||||
Field field = target.getClass().getDeclaredField(name);
|
||||
field.setAccessible(true);
|
||||
field.set(target, value);
|
||||
}
|
||||
|
||||
private void seedInventory(Map<String, Object> pageDefinitions, Map<String, Object> actionDefinitions) throws Exception {
|
||||
Map<String, Object> inventory = new LinkedHashMap<>();
|
||||
inventory.put("pageDefinitions", pageDefinitions);
|
||||
inventory.put("actionDefinitions", actionDefinitions);
|
||||
setInventoryField("cachedInventory", inventory);
|
||||
setInventoryField("cachedInventoryPath", null);
|
||||
setInventoryField("cachedInventoryLastModified", -1L);
|
||||
}
|
||||
|
||||
private void setInventoryField(String name, Object value) throws Exception {
|
||||
Field field = UiInventoryLoader.class.getDeclaredField(name);
|
||||
field.setAccessible(true);
|
||||
field.set(null, value);
|
||||
}
|
||||
|
||||
private Map<String, Object> pageDefinition(String pageId, List<String> permissions) {
|
||||
Map<String, Object> page = new LinkedHashMap<>();
|
||||
page.put("pageId", pageId);
|
||||
page.put("title", pageId);
|
||||
page.put("layout", "backoffice");
|
||||
page.put("blocks", List.of());
|
||||
page.put("actions", List.of());
|
||||
page.put("permissions", permissions);
|
||||
page.put("legacy", Map.of("generated", true));
|
||||
return page;
|
||||
}
|
||||
|
||||
private Map<String, Object> navigationAction(String actionId, String pageId) {
|
||||
Map<String, Object> target = new LinkedHashMap<>();
|
||||
target.put("name", "success");
|
||||
target.put("type", "view");
|
||||
target.put("value", pageId);
|
||||
target.put("pageId", pageId);
|
||||
target.put("modernPath", "/modern/app/#/pages/" + pageId);
|
||||
|
||||
Map<String, Object> action = new LinkedHashMap<>();
|
||||
action.put("actionId", actionId);
|
||||
action.put("label", actionId);
|
||||
action.put("source", "controller");
|
||||
action.put("eventType", "");
|
||||
action.put("eventInvoke", "");
|
||||
action.put("eventPath", "");
|
||||
action.put("serviceName", "");
|
||||
action.put("auth", true);
|
||||
action.put("responses", List.of(target));
|
||||
return action;
|
||||
}
|
||||
}
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
/*******************************************************************************
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF 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.apache.ofbiz.modernapi.core;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import org.apache.ofbiz.entity.GenericValue;
|
||||
import org.apache.ofbiz.entity.model.ModelEntity;
|
||||
import org.apache.ofbiz.security.Security;
|
||||
import org.junit.Test;
|
||||
|
||||
public class ModernApiUtilPermissionTests {
|
||||
@Test
|
||||
public void mapsOrderEntityToOrderManagerViewPermission() {
|
||||
Security security = mock(Security.class);
|
||||
GenericValue userLogin = new GenericValue();
|
||||
ModelEntity modelEntity = modelEntity("OrderHeader", "org.apache.ofbiz.order.order");
|
||||
|
||||
when(security.hasEntityPermission("ORDERMGR", "_VIEW", userLogin)).thenReturn(true);
|
||||
|
||||
assertTrue(ModernApiUtil.hasEntityViewPermission(security, userLogin, modelEntity));
|
||||
verify(security).hasEntityPermission("ORDERMGR", "_VIEW", userLogin);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void allowsCommonReferenceOptionsWhenUserHasBusinessViewPermission() {
|
||||
Security security = mock(Security.class);
|
||||
GenericValue userLogin = new GenericValue();
|
||||
ModelEntity modelEntity = modelEntity("StatusItem", "org.apache.ofbiz.common.status");
|
||||
|
||||
when(security.hasEntityPermission("ORDERMGR", "_VIEW", userLogin)).thenReturn(true);
|
||||
|
||||
assertTrue(ModernApiUtil.hasEntityViewPermission(security, userLogin, modelEntity));
|
||||
verify(security).hasEntityPermission("ORDERMGR", "_VIEW", userLogin);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void allowsEntityDataAdminToViewAnyEntity() {
|
||||
Security security = mock(Security.class);
|
||||
GenericValue userLogin = new GenericValue();
|
||||
ModelEntity modelEntity = modelEntity("UnmappedThing", "org.example.custom");
|
||||
|
||||
when(security.hasEntityPermission("ENTITY_DATA", "_ADMIN", userLogin)).thenReturn(true);
|
||||
|
||||
assertTrue(ModernApiUtil.hasEntityViewPermission(security, userLogin, modelEntity));
|
||||
verify(security).hasEntityPermission("ENTITY_DATA", "_ADMIN", userLogin);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void allowsBusinessAdminToViewMappedEntity() {
|
||||
Security security = mock(Security.class);
|
||||
GenericValue userLogin = new GenericValue();
|
||||
ModelEntity modelEntity = modelEntity("OrderHeader", "org.apache.ofbiz.order.order");
|
||||
|
||||
when(security.hasEntityPermission("ORDERMGR", "_ADMIN", userLogin)).thenReturn(true);
|
||||
|
||||
assertTrue(ModernApiUtil.hasEntityViewPermission(security, userLogin, modelEntity));
|
||||
verify(security).hasEntityPermission("ORDERMGR", "_ADMIN", userLogin);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mapsMarketingEntityToMarketingViewPermission() {
|
||||
Security security = mock(Security.class);
|
||||
GenericValue userLogin = new GenericValue();
|
||||
ModelEntity modelEntity = modelEntity("MarketingCampaign", "org.apache.ofbiz.marketing.campaign");
|
||||
|
||||
when(security.hasEntityPermission("MARKETING", "_VIEW", userLogin)).thenReturn(true);
|
||||
|
||||
assertTrue(ModernApiUtil.hasEntityViewPermission(security, userLogin, modelEntity));
|
||||
verify(security).hasEntityPermission("MARKETING", "_VIEW", userLogin);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mapsWebsiteEntityToContentManagerViewPermission() {
|
||||
Security security = mock(Security.class);
|
||||
GenericValue userLogin = new GenericValue();
|
||||
ModelEntity modelEntity = modelEntity("WebSite", "org.apache.ofbiz.webapp.website");
|
||||
|
||||
when(security.hasEntityPermission("CONTENTMGR", "_VIEW", userLogin)).thenReturn(true);
|
||||
|
||||
assertTrue(ModernApiUtil.hasEntityViewPermission(security, userLogin, modelEntity));
|
||||
verify(security).hasEntityPermission("CONTENTMGR", "_VIEW", userLogin);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void deniesUnmappedEntityWithoutExplicitPermission() {
|
||||
Security security = mock(Security.class);
|
||||
GenericValue userLogin = new GenericValue();
|
||||
ModelEntity modelEntity = modelEntity("UnmappedThing", "org.example.custom");
|
||||
|
||||
assertFalse(ModernApiUtil.hasEntityViewPermission(security, userLogin, modelEntity));
|
||||
}
|
||||
|
||||
private ModelEntity modelEntity(String entityName, String packageName) {
|
||||
ModelEntity modelEntity = new ModelEntity();
|
||||
modelEntity.setEntityName(entityName);
|
||||
modelEntity.setPackageName(packageName);
|
||||
return modelEntity;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user