github.com/cloudwego/kitex@v0.9.0/tool/internal_pkg/pluginmode/thriftgo/hessian2.go (about)

     1  // Copyright 2023 CloudWeGo Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //   http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package thriftgo
    16  
    17  import (
    18  	"io"
    19  	"io/ioutil"
    20  	"os"
    21  	"path/filepath"
    22  	"regexp"
    23  	"strings"
    24  
    25  	"github.com/cloudwego/thriftgo/config"
    26  	"gopkg.in/yaml.v3"
    27  
    28  	"github.com/cloudwego/kitex/tool/internal_pkg/generator"
    29  	"github.com/cloudwego/kitex/tool/internal_pkg/log"
    30  	"github.com/cloudwego/kitex/tool/internal_pkg/util"
    31  )
    32  
    33  const (
    34  	JavaExtensionOption = "java_extension"
    35  
    36  	DubboCodec        = "github.com/kitex-contrib/codec-dubbo"
    37  	JavaThrift        = "java.thrift"
    38  	JavaThriftAddress = "https://raw.githubusercontent.com/kitex-contrib/codec-dubbo/main/java/java.thrift"
    39  )
    40  
    41  // Hessian2PreHook Hook before building cmd
    42  func Hessian2PreHook(cfg *generator.Config) {
    43  	// add thrift option
    44  	cfg.ThriftOptions = append(cfg.ThriftOptions, "template=slim,with_reflection,code_ref")
    45  	cfg.ThriftOptions = append(cfg.ThriftOptions, "enable_nested_struct")
    46  
    47  	// run hessian2 options
    48  	for _, opt := range cfg.Hessian2Options {
    49  		runOption(cfg, opt)
    50  	}
    51  }
    52  
    53  func IsHessian2(a generator.Config) bool {
    54  	return strings.EqualFold(a.Protocol, "hessian2")
    55  }
    56  
    57  func EnableJavaExtension(a generator.Config) bool {
    58  	for _, v := range a.Hessian2Options {
    59  		if strings.EqualFold(v, JavaExtensionOption) {
    60  			return true
    61  		}
    62  	}
    63  	return false
    64  }
    65  
    66  // runOption Execute the corresponding function according to the option
    67  func runOption(cfg *generator.Config, opt string) {
    68  	switch opt {
    69  	case JavaExtensionOption:
    70  		runJavaExtensionOption(cfg)
    71  	}
    72  }
    73  
    74  // runJavaExtensionOption Pull the extension file of java class from remote
    75  func runJavaExtensionOption(cfg *generator.Config) {
    76  	// get java.thrift, we assume java.thrift and IDL in the same directory so that IDL just needs to include "java.thrift"
    77  	if path := util.JoinPath(filepath.Dir(cfg.IDL), JavaThrift); !util.Exists(path) {
    78  		if err := util.DownloadFile(JavaThriftAddress, path); err != nil {
    79  			log.Warn("Downloading java.thrift file failed:", err.Error())
    80  			abs, err := filepath.Abs(path)
    81  			if err != nil {
    82  				abs = path
    83  			}
    84  			log.Warnf("You can try to download again. If the download still fails, you can choose to manually download \"%s\" to the local path \"%s\".", JavaThriftAddress, abs)
    85  			os.Exit(1)
    86  		}
    87  	}
    88  
    89  	// merge idl-ref configuration patch
    90  	patchIDLRefConfig(cfg)
    91  }
    92  
    93  // patchIDLRefConfig merge idl-ref configuration patch
    94  func patchIDLRefConfig(cfg *generator.Config) {
    95  	// load the idl-ref config file (create it first if it does not exist)
    96  	// for making use of idl-ref of thriftgo, we need to check the project root directory
    97  	idlRef := filepath.Join(cfg.OutputPath, "idl-ref.yaml")
    98  	if !util.Exists(idlRef) {
    99  		idlRef = filepath.Join(cfg.OutputPath, "idl-ref.yml")
   100  	}
   101  	file, err := os.OpenFile(idlRef, os.O_RDWR|os.O_CREATE, 0o644)
   102  	if err != nil {
   103  		log.Warnf("Open %s file failed: %s\n", idlRef, err.Error())
   104  		os.Exit(1)
   105  	}
   106  	defer file.Close()
   107  	idlRefCfg := loadIDLRefConfig(file.Name(), file)
   108  
   109  	// assume java.thrift and IDL are in the same directory
   110  	javaRef := filepath.Join(filepath.Dir(cfg.IDL), JavaThrift)
   111  	idlRefCfg.Ref[javaRef] = DubboCodec + "/java"
   112  	out, err := yaml.Marshal(idlRefCfg)
   113  	if err != nil {
   114  		log.Warn("Marshal configuration failed:", err.Error())
   115  		os.Exit(1)
   116  	}
   117  	// clear the file content
   118  	if err := file.Truncate(0); err != nil {
   119  		log.Warnf("Truncate file %s failed: %s", idlRef, err.Error())
   120  		os.Exit(1)
   121  	}
   122  	// set the file offset
   123  	if _, err := file.Seek(0, 0); err != nil {
   124  		log.Warnf("Seek file %s failed: %s", idlRef, err.Error())
   125  		os.Exit(1)
   126  	}
   127  	_, err = file.Write(out)
   128  	if err != nil {
   129  		log.Warnf("Write to file %s failed: %s\n", idlRef, err.Error())
   130  		os.Exit(1)
   131  	}
   132  }
   133  
   134  // loadIDLRefConfig load idl-ref config from file object
   135  func loadIDLRefConfig(fileName string, reader io.Reader) *config.RawConfig {
   136  	data, err := ioutil.ReadAll(reader)
   137  	if err != nil {
   138  		log.Warnf("Read %s file failed: %s\n", fileName, err.Error())
   139  		os.Exit(1)
   140  	}
   141  
   142  	// build idl ref config
   143  	idlRefCfg := new(config.RawConfig)
   144  	if len(data) == 0 {
   145  		idlRefCfg.Ref = make(map[string]interface{})
   146  	} else {
   147  		err := yaml.Unmarshal(data, idlRefCfg)
   148  		if err != nil {
   149  			log.Warnf("Parse %s file failed: %s\n", fileName, err.Error())
   150  			log.Warn("Please check whether the idl ref configuration is correct.")
   151  			os.Exit(2)
   152  		}
   153  	}
   154  
   155  	return idlRefCfg
   156  }
   157  
   158  var (
   159  	javaObjectRe                     = regexp.MustCompile(`\*java\.Object\b`)
   160  	javaExceptionRe                  = regexp.MustCompile(`\*java\.Exception\b`)
   161  	javaExceptionEmptyVerificationRe = regexp.MustCompile(`return p\.Exception != nil\b`)
   162  )
   163  
   164  // Hessian2PatchByReplace args is the arguments from command, subDirPath used for xx/xx/xx
   165  func Hessian2PatchByReplace(args generator.Config, subDirPath string) error {
   166  	output := args.OutputPath
   167  	newPath := util.JoinPath(output, args.GenPath, subDirPath)
   168  	fs, err := ioutil.ReadDir(newPath)
   169  	if err != nil {
   170  		return err
   171  	}
   172  
   173  	for _, f := range fs {
   174  		fileName := util.JoinPath(args.OutputPath, args.GenPath, subDirPath, f.Name())
   175  		if f.IsDir() {
   176  			subDirPath := util.JoinPath(subDirPath, f.Name())
   177  			if err = Hessian2PatchByReplace(args, subDirPath); err != nil {
   178  				return err
   179  			}
   180  		} else if strings.HasSuffix(f.Name(), ".go") {
   181  			data, err := ioutil.ReadFile(fileName)
   182  			if err != nil {
   183  				return err
   184  			}
   185  
   186  			data = replaceJavaObject(data)
   187  			data = replaceJavaException(data)
   188  			data = replaceJavaExceptionEmptyVerification(data)
   189  			if err = ioutil.WriteFile(fileName, data, 0o644); err != nil {
   190  				return err
   191  			}
   192  		}
   193  	}
   194  
   195  	// users do not specify -service flag, we do not need to replace
   196  	if args.ServiceName == "" {
   197  		return nil
   198  	}
   199  
   200  	handlerName := util.JoinPath(output, "handler.go")
   201  	handler, err := ioutil.ReadFile(handlerName)
   202  	if err != nil {
   203  		return err
   204  	}
   205  
   206  	handler = replaceJavaObject(handler)
   207  	return ioutil.WriteFile(handlerName, handler, 0o644)
   208  }
   209  
   210  func replaceJavaObject(content []byte) []byte {
   211  	return javaObjectRe.ReplaceAll(content, []byte("java.Object"))
   212  }
   213  
   214  func replaceJavaException(content []byte) []byte {
   215  	return javaExceptionRe.ReplaceAll(content, []byte("java.Exception"))
   216  }
   217  
   218  // replaceJavaExceptionEmptyVerification is used to resolve this issue:
   219  // After generating nested struct, the generated struct would be:
   220  //
   221  //	 type CustomizedException struct {
   222  //	     *java.Exception
   223  //	 }
   224  //
   225  //	 It has a method:
   226  //	 func (p *EchoCustomizedException) IsSetException() bool {
   227  //		    return p.Exception != nil
   228  //	 }
   229  //
   230  //	 After invoking replaceJavaException, *java.Exception would be converted
   231  //	 to java.Exception and IsSetException became invalid. We convert the statement
   232  //	 to `return true` to ignore this problem.
   233  func replaceJavaExceptionEmptyVerification(content []byte) []byte {
   234  	return javaExceptionEmptyVerificationRe.ReplaceAll(content, []byte("return true"))
   235  }