diff pkg/xlsx/templater.go @ 5399:47c2ca05e8ec

Merged extented-report branch back into default.
author Sascha L. Teichmann <sascha.teichmann@intevation.de>
date Wed, 07 Jul 2021 11:44:40 +0200
parents 1adbd6a5f849
children 699048c86848
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pkg/xlsx/templater.go	Wed Jul 07 11:44:40 2021 +0200
@@ -0,0 +1,672 @@
+// This is Free Software under GNU Affero General Public License v >= 3.0
+// without warranty, see README.md and license for details.
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+// License-Filename: LICENSES/AGPL-3.0.txt
+//
+// Copyright (C) 2021 by via donau
+//   – Österreichische Wasserstraßen-Gesellschaft mbH
+// Software engineering by Intevation GmbH
+//
+// Author(s):
+//  * Sascha L. Teichmann <sascha.teichmann@intevation.de>
+
+package xlsx
+
+import (
+	"bufio"
+	"context"
+	"database/sql"
+	"errors"
+	"fmt"
+	"io"
+	"log"
+	"os"
+	"strconv"
+	"strings"
+
+	"github.com/360EntSecGroup-Skylar/excelize/v2"
+	"gopkg.in/yaml.v2"
+
+	"github.com/PaesslerAG/gval"
+)
+
+type Action struct {
+	Type        string    `yaml:"type"`
+	Actions     []*Action `yaml:"actions"`
+	Location    []string  `yaml:"location"`
+	Source      string    `yaml:"source"`
+	Destination string    `yaml:"destination"`
+	Statement   string    `yaml:"statement"`
+	Vars        []string  `yaml:"vars"`
+	Name        string    `yaml:"name"`
+	Expr        string    `yaml:"expr"`
+}
+
+type frame struct {
+	res   *sqlResult
+	index int
+}
+
+type sheetAxis struct {
+	sheet string
+	axis  string
+}
+
+type cellValue struct {
+	value string
+	err   error
+}
+
+type executor struct {
+	ctx              context.Context
+	tx               *sql.Tx
+	template         *excelize.File
+	keep             map[string]bool
+	expressions      map[string]gval.Evaluable
+	sourceSheet      string
+	destinationSheet string
+	frames           []frame
+	// Fetching formulas out of cells is very expensive so we cache them.
+	formulaCache map[sheetAxis]cellValue
+}
+
+type area struct {
+	x1 int
+	y1 int
+	x2 int
+	y2 int
+	mc excelize.MergeCell
+}
+
+func mergeCellToArea(mc excelize.MergeCell) (area, error) {
+	x1, y1, err := excelize.CellNameToCoordinates(mc.GetStartAxis())
+	if err != nil {
+		return area{}, err
+	}
+	x2, y2, err := excelize.CellNameToCoordinates(mc.GetEndAxis())
+	if err != nil {
+		return area{}, err
+	}
+	return area{
+		x1: x1,
+		y1: y1,
+		x2: x2,
+		y2: y2,
+		mc: mc,
+	}, nil
+}
+
+func (a *area) contains(x, y int) bool {
+	return a.x1 <= x && x <= a.x2 && a.y1 <= y && y <= a.y2
+}
+
+func ActionFromFile(filename string) (*Action, error) {
+	f, err := os.Open(filename)
+	if err != nil {
+		return nil, err
+	}
+	defer f.Close()
+	return ActionFromReader(f)
+}
+
+func ActionFromReader(r io.Reader) (*Action, error) {
+	action := new(Action)
+	err := yaml.NewDecoder(bufio.NewReader(r)).Decode(action)
+	return action, err
+}
+
+func (a *Action) Execute(
+	ctx context.Context,
+	tx *sql.Tx,
+	template *excelize.File,
+) error {
+
+	//if f, err := os.Create("cpu.prof"); err == nil {
+	//	pprof.StartCPUProfile(f)
+	//	defer pprof.StopCPUProfile()
+	//}
+
+	e := executor{
+		ctx:          ctx,
+		tx:           tx,
+		template:     template,
+		keep:         map[string]bool{},
+		expressions:  map[string]gval.Evaluable{},
+		formulaCache: map[sheetAxis]cellValue{},
+	}
+
+	oldSheets := template.GetSheetList()
+
+	if err := e.dispatch(a); err != nil {
+		return err
+	}
+
+	for _, sheet := range oldSheets {
+		if !e.keep[sheet] {
+			template.DeleteSheet(sheet)
+		}
+	}
+	return nil
+}
+
+var reused int
+
+func (e *executor) getCellFormula(sheet, axis string) (string, error) {
+	var (
+		k  = sheetAxis{sheet: sheet, axis: axis}
+		v  cellValue
+		ok bool
+	)
+	if v, ok = e.formulaCache[k]; !ok {
+		v.value, v.err = e.template.GetCellFormula(sheet, axis)
+		e.formulaCache[k] = v
+	}
+	return v.value, v.err
+}
+
+func (e *executor) setCellFormula(sheet, axis, formula string) {
+	e.formulaCache[sheetAxis{sheet: sheet, axis: axis}] = cellValue{value: formula}
+	e.template.SetCellFormula(sheet, axis, formula)
+}
+
+func (e *executor) dispatch(action *Action) error {
+	if len(action.Vars) > 0 {
+		e.pushVars(action.Vars)
+		defer e.popFrame()
+	}
+	switch action.Type {
+	case "sheet":
+		return e.sheet(action)
+	case "copy":
+		return e.copy(action)
+	case "select":
+		return e.sel(action)
+	case "assign":
+		return e.assign(action)
+	case "":
+		return e.actions(action)
+	}
+	return fmt.Errorf("unknown type '%s'", action.Type)
+}
+
+func (e *executor) pushVars(vars []string) {
+	e.frames = append(e.frames, frame{
+		res: &sqlResult{
+			columns: vars,
+			rows:    [][]interface{}{make([]interface{}, len(vars))},
+		},
+	})
+}
+
+func (e *executor) popFrame() {
+	n := len(e.frames)
+	e.frames[n-1].res = nil
+	e.frames = e.frames[:n-1]
+}
+
+func (e *executor) assign(action *Action) error {
+	if action.Name == "" {
+		return errors.New("missing name in assign")
+	}
+	if action.Expr == "" {
+		return errors.New("missing expr in assign")
+	}
+
+	for i := len(e.frames) - 1; i >= 0; i-- {
+		fr := &e.frames[i]
+		if idx := fr.res.find(action.Name); idx >= 0 {
+			f, err := e.expr(action.Expr)
+			if err != nil {
+				return err
+			}
+			value, err := f(e.ctx, e.vars())
+			if err != nil {
+				return err
+			}
+			fr.res.rows[fr.index][idx] = value
+			break
+		}
+	}
+	return e.actions(action)
+}
+
+func order(a, b int) (int, int) {
+	if a < b {
+		return a, b
+	}
+	return b, a
+}
+
+func (e *executor) copy(action *Action) error {
+	if n := len(action.Location); !(n == 1 || n == 2) {
+		return fmt.Errorf("length location = %d (expect 1 or 2)",
+			len(action.Location))
+	}
+
+	vars := e.vars()
+
+	var err error
+	expand := func(s string) string {
+		if err == nil {
+			s, err = e.expand(s, vars)
+		}
+		return s
+	}
+	split := func(s string) (int, int) {
+		var x, y int
+		if err == nil {
+			x, y, err = excelize.CellNameToCoordinates(s)
+		}
+		return x, y
+	}
+
+	var location []string
+	if len(action.Location) == 1 {
+		location = []string{action.Location[0], action.Location[0]}
+	} else {
+		location = action.Location
+	}
+
+	var destination string
+	if action.Destination == "" {
+		destination = location[0]
+	} else {
+		destination = action.Destination
+	}
+
+	var (
+		s1       = expand(location[0])
+		s2       = expand(location[1])
+		d1       = expand(destination)
+		sx1, sy1 = split(s1)
+		sx2, sy2 = split(s2)
+		dx1, dy1 = split(d1)
+	)
+	if err != nil {
+		return err
+	}
+	sx1, sx2 = order(sx1, sx2)
+	sy1, sy2 = order(sy1, sy2)
+
+	var areas []area
+
+	//log.Println("merged cells")
+	if mcs, err := e.template.GetMergeCells(e.sourceSheet); err == nil {
+		areas = make([]area, 0, len(mcs))
+		for _, mc := range mcs {
+			if a, err := mergeCellToArea(mc); err == nil {
+				areas = append(areas, a)
+			}
+		}
+	}
+
+	for y, i := sy1, 0; y <= sy2; y, i = y+1, i+1 {
+	nextX:
+		for x, j := sx1, 0; x <= sx2; x, j = x+1, j+1 {
+
+			// check if cell is part of a merged cell
+			for k := range areas {
+				area := &areas[k]
+
+				if area.contains(x, y) {
+					ofsX := x - area.x1
+					ofsY := y - area.y1
+
+					sx := dx1 + j - ofsX
+					sy := dy1 + i - ofsY
+					ex := sx + (area.x2 - area.x1)
+					ey := sy + (area.y2 - area.y1)
+
+					// Copy over attributes
+					for l := 0; l <= area.x2-area.x1; l++ {
+						for m := 0; m <= area.y2-area.y1; m++ {
+							src, err1 := excelize.CoordinatesToCellName(area.x1+l, area.y1+m)
+							dst, err2 := excelize.CoordinatesToCellName(sx+l, sy+m)
+							if err1 != nil || err2 != nil {
+								continue
+							}
+							if s, err := e.template.GetCellStyle(e.sourceSheet, src); err == nil {
+								e.template.SetCellStyle(e.destinationSheet, dst, dst, s)
+							}
+							if s, err := e.getCellFormula(e.sourceSheet, src); err == nil {
+								e.setCellFormula(e.destinationSheet, dst, s)
+							}
+						}
+					}
+
+					dst, err := excelize.CoordinatesToCellName(sx, sy)
+					if err != nil {
+						continue nextX
+					}
+
+					// Copy over expanded text
+					if v, err := e.typedExpand(area.mc.GetCellValue(), vars); err == nil {
+						e.template.SetCellValue(e.destinationSheet, dst, v)
+					}
+
+					// Finally merge the cells
+					if end, err := excelize.CoordinatesToCellName(ex, ey); err == nil {
+						e.template.MergeCell(e.destinationSheet, dst, end)
+					}
+
+					continue nextX
+				}
+			}
+
+			// Regular cell
+
+			src, err := excelize.CoordinatesToCellName(x, y)
+			if err != nil {
+				continue
+			}
+			dst, err := excelize.CoordinatesToCellName(dx1+j, dy1+i)
+			if err != nil {
+				continue
+			}
+
+			cn, err := excelize.ColumnNumberToName(x)
+			if err != nil {
+				continue
+			}
+
+			cw, err := e.template.GetColWidth(e.sourceSheet, cn)
+			if err != nil {
+				continue
+			}
+
+			rh, err := e.template.GetRowHeight(e.sourceSheet, y)
+			if err != nil {
+				continue
+			}
+
+			dc, err := excelize.ColumnNumberToName(dx1 + j)
+			if err != nil {
+				continue
+			}
+
+			if e.template.SetColWidth(e.destinationSheet, dc, dc, cw) != nil {
+				continue
+			}
+
+			if e.template.SetRowHeight(e.destinationSheet, dy1+i, rh) != nil {
+				continue
+			}
+
+			if s, err := e.template.GetCellStyle(e.sourceSheet, src); err == nil {
+				e.template.SetCellStyle(e.destinationSheet, dst, dst, s)
+			}
+			if s, err := e.getCellFormula(e.sourceSheet, src); err == nil {
+				e.setCellFormula(e.destinationSheet, dst, s)
+			}
+			if s, err := e.template.GetCellValue(e.sourceSheet, src); err == nil {
+				if v, err := e.typedExpand(s, vars); err == nil {
+					e.template.SetCellValue(e.destinationSheet, dst, v)
+				}
+			}
+		}
+	}
+
+	return nil
+}
+
+func (e *executor) sel(action *Action) error {
+	vars := e.vars()
+
+	eval := func(x string) (interface{}, error) {
+		f, err := e.expr(x)
+		if err != nil {
+			return nil, err
+		}
+		return f(e.ctx, vars)
+	}
+
+	res, err := query(e.ctx, e.tx, action.Statement, eval)
+	if err != nil {
+		return err
+	}
+
+	e.frames = append(e.frames, frame{res: res})
+	defer e.popFrame()
+
+	for i := range res.rows {
+		e.frames[len(e.frames)-1].index = i
+		if err := e.actions(action); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func (e *executor) actions(action *Action) error {
+	for _, a := range action.Actions {
+		if err := e.dispatch(a); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func (e *executor) sheet(action *Action) error {
+
+	vars := e.vars()
+	source, err := e.expand(action.Source, vars)
+	if err != nil {
+		return err
+	}
+
+	srcIdx := e.template.GetSheetIndex(source)
+	if srcIdx == -1 {
+		return fmt.Errorf("sheet '%s' not found", source)
+	}
+
+	destination := action.Destination
+	if destination == "" { // same as source
+		e.keep[source] = true
+		destination = source
+	} else { // new sheet
+		destination, err = e.expand(destination, vars)
+		if err != nil {
+			return err
+		}
+		dstIdx := e.template.NewSheet(destination)
+		if len(action.Actions) == 0 {
+			// Only copy if there are no explicit instructions.
+			if err := e.template.CopySheet(srcIdx, dstIdx); err != nil {
+				return err
+			}
+		}
+	}
+
+	if len(action.Actions) > 0 {
+		pSrc, pDst := e.sourceSheet, e.destinationSheet
+		defer func() {
+			e.sourceSheet, e.destinationSheet = pSrc, pDst
+		}()
+		e.sourceSheet, e.destinationSheet = source, destination
+		return e.actions(action)
+	}
+
+	// Simple filling
+
+	// "{{" only as a quick filter
+	result, err := e.template.SearchSheet(destination, "{{", true)
+	if err != nil {
+		return err
+	}
+	for _, axis := range result {
+		value, err := e.template.GetCellValue(destination, axis)
+		if err != nil {
+			return err
+		}
+		nvalue, err := e.typedExpand(value, vars)
+		if err != nil {
+			return err
+		}
+		if err := e.template.SetCellValue(destination, axis, nvalue); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func columnToNum(col interface{}) interface{} {
+	var name string
+	switch v := col.(type) {
+	case string:
+		name = v
+	default:
+		name = fmt.Sprintf("%v", col)
+	}
+	num, err := excelize.ColumnNameToNumber(name)
+	if err != nil {
+		log.Printf("error: invalid column name '%v'\n", col)
+		return 1
+	}
+	return num
+}
+
+func asInt(i interface{}) (int, error) {
+	switch v := i.(type) {
+	case int:
+		return v, nil
+	case int8:
+		return int(v), nil
+	case int16:
+		return int(v), nil
+	case int32:
+		return int(v), nil
+	case int64:
+		return int(v), nil
+	case float32:
+		return int(v), nil
+	case float64:
+		return int(v), nil
+	case string:
+		return strconv.Atoi(v)
+	default:
+		return 0, fmt.Errorf("invalid int '%v'", i)
+	}
+}
+
+func coord2cell(ix, iy interface{}) interface{} {
+	x, err := asInt(ix)
+	if err != nil {
+		log.Printf("error: invalid x value: %v\n", err)
+		return "A1"
+	}
+	y, err := asInt(iy)
+	if err != nil {
+		log.Printf("error: invalid y value: %v\n", err)
+		return "A1"
+	}
+
+	cell, err := excelize.CoordinatesToCellName(x, y)
+	if err != nil {
+		log.Printf("error: invalid cell coord (%d, %d)\n", x, y)
+		return "A1"
+	}
+	return cell
+}
+
+var templateLang = gval.Full(
+	gval.Function("column2num", columnToNum),
+	gval.Function("coord2cell", coord2cell),
+)
+
+func (e *executor) expr(x string) (gval.Evaluable, error) {
+	if f := e.expressions[x]; f != nil {
+		return f, nil
+	}
+	f, err := templateLang.NewEvaluable(x)
+	if err != nil {
+		return nil, err
+	}
+	e.expressions[x] = f
+	return f, nil
+}
+
+func (e *executor) vars() map[string]interface{} {
+	vars := map[string]interface{}{}
+	if len(e.frames) > 0 {
+		vars["row_number"] = e.frames[len(e.frames)-1].index
+	}
+	for i := len(e.frames) - 1; i >= 0; i-- {
+		fr := &e.frames[i]
+		for j, n := range fr.res.columns {
+			if _, found := vars[n]; !found {
+				vars[n] = fr.res.rows[fr.index][j]
+			}
+		}
+	}
+	return vars
+}
+
+func (e *executor) expand(
+	str string,
+	vars map[string]interface{},
+) (string, error) {
+
+	var err error
+
+	replace := func(s string) string {
+		if err != nil {
+			return ""
+		}
+		var eval gval.Evaluable
+		if eval, err = e.expr(strings.TrimSpace(s)); err != nil {
+			return ""
+		}
+		s, err = eval.EvalString(e.ctx, vars)
+		if err != nil {
+			log.Printf("error: '%s' '%s' %v\n", str, s, err)
+		}
+		return s
+	}
+
+	str = handlebars(str, replace)
+	return str, err
+}
+
+func (e *executor) typedExpand(
+	str string,
+	vars map[string]interface{},
+) (interface{}, error) {
+
+	var (
+		err      error
+		repCount int
+		last     interface{}
+	)
+
+	replace := func(s string) string {
+		if err != nil {
+			return ""
+		}
+		var eval gval.Evaluable
+		if eval, err = e.expr(strings.TrimSpace(s)); err != nil {
+			return ""
+		}
+		repCount++
+		last, err = eval(e.ctx, vars)
+		if err != nil {
+			log.Printf("error: '%s' '%s' %v\n", str, s, err)
+		}
+		return fmt.Sprintf("%v", last)
+	}
+
+	nstr := handlebars(str, replace)
+
+	if err != nil {
+		return nil, err
+	}
+
+	if repCount == 1 &&
+		strings.HasPrefix(str, "{{") &&
+		strings.HasSuffix(str, "}}") {
+		return last, nil
+	}
+	return nstr, nil
+}