| /* |
| * 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.flink.sql.parser.errorcode; |
| |
| import org.apache.commons.lang3.StringUtils; |
| |
| import java.lang.annotation.ElementType; |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| import java.lang.annotation.Target; |
| import java.lang.reflect.Method; |
| import java.lang.reflect.Modifier; |
| import java.lang.reflect.Proxy; |
| import java.text.DateFormat; |
| import java.text.Format; |
| import java.text.MessageFormat; |
| import java.text.NumberFormat; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Date; |
| import java.util.EnumSet; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Set; |
| import java.util.regex.Pattern; |
| |
| /** |
| * define static error code instances and implements logic |
| * |
| * <p>it serves as: |
| * I. Define static error code instances per module: |
| * table/sql api, blink sql parser, runtime, state backend, and connector |
| * II. Define some annotations for use. |
| * III. Implement logic for proxy instance creating, error message assembly, and validation. |
| */ |
| public class ErrorFactory { |
| |
| protected static Pattern pattern1 = Pattern.compile("[0-9]{8}"); |
| protected static Set<String> modNames = new HashSet<>(); |
| |
| static { |
| modNames.add("SQL"); |
| modNames.add("PAR"); |
| modNames.add("CON"); |
| modNames.add("STB"); |
| modNames.add("RUN"); |
| } |
| |
| private ErrorFactory() { |
| } |
| |
| /** |
| * create proxy instance for one module's error code interface. |
| * |
| * @param clazz interface that has error code definitions for one module. |
| * @return instance of the interface that can be used by developer for specifying error code |
| * (for now, it is for dumping error code and its cause&action message) |
| * when throwing exceptions |
| */ |
| public static Object createProxy(Class clazz) { |
| |
| return Proxy.newProxyInstance( |
| clazz.getClassLoader(), |
| clazz.isInterface() ? new Class[]{clazz} : clazz.getInterfaces(), |
| (obj, method, args) -> { |
| checkParam(method, args); |
| return assemblyErrCodeString(method, args); |
| }); |
| |
| } |
| |
| /** |
| * Parameter check when invoking method. |
| * |
| * @param method |
| * @param args |
| */ |
| protected static void checkParam(Method method, Object[] args) { |
| ErrCode errCode = method.getAnnotation(ErrCode.class); |
| String errDetail = errCode.details(); |
| String errCause = errCode.cause(); |
| |
| MessageFormat format1 = new MessageFormat(errDetail); |
| MessageFormat format2 = new MessageFormat(errCause); |
| |
| if (args == null || args.length == 0) { |
| if ((format1.getFormatsByArgumentIndex() != null && format1.getFormatsByArgumentIndex().length > 0) |
| || (format2.getFormatsByArgumentIndex() != null && format2.getFormatsByArgumentIndex().length > 0)) { |
| throw new AssertionError("mismatched parameter length between " |
| + method.getName() + " and its annotation @ErrCode"); |
| } |
| } else { |
| if ((format1.getFormatsByArgumentIndex() != null && format1.getFormatsByArgumentIndex().length > args.length) |
| || format1.getFormatsByArgumentIndex() == null |
| || (format2.getFormatsByArgumentIndex() != null && format2.getFormatsByArgumentIndex().length > args.length)) { |
| throw new AssertionError("mismatched parameter length between " |
| + method.getName() + " and its annotation @ErrCode"); |
| } |
| } |
| } |
| |
| /** |
| * assembly error code messages. |
| * |
| * @param method error code related function declared in error interface |
| * @param args args passed to that related function |
| * @return error code messages containing code id, cause and action. |
| */ |
| protected static String assemblyErrCodeString(Method method, Object[] args) { |
| ErrCode errCode = method.getAnnotation(ErrCode.class); |
| String errId = errCode.codeId(); |
| String errCause = errCode.cause(); |
| String errDetail = errCode.details(); |
| String errAction = errCode.action(); |
| |
| if (args != null && args.length != 0) { |
| MessageFormat format1 = new MessageFormat(errDetail); |
| errDetail = format1.format(args); |
| |
| MessageFormat format2 = new MessageFormat(errCause); |
| errCause = format2.format(args); |
| } |
| |
| errId = prettyPrint(errId); |
| errCause = prettyPrint(errCause); |
| errDetail = prettyPrint(errDetail); |
| errAction = prettyPrint(errAction); |
| |
| String msg = "\n************\n" |
| //"\n*******************************************************\n" |
| + "ERR_ID:\n" |
| + errId + "\n" |
| + "CAUSE:\n" |
| + errCause + "\n" |
| + "ACTION:\n" |
| + errAction + "\n" |
| + "DETAIL:\n" |
| + errDetail + "\n" |
| //+ "*******************************************************"; |
| + "************"; |
| return msg; |
| } |
| |
| /** |
| * Print out error code in a pretty way. |
| * |
| * @param str |
| * @return |
| */ |
| public static String prettyPrint(String str) { |
| if (str != null && str.length() != 0) { |
| str = indent(5) + str.replaceAll("\n", "\n" + indent(5)); |
| } |
| |
| return str; |
| } |
| |
| /** |
| * validate an error code definition interface to check its annotation usage |
| * and err code format. |
| * |
| * @param clazz err code definition interface |
| */ |
| public static void validate(Class<?> clazz) { |
| validate(clazz, EnumSet.allOf(ValidationType.class)); |
| } |
| |
| /** |
| * validate an error code definition interface to check its annotation usage |
| * and err code format. |
| * |
| * @param clazz err code definition interface |
| * @param validations types of validations to perform. |
| */ |
| public static void validate(Class<?> clazz, EnumSet<ValidationType> validations) { |
| int cnt = 0; |
| Set<String> errIds = new HashSet<>(); |
| |
| for (Method method : clazz.getMethods()) { |
| if (!Modifier.isStatic(method.getModifiers())) { |
| cnt++; |
| |
| final ErrCode anno1 = method.getAnnotation(ErrCode.class); |
| |
| for (ValidationType validation : validations) { |
| switch (validation) { |
| case ANNOTATION_SPECIFIED: |
| if (anno1 == null || StringUtils.isEmpty(anno1.codeId()) |
| || StringUtils.isEmpty(anno1.cause())) { |
| throw new AssertionError(String.format("error code method[%s] " |
| + "must specify @ErrCode annotation with cause, details and none-empty codeId!", |
| method.getName())); |
| } |
| break; |
| |
| case ERROR_ID_CHECK: |
| if (anno1 == null) { |
| throw new AssertionError(String.format("error code method[%s]" |
| + "has no @ErrId annotation!", |
| method.getName())); |
| } |
| String errId = anno1.codeId(); |
| if (!checkErrorCodeFmt(errId)) { |
| throw new AssertionError(String.format("error code method[%s]" |
| + "has invalid error code: %s", |
| method.getName(), |
| errId)); |
| } |
| if (errIds.contains(errId)) { |
| throw new AssertionError(String.format("error code method[%s]" |
| + "has duplicated err id: %s", |
| method.getName(), |
| errId)); |
| } |
| errIds.add(errId); |
| break; |
| |
| case ARGUMENT_MATCH: |
| if (anno1 == null) { |
| throw new AssertionError(String.format("error code method[%s]" |
| + "has no @ErrCode annotation!", |
| method.getName())); |
| } |
| |
| String msg = anno1.details(); |
| String cause = anno1.cause(); |
| |
| MessageFormat msgFmt = new MessageFormat(msg); |
| MessageFormat causeFmt = new MessageFormat(cause); |
| |
| final Format[] msgFormats = msgFmt.getFormatsByArgumentIndex(); |
| final Format[] causeFormats = causeFmt.getFormatsByArgumentIndex(); |
| |
| final Format[] formats = msgFormats.length != 0 ? msgFormats : causeFormats; |
| |
| final List<Class> types = new ArrayList<>(); |
| final Class<?>[] paramTypes = method.getParameterTypes(); |
| |
| /* implement check on case when only one field get parameter(s) |
| * TODO implement check on case when both fields get parameter(s) |
| */ |
| if (!(msgFormats.length != 0 && causeFormats.length != 0)) { |
| |
| for (int i = 0; i < formats.length; i++) { |
| Format fmt1 = formats[i]; |
| Class paramType = paramTypes[i]; |
| final Class<?> e; |
| if (fmt1 instanceof NumberFormat) { |
| e = paramType == short.class |
| || paramType == int.class |
| || paramType == long.class |
| || paramType == float.class |
| || paramType == double.class |
| || Number.class.isAssignableFrom(paramType) |
| ? paramType |
| : Number.class; |
| } else if (fmt1 instanceof DateFormat) { |
| e = Date.class; |
| } else { |
| e = String.class; |
| } |
| |
| types.add(e); |
| } |
| |
| final List<Class<?>> paramTypeList = Arrays.asList(paramTypes); |
| if (!types.equals(paramTypeList)) { |
| throw new AssertionError(String.format("error code[%s]" |
| + " has type mismatch(s) between method param %s and" |
| + " format elements %s in annotation", |
| method.getName(), |
| types, |
| paramTypeList |
| )); |
| } |
| } |
| break; |
| |
| default: |
| break; |
| } |
| } |
| } |
| } |
| |
| if (cnt == 0 && validations.contains(ValidationType.AT_LEAST_ONE)) { |
| throw new AssertionError(clazz + " contains no error code"); |
| } |
| } |
| |
| /** |
| * Check error code id format. |
| * |
| * @param errId |
| * @return |
| */ |
| protected static boolean checkErrorCodeFmt(String errId) { |
| if (errId == null || errId.isEmpty()) { |
| return false; |
| } |
| String[] parts = errId.split("-"); |
| if (parts.length != 2) { |
| return false; |
| } |
| if (!modNames.contains(parts[0])) { |
| return false; |
| } |
| if (!pattern1.matcher(parts[1]).matches()) { |
| return false; |
| } |
| |
| return true; |
| } |
| |
| /** |
| * Get multiple indent. |
| * |
| * @param cnt |
| * @return |
| */ |
| public static String indent(int cnt) { |
| if (cnt <= 0) { |
| return null; |
| } |
| StringBuilder sb = new StringBuilder(); |
| for (int i = 0; i < cnt; i++) { |
| sb.append(" "); |
| } |
| return sb.toString(); |
| } |
| |
| /** |
| * Types of validation that can be performed on a resource. |
| */ |
| public enum ValidationType { |
| /** |
| * Checks that the ErrId, ErrCause and ErrAction annotations are on every resource. |
| */ |
| ANNOTATION_SPECIFIED, |
| |
| /** |
| * Checks that there is at least one resource. |
| */ |
| AT_LEAST_ONE, |
| |
| /** |
| * Checks that @ErrId anno has non-null value which has expected error id format, |
| * and there's no duplicated error id in resource instance. |
| */ |
| ERROR_ID_CHECK, |
| |
| /** |
| * Checks that the parameters of the method are consistent with the |
| * format elements in the error cause message. |
| */ |
| ARGUMENT_MATCH, |
| } |
| |
| /** |
| * err code id, cause, detailed message and action. |
| **/ |
| @Retention(RetentionPolicy.RUNTIME) |
| @Target(ElementType.METHOD) |
| public @interface ErrCode { |
| String codeId(); |
| |
| String cause(); |
| |
| String details(); |
| |
| String action(); |
| } |
| |
| } |