This is an automated email from the ASF dual-hosted git repository.

nferraro pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/camel-k.git

commit 7a9b7f896fd9e975c0c077de7223eae49eff5222
Author: Nicola Ferraro <ni.ferr...@gmail.com>
AuthorDate: Thu May 7 15:05:37 2020 +0200

    Fix #1449: add support for modeline options
---
 cmd/kamel/main.go                |  16 ++++-
 pkg/cmd/modeline.go              | 130 +++++++++++++++++++++++++++++++++
 pkg/cmd/modeline_test.go         | 119 ++++++++++++++++++++++++++++++
 pkg/cmd/run.go                   |   8 +--
 pkg/util/modeline/parser.go      |  84 ++++++++++++++++++++++
 pkg/util/modeline/parser_test.go | 152 +++++++++++++++++++++++++++++++++++++++
 pkg/util/modeline/types.go       |   7 ++
 7 files changed, 510 insertions(+), 6 deletions(-)

diff --git a/cmd/kamel/main.go b/cmd/kamel/main.go
index 2ba8ea9..be385b4 100644
--- a/cmd/kamel/main.go
+++ b/cmd/kamel/main.go
@@ -19,6 +19,7 @@ package main
 
 import (
        "context"
+       "fmt"
        "math/rand"
        "os"
        "time"
@@ -40,8 +41,19 @@ func main() {
        // Cancel ctx as soon as main returns
        defer cancel()
 
-       rootCmd, err := cmd.NewKamelCommand(ctx)
-       exitOnError(err)
+       // Add modeline options to the command
+       rootCmd, args, err := cmd.NewKamelWithModelineCommand(ctx, os.Args)
+       if err != nil {
+               fmt.Printf("Error: %s\n", err.Error())
+               exitOnError(err)
+       }
+
+       // Give a feedback about the actual command that is run
+       fmt.Fprint(rootCmd.OutOrStdout(), "Executing: kamel ")
+       for _, a := range args {
+               fmt.Fprintf(rootCmd.OutOrStdout(), "%s ", a)
+       }
+       fmt.Fprintln(rootCmd.OutOrStdout())
 
        err = rootCmd.Execute()
        exitOnError(err)
diff --git a/pkg/cmd/modeline.go b/pkg/cmd/modeline.go
new file mode 100644
index 0000000..60343ca
--- /dev/null
+++ b/pkg/cmd/modeline.go
@@ -0,0 +1,130 @@
+package cmd
+
+import (
+       "context"
+       "fmt"
+       "path"
+
+       "github.com/apache/camel-k/pkg/util/modeline"
+       "github.com/pkg/errors"
+       "github.com/spf13/cobra"
+       "path/filepath"
+)
+
+const (
+       runCmdName        = "run"
+       runCmdSourcesArgs = "source"
+)
+
+var (
+       nonRunOptions = map[string]bool{
+               "language": true, // language is a marker modeline option for 
other tools
+       }
+
+       // file options must be considered relative to the source files they 
belong to
+       fileOptions = map[string]bool{
+               "source":        true,
+               "resource":      true,
+               "config":        true,
+               "open-api":      true,
+               "property-file": true,
+       }
+)
+
+func NewKamelWithModelineCommand(ctx context.Context, osArgs []string) 
(*cobra.Command, []string, error) {
+       processed := make(map[string]bool)
+       return createKamelWithModelineCommand(ctx, osArgs[1:], processed)
+}
+
+func createKamelWithModelineCommand(ctx context.Context, args []string, 
processedFiles map[string]bool) (*cobra.Command, []string, error) {
+       rootCmd, err := NewKamelCommand(ctx)
+       if err != nil {
+               return nil, nil, err
+       }
+
+       target, flags, err := rootCmd.Find(args)
+       if err != nil {
+               return nil, nil, err
+       }
+
+       if target.Name() != runCmdName {
+               return rootCmd, args, nil
+       }
+
+       err = target.ParseFlags(flags)
+       if err != nil {
+               return nil, nil, err
+       }
+
+       fg := target.Flags()
+
+       sources, err := fg.GetStringArray(runCmdSourcesArgs)
+       if err != nil {
+               return nil, nil, err
+       }
+
+       var files = append([]string(nil), fg.Args()...)
+       files = append(files, sources...)
+
+       var opts []modeline.Option
+       for _, f := range files {
+               if processedFiles[f] {
+                       continue
+               }
+               baseDir := filepath.Dir(f)
+               content, err := loadData(f, false)
+               if err != nil {
+                       return nil, nil, errors.Wrapf(err, "cannot read file 
%s", f)
+               }
+               ops, err := modeline.Parse(f, content)
+               if err != nil {
+                       return nil, nil, errors.Wrapf(err, "cannot process file 
%s", f)
+               }
+               for i, o := range ops {
+                       if fileOptions[o.Name] && !isRemoteHTTPFile(f) {
+                               refPath := o.Value
+                               if !filepath.IsAbs(refPath) {
+                                       full := path.Join(baseDir, refPath)
+                                       o.Value = full
+                                       ops[i] = o
+                               }
+                       }
+               }
+               opts = append(opts, ops...)
+       }
+       // filter out in place non-run options
+       nOpts := 0
+       for _, o := range opts {
+               if !nonRunOptions[o.Name] {
+                       opts[nOpts] = o
+                       nOpts++
+               }
+       }
+       opts = opts[:nOpts]
+
+       // No new options, returning a new command with computed args
+       if len(opts) == 0 {
+               // Recreating the command as it's dirty
+               rootCmd, err = NewKamelCommand(ctx)
+               if err != nil {
+                       return nil, nil, err
+               }
+               rootCmd.SetArgs(args)
+               return rootCmd, args, nil
+       }
+
+       // New options added, recomputing
+       for _, f := range files {
+               processedFiles[f] = true
+       }
+       for _, o := range opts {
+               prefix := "-"
+               if len(o.Name) > 1 {
+                       prefix = "--"
+               }
+               args = append(args, fmt.Sprintf("%s%s", prefix, o.Name))
+               args = append(args, o.Value)
+       }
+
+       return createKamelWithModelineCommand(ctx, args, processedFiles)
+}
diff --git a/pkg/cmd/modeline_test.go b/pkg/cmd/modeline_test.go
new file mode 100644
index 0000000..6eabd18
--- /dev/null
+++ b/pkg/cmd/modeline_test.go
@@ -0,0 +1,119 @@
+/*
+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 cmd
+
+import (
+       "context"
+       "os"
+       "path"
+       "testing"
+
+       "github.com/stretchr/testify/assert"
+       "io/ioutil"
+)
+
+func TestModelineRunSimple(t *testing.T) {
+       dir, err := ioutil.TempDir("", "camel-k-test-")
+       assert.NoError(t, err)
+       defer os.RemoveAll(dir)
+
+       file := `
+               // camel-k: dependency=mvn:org.my:lib:1.0
+       `
+       fileName := path.Join(dir, "simple.groovy")
+       err = ioutil.WriteFile(fileName, []byte(file), 0777)
+       assert.NoError(t, err)
+
+       cmd, flags, err := NewKamelWithModelineCommand(context.TODO(), 
[]string{"kamel", "run", fileName})
+       assert.NoError(t, err)
+       assert.NotNil(t, cmd)
+       assert.Equal(t, []string{"run", fileName, "--dependency", 
"mvn:org.my:lib:1.0"}, flags)
+}
+
+func TestModelineRunChain(t *testing.T) {
+       dir, err := ioutil.TempDir("", "camel-k-test-")
+       assert.NoError(t, err)
+       defer os.RemoveAll(dir)
+
+       file := `
+               // camel-k: dependency=mvn:org.my:lib:1.0
+       `
+       fileName := path.Join(dir, "simple.groovy")
+       err = ioutil.WriteFile(fileName, []byte(file), 0777)
+       assert.NoError(t, err)
+
+       cmd, flags, err := NewKamelWithModelineCommand(context.TODO(), 
[]string{"kamel", "run", "-d", "mvn:org.my:lib2:1.0", fileName})
+       assert.NoError(t, err)
+       assert.NotNil(t, cmd)
+       assert.Equal(t, []string{"run", "-d", "mvn:org.my:lib2:1.0", fileName, 
"--dependency", "mvn:org.my:lib:1.0"}, flags)
+}
+
+func TestModelineRunMultipleFiles(t *testing.T) {
+       dir, err := ioutil.TempDir("", "camel-k-test-")
+       assert.NoError(t, err)
+       defer os.RemoveAll(dir)
+
+       file := `
+               // camel-k: source=ext.groovy
+       `
+       fileName := path.Join(dir, "simple.groovy")
+       err = ioutil.WriteFile(fileName, []byte(file), 0777)
+       assert.NoError(t, err)
+
+       file2 := `
+               // camel-k: dependency=mvn:org.my:lib:1.0
+       `
+       fileName2 := path.Join(dir, "ext.groovy")
+       err = ioutil.WriteFile(fileName2, []byte(file2), 0777)
+       assert.NoError(t, err)
+
+       cmd, flags, err := NewKamelWithModelineCommand(context.TODO(), 
[]string{"kamel", "run", fileName})
+       assert.NoError(t, err)
+       assert.NotNil(t, cmd)
+       assert.Equal(t, []string{"run", fileName, "--source", fileName2, 
"--dependency", "mvn:org.my:lib:1.0"}, flags)
+}
+
+func TestModelineRunPropertyFiles(t *testing.T) {
+       dir, err := ioutil.TempDir("", "camel-k-test-")
+       assert.NoError(t, err)
+       defer os.RemoveAll(dir)
+
+       subDir := path.Join(dir, "sub")
+       err = os.Mkdir(subDir, 0777)
+       assert.NoError(t, err)
+
+       file := `
+               // camel-k: property-file=../application.properties
+       `
+       fileName := path.Join(subDir, "simple.groovy")
+       err = ioutil.WriteFile(fileName, []byte(file), 0777)
+       assert.NoError(t, err)
+
+       propFile := `
+               a=b
+       `
+       propFileName := path.Join(dir, "application.properties")
+       err = ioutil.WriteFile(propFileName, []byte(propFile), 0777)
+       assert.NoError(t, err)
+
+       cmd, flags, err := NewKamelWithModelineCommand(context.TODO(), 
[]string{"kamel", "run", fileName})
+       assert.NoError(t, err)
+       assert.NotNil(t, cmd)
+       assert.Equal(t, []string{"run", fileName, "--property-file", 
propFileName}, flags)
+}
diff --git a/pkg/cmd/run.go b/pkg/cmd/run.go
index f2ee010..66e3a88 100644
--- a/pkg/cmd/run.go
+++ b/pkg/cmd/run.go
@@ -470,7 +470,7 @@ func (o *runCmdOptions) updateIntegrationCode(c 
client.Client, sources []string)
        srcs = append(srcs, o.Sources...)
 
        for _, source := range srcs {
-               data, err := o.loadData(source, o.Compression)
+               data, err := loadData(source, o.Compression)
                if err != nil {
                        return nil, err
                }
@@ -485,7 +485,7 @@ func (o *runCmdOptions) updateIntegrationCode(c 
client.Client, sources []string)
        }
 
        for _, resource := range o.Resources {
-               data, err := o.loadData(resource, o.Compression)
+               data, err := loadData(resource, o.Compression)
                if err != nil {
                        return nil, err
                }
@@ -501,7 +501,7 @@ func (o *runCmdOptions) updateIntegrationCode(c 
client.Client, sources []string)
        }
 
        for _, resource := range o.OpenAPIs {
-               data, err := o.loadData(resource, o.Compression)
+               data, err := loadData(resource, o.Compression)
                if err != nil {
                        return nil, err
                }
@@ -623,7 +623,7 @@ func (o *runCmdOptions) GetIntegrationName(sources 
[]string) string {
        return name
 }
 
-func (*runCmdOptions) loadData(fileName string, compress bool) (string, error) 
{
+func loadData(fileName string, compress bool) (string, error) {
        var content []byte
        var err error
 
diff --git a/pkg/util/modeline/parser.go b/pkg/util/modeline/parser.go
new file mode 100644
index 0000000..f1ce980
--- /dev/null
+++ b/pkg/util/modeline/parser.go
@@ -0,0 +1,84 @@
+package modeline
+
+import (
+       "bufio"
+       "fmt"
+       "regexp"
+       "strings"
+
+       v1 "github.com/apache/camel-k/pkg/apis/camel/v1"
+)
+
+var (
+       commonModelineRegexp = 
regexp.MustCompile(`^\s*//\s*camel-k\s*:\s*([^\s]+.*)$`)
+       yamlModelineRegexp   = 
regexp.MustCompile(`^\s*#+\s*camel-k\s*:\s*([^\s]+.*)$`)
+       xmlModelineRegexp    = 
regexp.MustCompile(`^.*<!--\s*camel-k\s*:\s*([^\s]+[^>]*)-->.*$`)
+
+       delimiter = regexp.MustCompile(`\s+`)
+)
+
+func Parse(name, content string) (res []Option, err error) {
+       lang := inferLanguage(name)
+       if lang == "" {
+               return nil, fmt.Errorf("unsupported file type %s", name)
+       }
+       scanner := bufio.NewScanner(strings.NewReader(content))
+       for scanner.Scan() {
+               res = append(res, getModelineOptions(scanner.Text(), lang)...)
+       }
+       return res, scanner.Err()
+}
+
+func getModelineOptions(line string, lang v1.Language) (res []Option) {
+       reg := modelineRegexp(lang)
+       if !reg.MatchString(line) {
+               return nil
+       }
+       strs := reg.FindStringSubmatch(line)
+       if len(strs) == 2 {
+               tokens := delimiter.Split(strs[1], -1)
+               for _, token := range tokens {
+                       if len(strings.Trim(token, "\t\n\f\r ")) == 0 {
+                               continue
+                       }
+                       eq := strings.Index(token, "=")
+                       var name, value string
+                       if eq > 0 {
+                               name = token[0:eq]
+                               value = token[eq+1:]
+                       } else {
+                               name = token
+                               value = ""
+                       }
+                       opt := Option{
+                               Name:  name,
+                               Value: value,
+                       }
+                       res = append(res, opt)
+               }
+       }
+       return res
+}
+
+func modelineRegexp(lang v1.Language) *regexp.Regexp {
+       switch lang {
+       case v1.LanguageYaml:
+               return yamlModelineRegexp
+       case v1.LanguageXML:
+               return xmlModelineRegexp
+       default:
+               return commonModelineRegexp
+       }
+}
+
+func inferLanguage(fileName string) v1.Language {
+       for _, l := range v1.Languages {
+               if strings.HasSuffix(fileName, fmt.Sprintf(".%s", string(l))) {
+                       return l
+               }
+       }
+       if strings.HasSuffix(fileName, ".yml") {
+               return v1.LanguageYaml
+       }
+       return ""
+}
diff --git a/pkg/util/modeline/parser_test.go b/pkg/util/modeline/parser_test.go
new file mode 100644
index 0000000..0a703db
--- /dev/null
+++ b/pkg/util/modeline/parser_test.go
@@ -0,0 +1,152 @@
+package modeline
+
+import (
+       "github.com/stretchr/testify/assert"
+       "testing"
+)
+
+func TestParseGroovyFile(t *testing.T) {
+       it := `
+               //     camel-k: pippo=pluto     paperino ciao=1   
+               // camel-k : ciao
+
+               from("timer:tick").log("Ciao")
+    `
+       opts, err := Parse("simple.groovy", it)
+       assert.NoError(t, err)
+       assert.Len(t, opts, 4)
+       assert.Contains(t, opts, Option{Name: "pippo", Value: "pluto"})
+       assert.Contains(t, opts, Option{Name: "paperino"})
+       assert.Contains(t, opts, Option{Name: "ciao", Value: "1"})
+       assert.Contains(t, opts, Option{Name: "ciao"})
+}
+
+func TestParseKotlinFile(t *testing.T) {
+       it := `
+               //     camel-k: pippo=pluto     paperino ciao=1   
+               // camel-k : ciao
+
+               from("timer:tick").log("Ciao")
+    `
+       opts, err := Parse("example.kts", it)
+       assert.NoError(t, err)
+       assert.Len(t, opts, 4)
+       assert.Contains(t, opts, Option{Name: "pippo", Value: "pluto"})
+       assert.Contains(t, opts, Option{Name: "paperino"})
+       assert.Contains(t, opts, Option{Name: "ciao", Value: "1"})
+       assert.Contains(t, opts, Option{Name: "ciao"})
+}
+
+func TestParseJavaFile(t *testing.T) {
+       it := `
+               //     camel-k: pippo=pluto     paperino ciao=1   
+               // camel-k : ciao
+
+               import org.apache.camel.builder.RouteBuilder;
+               
+               public class {{ .Name }} extends RouteBuilder {
+                 @Override
+                 public void configure() throws Exception {
+               
+                         // Write your routes here, for example:
+                         from("timer:java?period=1000")
+                               .routeId("java")
+                               .setBody()
+                                 .simple("Hello Camel K from ${routeId}")
+                               .to("log:info");
+               
+                 }
+               }
+    `
+       opts, err := Parse("Example.java", it)
+       assert.NoError(t, err)
+       assert.Len(t, opts, 4)
+       assert.Contains(t, opts, Option{Name: "pippo", Value: "pluto"})
+       assert.Contains(t, opts, Option{Name: "paperino"})
+       assert.Contains(t, opts, Option{Name: "ciao", Value: "1"})
+       assert.Contains(t, opts, Option{Name: "ciao"})
+}
+
+func TestParseJSFile(t *testing.T) {
+       it := `
+               //     camel-k: pippo=pluto     paperino ciao=1   
+               // camel-k : ciao 
+               // Write your routes here, for example:
+               from('timer:js?period=1000')
+                       .routeId('js')
+                       .setBody()
+                         .simple('Hello Camel K from ${routeId}')
+                       .to('log:info')
+    `
+       opts, err := Parse("example.js", it)
+       assert.NoError(t, err)
+       assert.Len(t, opts, 4)
+       assert.Contains(t, opts, Option{Name: "pippo", Value: "pluto"})
+       assert.Contains(t, opts, Option{Name: "paperino"})
+       assert.Contains(t, opts, Option{Name: "ciao", Value: "1"})
+       assert.Contains(t, opts, Option{Name: "ciao"})
+}
+
+func TestParseYAMLFile(t *testing.T) {
+       it := `
+               #     camel-k: pippo=pluto     paperino ciao=1   
+               ### camel-k : ciao 
+
+               # Write your routes here, for example:
+               - from:
+                       uri: "timer:yaml"
+                       parameters:
+                         period: "1000"
+                       steps:
+                         - set-body:
+                                 constant: "Hello Camel K from yaml"
+                         - to: "log:info"
+
+    `
+       opts, err := Parse("example.yaml", it)
+       assert.NoError(t, err)
+       assert.Len(t, opts, 4)
+       assert.Contains(t, opts, Option{Name: "pippo", Value: "pluto"})
+       assert.Contains(t, opts, Option{Name: "paperino"})
+       assert.Contains(t, opts, Option{Name: "ciao", Value: "1"})
+       assert.Contains(t, opts, Option{Name: "ciao"})
+}
+
+func TestParseXMLFile(t *testing.T) {
+       it := `
+               #     camel-k: pippo=pluto     paperino ciao=1   
+               ### camel-k : ciao 
+
+               <?xml version="1.0" encoding="UTF-8"?>
+               <!--     camel-k: pippo=pluto     paperino ciao=1-->
+               <!--camel-k : ciao -->
+               <!--     camel-k: language=xml -->
+
+               <routes xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+                               xmlns="http://camel.apache.org/schema/spring";
+                               xsi:schemaLocation="
+                                       http://camel.apache.org/schema/spring
+                                       
http://camel.apache.org/schema/spring/camel-spring.xsd";>
+               
+                       <!-- Write your routes here, for example: -->
+                       <route id="xml">
+                               <from uri="timer:xml?period=1000"/>
+                               <setBody>
+                                       <simple>Hello Camel K from 
${routeId}</simple>
+                               </setBody>
+                               <to uri="log:info"/>
+                       </route>
+               
+               </routes>
+
+
+    `
+       opts, err := Parse("example.xml", it)
+       assert.NoError(t, err)
+       assert.Len(t, opts, 5)
+       assert.Contains(t, opts, Option{Name: "pippo", Value: "pluto"})
+       assert.Contains(t, opts, Option{Name: "paperino"})
+       assert.Contains(t, opts, Option{Name: "ciao", Value: "1"})
+       assert.Contains(t, opts, Option{Name: "ciao"})
+       assert.Contains(t, opts, Option{Name: "language", Value: "xml"})
+}
diff --git a/pkg/util/modeline/types.go b/pkg/util/modeline/types.go
new file mode 100644
index 0000000..e7c6840
--- /dev/null
+++ b/pkg/util/modeline/types.go
@@ -0,0 +1,7 @@
+package modeline
+
+// Option represents a key/(optional)value modeline option
+type Option struct {
+       Name  string
+       Value string
+}

Reply via email to