package webutility import ( "database/sql" "encoding/json" "errors" "fmt" "io" "net/http" "os" "strings" "sync" "time" "git.to-net.rs/marko.tikvic/gologger" ) var ( mu = &sync.Mutex{} metadata = make(map[string]Payload) updateQue = make(map[string][]byte) metadataDB *sql.DB activeProject string inited bool driver string logger *gologger.Logger ) type LangMap map[string]map[string]string type Field struct { Parameter string `json:"param"` Type string `json:"type"` Visible bool `json:"visible"` Editable bool `json:"editable"` } type CorrelationField struct { Result string `json:"result"` Elements []string `json:"elements"` Type string `json:"type"` } type Translation struct { Language string `json:"language"` FieldsLabels map[string]string `json:"fieldsLabels"` } // output type PaginationLinks struct { Base string `json:"base"` Next string `json:"next"` Prev string `json:"prev"` Self string `json:"self"` } // input type PaginationParameters struct { URL string `json:"-"` Offset int64 `json:"offset"` Limit int64 `json:"limit"` SortBy string `json:"sortBy"` Order string `json:"order"` } // TODO(marko) func GetPaginationParameters(req *http.Request) (p PaginationParameters) { return p } // TODO(marko) func (p *PaginationParameters) paginationLinks() (links PaginationLinks) { return links } type Payload struct { Method string `json:"method"` Params map[string]string `json:"params"` Lang []Translation `json:"lang"` Fields []Field `json:"fields"` Correlations []CorrelationField `json:"correlationFields"` IdField string `json:"idField"` // Pagination Count int64 `json:"count"` Total int64 `json:"total"` Links PaginationLinks `json:"_links"` // Data holds JSON payload. It can't be used for itteration. Data interface{} `json:"data"` } func (p *Payload) addLang(code string, labels map[string]string) { t := Translation{ Language: code, FieldsLabels: labels, } p.Lang = append(p.Lang, t) } func (p *Payload) SetData(data interface{}) { p.Data = data } func (p *Payload) SetPaginationInfo(count, total int64, params PaginationParameters) { p.Count = count p.Total = total p.Links = params.paginationLinks() } // NewPayload returs a payload sceleton for entity described with key. func NewPayload(r *http.Request, key string) Payload { p := metadata[key] p.Method = r.Method + " " + r.RequestURI return p } // DecodeJSON decodes JSON data from r to v. // Returns an error if it fails. func DecodeJSON(r io.Reader, v interface{}) error { return json.NewDecoder(r).Decode(v) } // InitPayloadsMetadata loads all payloads' information into 'metadata' variable. func InitPayloadsMetadata(drv string, db *sql.DB, project string) error { var err error if drv != "ora" && drv != "mysql" { err = errors.New("driver not supported") return err } driver = drv metadataDB = db activeProject = project logger, err = gologger.New("metadata", gologger.MaxLogSize100KB) if err != nil { fmt.Printf("webutility: %s\n", err.Error()) } mu.Lock() defer mu.Unlock() err = initMetadata(project) if err != nil { return err } inited = true return nil } func EnableHotloading(interval int) { if interval > 0 { go hotload(interval) } } func GetMetadataForAllEntities() map[string]Payload { return metadata } func GetMetadataForEntity(t string) (Payload, bool) { p, ok := metadata[t] return p, ok } func QueEntityModelUpdate(entityType string, v interface{}) { updateQue[entityType], _ = json.Marshal(v) } func UpdateEntityModels(command string) (total, upd, add int, err error) { if command != "force" && command != "missing" { return total, 0, 0, errors.New("webutility: unknown command: " + command) } if !inited { return 0, 0, 0, errors.New("webutility: metadata not initialized but update was tried.") } total = len(updateQue) toUpdate := make([]string, 0) toAdd := make([]string, 0) for k, _ := range updateQue { if _, exists := metadata[k]; exists { if command == "force" { toUpdate = append(toUpdate, k) } } else { toAdd = append(toAdd, k) } } var uStmt *sql.Stmt if driver == "ora" { uStmt, err = metadataDB.Prepare("update entities set entity_model = :1 where entity_type = :2") if err != nil { logger.Trace(err.Error()) return } } else if driver == "mysql" { uStmt, err = metadataDB.Prepare("update entities set entity_model = ? where entity_type = ?") if err != nil { logger.Trace(err.Error()) return } } for _, k := range toUpdate { _, err = uStmt.Exec(string(updateQue[k]), k) if err != nil { logger.Trace(err.Error()) return } upd++ } blankPayload, _ := json.Marshal(Payload{}) var iStmt *sql.Stmt if driver == "ora" { iStmt, err = metadataDB.Prepare("insert into entities(projekat, metadata, entity_type, entity_model) values(:1, :2, :3, :4)") if err != nil { logger.Trace(err.Error()) return } } else if driver == "mysql" { iStmt, err = metadataDB.Prepare("insert into entities(projekat, metadata, entity_type, entity_model) values(?, ?, ?, ?)") if err != nil { logger.Trace(err.Error()) return } } for _, k := range toAdd { _, err = iStmt.Exec(activeProject, string(blankPayload), k, string(updateQue[k])) if err != nil { logger.Trace(err.Error()) return } metadata[k] = Payload{} add++ } return total, upd, add, nil } func initMetadata(project string) error { rows, err := metadataDB.Query(`select entity_type, metadata from entities where projekat = ` + fmt.Sprintf("'%s'", project)) if err != nil { return err } defer rows.Close() if len(metadata) > 0 { metadata = nil } metadata = make(map[string]Payload) for rows.Next() { var name, load string rows.Scan(&name, &load) p := Payload{} err := json.Unmarshal([]byte(load), &p) if err != nil { logger.Log("webutility: couldn't init: '%s' metadata: %s:\n%s\n", name, err.Error(), load) } else { metadata[name] = p } } return nil } // TODO(marko): // // Currently supports only one hardcoded language... // // // // // // Metadata file ecpected format: // // [ payload A identifier ] // key1 : value1 // key2 : value2 // ... // [ payload B identifier ] // key1 : value1 // key2 : value2 // ... func LoadMetadataFromFile(path string) error { lines, err := ReadFileLines(path) if err != nil { return err } metadata = make(map[string]Payload) var name string for i, l := range lines { // skip empty lines if l = trimSpaces(l); len(l) == 0 { continue } if isWrappedWith(l, "[", "]") { name = strings.Trim(l, "[]") p := Payload{} p.addLang("sr", make(map[string]string)) metadata[name] = p continue } if name == "" { return fmt.Errorf("webutility: LoadMetadataFromFile: error on line %d: [no header] [%s]\n", i+1, l) } parts := strings.Split(l, ":") if len(parts) != 2 { return fmt.Errorf("webutility: LoadMetadataFromFile: error on line %d: [invalid format] [%s]\n", i+1, l) } k := trimSpaces(parts[0]) v := trimSpaces(parts[1]) if v != "-" { metadata[name].Lang[0].FieldsLabels[k] = v } } return nil } func isWrappedWith(src, begin, end string) bool { return strings.HasPrefix(src, begin) && strings.HasSuffix(src, end) } func trimSpaces(s string) string { return strings.TrimSpace(s) } // TODO(marko): Move to separate package func ReadFileLines(path string) ([]string, error) { f, err := os.Open(path) if err != nil { return nil, err } defer f.Close() var s strings.Builder if _, err = io.Copy(&s, f); err != nil { return nil, err } lines := strings.Split(s.String(), "\n") return lines, nil } func hotload(n int) { entityScan := make(map[string]int64) firstCheck := true for { time.Sleep(time.Duration(n) * time.Second) rows, err := metadataDB.Query(`select ora_rowscn, entity_type from entities where projekat = ` + fmt.Sprintf("'%s'", activeProject)) if err != nil { logger.Log("webutility: hotload failed: %v\n", err) time.Sleep(time.Duration(n) * time.Second) continue } var toRefresh []string for rows.Next() { var scanID int64 var entity string rows.Scan(&scanID, &entity) oldID, ok := entityScan[entity] if !ok || oldID != scanID { entityScan[entity] = scanID toRefresh = append(toRefresh, entity) } } rows.Close() if rows.Err() != nil { logger.Log("webutility: hotload rset error: %v\n", rows.Err()) time.Sleep(time.Duration(n) * time.Second) continue } if len(toRefresh) > 0 && !firstCheck { mu.Lock() refreshMetadata(toRefresh) mu.Unlock() } if firstCheck { firstCheck = false } } } func refreshMetadata(entities []string) { for _, e := range entities { fmt.Printf("refreshing %s\n", e) rows, err := metadataDB.Query(`select metadata from entities where projekat = ` + fmt.Sprintf("'%s'", activeProject) + ` and entity_type = ` + fmt.Sprintf("'%s'", e)) if err != nil { logger.Log("webutility: refresh: prep: %v\n", err) rows.Close() continue } for rows.Next() { var load string rows.Scan(&load) p := Payload{} err := json.Unmarshal([]byte(load), &p) if err != nil { logger.Log("webutility: couldn't refresh: '%s' metadata: %s\n%s\n", e, err.Error(), load) } else { metadata[e] = p } } rows.Close() } } /* func ModifyMetadataForEntity(entityType string, p *Payload) error { md, err := json.Marshal(*p) if err != nil { return err } mu.Lock() defer mu.Unlock() _, err = metadataDB.PrepAndExe(`update entities set metadata = :1 where projekat = :2 and entity_type = :3`, string(md), activeProject, entityType) if err != nil { return err } return nil } func DeleteEntityModel(entityType string) error { _, err := metadataDB.PrepAndExe("delete from entities where entity_type = :1", entityType) if err == nil { mu.Lock() delete(metadata, entityType) mu.Unlock() } return err } */