| // Copyright 2013 Frank Schroeder. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package goproperties |
| |
| import ( |
| "fmt" |
| "io" |
| "log" |
| "strings" |
| "unicode/utf8" |
| ) |
| |
| type Properties struct { |
| // Pre-/Postfix for property expansion. |
| Prefix string |
| Postfix string |
| |
| m map[string]string |
| } |
| |
| // NewProperties creates a new Properties struct with the default |
| // configuration for "${key}" expressions. |
| func NewProperties() *Properties { |
| return &Properties{ |
| Prefix: "${", |
| Postfix: "}", |
| m: make(map[string]string), |
| } |
| } |
| |
| // Get returns the expanded value for the given key if exists. Otherwise, ok is false. |
| func (p *Properties) Get(key string) (value string, ok bool) { |
| v, ok := p.m[key] |
| if !ok { |
| return "", false |
| } |
| |
| expanded, err := p.expand(v) |
| |
| // if there is an error then this is a format exception which we just log |
| // and return the input unchanged. |
| if err != nil { |
| log.Printf("%s in %q", err, key+" = "+v) |
| return v, true |
| } |
| |
| return expanded, true |
| } |
| |
| // GetDefault returns the expanded value for the given key if exists or the default value otherwise. |
| func (p *Properties) GetDefault(key, defaultValue string) (value string) { |
| if v, ok := p.Get(key); ok { |
| return v |
| } |
| return defaultValue |
| } |
| |
| // Len returns the number of keys. |
| func (p *Properties) Len() int { |
| return len(p.m) |
| } |
| |
| // Dump returns a string of all unexpanded 'key = value' pairs. |
| func (p *Properties) Dump() string { |
| var s string |
| for key, value := range p.m { |
| s = fmt.Sprintf("%s%s = %s\n", s, key, value) |
| } |
| return s |
| } |
| |
| // String returns a string of all expanded 'key = value' pairs. |
| func (p *Properties) String() string { |
| var s string |
| for key, _ := range p.m { |
| value, _ := p.Get(key) |
| s = fmt.Sprintf("%s%s = %s\n", s, key, value) |
| } |
| return s |
| } |
| |
| // Write writes all unexpanded 'key = value' pairs as ISO-8859-1 to the given writer. |
| func (p *Properties) Write(w io.Writer) (int, error) { |
| total := 0 |
| for key, value := range p.m { |
| s := fmt.Sprintf("%s = %s\n", encode(key, " :"), encode(value, "")) |
| n, err := w.Write([]byte(s)) |
| if err != nil { |
| return total, err |
| } |
| total += n |
| } |
| return total, nil |
| } |
| |
| // expand recursively expands expressions of '(prefix)key(postfix)' to their corresponding values. |
| // The function keeps track of the keys that were already expanded and stops if it |
| // detects a circular reference or a malformed expression. |
| func (p *Properties) expand(input string) (string, error) { |
| // no pre/postfix -> nothing to expand |
| if p.Prefix == "" && p.Postfix == "" { |
| return input, nil |
| } |
| |
| return p.doExpand(input, make(map[string]bool)) |
| } |
| |
| func (p *Properties) doExpand(s string, keys map[string]bool) (string, error) { |
| a := strings.Index(s, p.Prefix) |
| if a == -1 { |
| return s, nil |
| } |
| |
| b := strings.Index(s[a:], p.Postfix) |
| if b == -1 { |
| return "", fmt.Errorf("Malformed expression") |
| } |
| |
| key := s[a+len(p.Prefix) : b-len(p.Postfix)+1] |
| |
| if _, ok := keys[key]; ok { |
| return "", fmt.Errorf("Circular reference") |
| } |
| |
| val, ok := p.m[key] |
| if !ok { |
| val = "" |
| } |
| |
| // remember that we've seen the key |
| keys[key] = true |
| |
| return p.doExpand(s[:a]+val+s[b+1:], keys) |
| } |
| |
| // encode encodes a UTF-8 string to ISO-8859-1 and escapes some characters. |
| func encode(s string, special string) string { |
| var r rune |
| var w int |
| var v string |
| for pos := 0; pos < len(s); { |
| switch r, w = utf8.DecodeRuneInString(s[pos:]); { |
| case r < 1<<8: // single byte rune -> escape special chars only |
| v += escape(r, special) |
| case r < 1<<16: // two byte rune -> unicode literal |
| v += fmt.Sprintf("\\u%04x", r) |
| default: // more than two bytes per rune -> can't encode |
| v += "?" |
| } |
| pos += w |
| } |
| return v |
| } |
| |
| func escape(r rune, special string) string { |
| switch r { |
| case '\f': |
| return "\\f" |
| case '\n': |
| return "\\n" |
| case '\r': |
| return "\\r" |
| case '\t': |
| return "\\t" |
| default: |
| if strings.ContainsRune(special, r) { |
| return "\\" + string(r) |
| } |
| return string(r) |
| } |
| } |