blob: 4b3b5d1eb5daa9a035807b453255ad7b505cc781 [file] [log] [blame]
// 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)
}
}