<commit_before>package main

import (
	"bytes"
	"crypto/rand"
	"crypto/rsa"
	"encoding/base64"
	"github.com/realglobe-Inc/edo/util"
	"github.com/realglobe-Inc/go-lib-rg/erro"
	"io"
	"io/ioutil"
	"net"
	"net/http"
	"net/url"
	"strings"
	"time"
)

const (
	headerTaId       = "X-Edo-Ta-Id"
	headerTaToken    = "X-Edo-Ta-Token"
	headerTaTokenSig = "X-Edo-Ta-Token-Sign"
	headerHashFunc   = "X-Edo-Hash-Function"

	headerAccProxErr = "X-Edo-Access-Proxy-Error"

	headerTaAuthErr = "X-Edo-Ta-Auth-Error"

	cookieTaSess = "X-Edo-Ta-Session"
)

// Web プロキシ。
func proxyApi(sys *system, w http.ResponseWriter, r *http.Request) error {

	if !strings.HasPrefix(r.RequestURI, "http://") && !strings.HasPrefix(r.RequestURI, "https://") {
		return erro.Wrap(util.NewHttpStatusError(http.StatusBadRequest, "no scheme in request uri", nil))
	}

	taId := r.Header.Get(headerTaId)
	if taId == "" {
		taId = sys.taId
	}

	sess, _, err := sys.session(uriBase(r.URL), taId, nil)
	if err != nil {
		return erro.Wrap(err)
	}

	if sess != nil {
		// セッション確立済み。
		log.Debug("authenticated session is exist")
		return forward(sys, w, r, taId, sess)
	} else {
		// セッション未確立。
		log.Debug("session is not exist")
		return startSession(sys, w, r, taId)
	}
}

// 転送する。
func forward(sys *system, w http.ResponseWriter, r *http.Request, taId string, sess *session) error {
	r.AddCookie(&http.Cookie{Name: cookieTaSess, Value: sess.id})
	r.RequestURI = ""

	////////////////////////////////////////////////////////////
	util.LogRequest(r, true)
	////////////////////////////////////////////////////////////
	resp, err := sess.cli.Do(r)
	if err != nil {
		err = erro.Wrap(err)
		switch erro.Unwrap(err).(type) {
		case *net.OpError:
			return erro.Wrap(util.NewHttpStatusError(http.StatusNotFound, "cannot connect "+uriBase(r.URL), err))
		default:
			return err
		}
	}
	defer resp.Body.Close()
	////////////////////////////////////////////////////////////
	util.LogResponse(resp, true)
	////////////////////////////////////////////////////////////

	log.Debug("forwarded")

	if resp.StatusCode == http.StatusUnauthorized && resp.Header.Get(headerTaAuthErr) != "" {
		// edo-auth で 401 Unauthorized なら、タイミングの問題なので startSession からやり直す。
		// 古いセッションは上書きされるので消す必要無し。
		return startSession(sys, w, r, taId)
	}

	return copyResponse(resp, w)
}

// セッション開始。
func startSession(sys *system, w http.ResponseWriter, r *http.Request, taId string) error {

	cli := &http.Client{}

	body, err := ioutil.ReadAll(r.Body)
	if err != nil {
		return erro.Wrap(err)
	}

	r.RequestURI = ""
	r.Body = ioutil.NopCloser(bytes.NewReader(body))

	////////////////////////////////////////////////////////////
	util.LogRequest(r, true)
	////////////////////////////////////////////////////////////
	resp, err := cli.Do(r)
	if err != nil {
		err = erro.Wrap(err)
		switch erro.Unwrap(err).(type) {
		case *net.OpError:
			return erro.Wrap(util.NewHttpStatusError(http.StatusNotFound, "cannot connect "+uriBase(r.URL), err))
		default:
			return err
		}
	}
	defer resp.Body.Close()
	////////////////////////////////////////////////////////////
	util.LogResponse(resp, true)
	////////////////////////////////////////////////////////////

	log.Debug("sent raw request")

	if resp.Header.Get(headerTaAuthErr) == "" || resp.StatusCode != http.StatusUnauthorized {
		// 相手側が TA 認証を必要としていなかったのかもしれない。
		return copyResponse(resp, w)
	}

	// 相手側 TA も認証始めた。
	log.Debug("authentication started")

	sess, sessToken := parseSession(resp)
	if sess == nil {
		return erro.Wrap(util.NewHttpStatusError(http.StatusForbidden, "no cookie "+cookieTaSess, nil))
	} else if sessToken == "" {
		return erro.Wrap(util.NewHttpStatusError(http.StatusForbidden, "no header field "+headerTaToken, nil))
	}

	expiDate := getExpirationDate(sess)

	// 認証用データが揃ってた。
	log.Debug("authentication data was found")

	priKey, _, err := sys.privateKey(taId, nil)
	if err != nil {
		return erro.Wrap(err)
	} else if priKey == nil {
		return erro.Wrap(util.NewHttpStatusError(http.StatusForbidden, "no private key of "+taId, nil))
	}

	// 秘密鍵を用意できた。
	log.Debug("private key of " + taId + " is exist")

	hashName := r.Header.Get(headerHashFunc)
	if hashName == "" {
		hashName = sys.hashName
	}

	tokenSign, err := sign(priKey, hashName, sessToken)
	if err != nil {
		return erro.Wrap(err)
	}

	// 署名できた。
	log.Debug("signed")

	r.AddCookie(&http.Cookie{Name: cookieTaSess, Value: sess.Value})
	r.Header.Set(headerTaId, taId)
	r.Header.Set(headerTaTokenSig, tokenSign)
	r.Header.Set(headerHashFunc, hashName)
	r.RequestURI = ""
	r.Body = ioutil.NopCloser(bytes.NewReader(body))

	////////////////////////////////////////////////////////////
	util.LogRequest(r, true)
	////////////////////////////////////////////////////////////
	resp, err = cli.Do(r)
	if err != nil {
		err = erro.Wrap(err)
		switch erro.Unwrap(err).(type) {
		case *net.OpError:
			return erro.Wrap(util.NewHttpStatusError(http.StatusNotFound, "cannot connect "+uriBase(r.URL), err))
		default:
			return err
		}
	}
	defer resp.Body.Close()
	////////////////////////////////////////////////////////////
	util.LogResponse(resp, true)
	////////////////////////////////////////////////////////////

	// 認証された。
	log.Debug("authentication finished")

	if resp.Header.Get(headerTaAuthErr) == "" {
		// セッションを保存。
		if _, err := sys.addSession(&session{id: sess.Value, uri: uriBase(r.URL), taId: taId, cli: cli}, expiDate); err != nil {
			err = erro.Wrap(err)
			log.Err(erro.Unwrap(err))
			log.Debug(err)
		} else {
			log.Debug("session was saved")
		}
	}

	return copyResponse(resp, w)
}

// クエリパラメータ等を除いた URL を得る。
func uriBase(url *url.URL) string {
	return url.Scheme + "://" + url.Host + url.Path
}

// 相手側 TA の認証開始レスポンスから必要情報を抜き出す。
func parseSession(resp *http.Response) (sess *http.Cookie, sessToken string) {
	for _, cookie := range resp.Cookies() {
		if cookie.Name == cookieTaSess {
			sess = cookie
			break
		}
	}

	return sess, resp.Header.Get(headerTaToken)
}

// 相手側 TA からのレスポンスをリクエスト元へのレスポンスに写す。
func copyResponse(resp *http.Response, w http.ResponseWriter) error {
	// ヘッダフィールドのコピー。
	for key, values := range resp.Header {
		for _, value := range values {
			w.Header().Add(key, value)
		}
	}

	// ステータスのコピー。
	w.WriteHeader(resp.StatusCode)

	// ボディのコピー。
	if _, err := io.Copy(w, resp.Body); err != nil {
		return erro.Wrap(err)
	}

	return nil
}

// 相手側 TA からのお題に署名する。
func sign(priKey *rsa.PrivateKey, hashName, token string) (string, error) {
	hash, err := util.ParseHashFunction(hashName)
	if err != nil {
		return "", erro.Wrap(err)
	}

	h := hash.New()
	h.Write([]byte(token))
	buff, err := rsa.SignPKCS1v15(rand.Reader, priKey, hash, h.Sum(nil))
	if err != nil {
		return "", erro.Wrap(err)
	}

	return base64.StdEncoding.EncodeToString(buff), nil
}

// 相手側 TA が提示したセッションの有効期限を読み取る。
func getExpirationDate(sess *http.Cookie) (expiDate time.Time) {
	if sess.MaxAge != 0 {
		return time.Now().Add(time.Duration(sess.MaxAge))
	} else {
		return sess.Expires
	}
}
<commit_msg>通信先の問題かどうか判定する関数を追加<commit_after>package main

import (
	"bytes"
	"crypto/rand"
	"crypto/rsa"
	"encoding/base64"
	"github.com/realglobe-Inc/edo/util"
	"github.com/realglobe-Inc/go-lib-rg/erro"
	"io"
	"io/ioutil"
	"net"
	"net/http"
	"net/url"
	"strings"
	"time"
)

const (
	headerTaId       = "X-Edo-Ta-Id"
	headerTaToken    = "X-Edo-Ta-Token"
	headerTaTokenSig = "X-Edo-Ta-Token-Sign"
	headerHashFunc   = "X-Edo-Hash-Function"

	headerAccProxErr = "X-Edo-Access-Proxy-Error"

	headerTaAuthErr = "X-Edo-Ta-Auth-Error"

	cookieTaSess = "X-Edo-Ta-Session"
)

// Web プロキシ。
func proxyApi(sys *system, w http.ResponseWriter, r *http.Request) error {

	if !strings.HasPrefix(r.RequestURI, "http://") && !strings.HasPrefix(r.RequestURI, "https://") {
		return erro.Wrap(util.NewHttpStatusError(http.StatusBadRequest, "no scheme in request uri", nil))
	}

	taId := r.Header.Get(headerTaId)
	if taId == "" {
		taId = sys.taId
	}

	sess, _, err := sys.session(uriBase(r.URL), taId, nil)
	if err != nil {
		return erro.Wrap(err)
	}

	if sess != nil {
		// セッション確立済み。
		log.Debug("authenticated session is exist")
		return forward(sys, w, r, taId, sess)
	} else {
		// セッション未確立。
		log.Debug("session is not exist")
		return startSession(sys, w, r, taId)
	}
}

// 転送する。
func forward(sys *system, w http.ResponseWriter, r *http.Request, taId string, sess *session) error {
	r.AddCookie(&http.Cookie{Name: cookieTaSess, Value: sess.id})
	r.RequestURI = ""

	////////////////////////////////////////////////////////////
	util.LogRequest(r, true)
	////////////////////////////////////////////////////////////
	resp, err := sess.cli.Do(r)
	if err != nil {
		err = erro.Wrap(err)
		switch erro.Unwrap(err).(type) {
		case *net.OpError:
			return erro.Wrap(util.NewHttpStatusError(http.StatusNotFound, "cannot connect "+uriBase(r.URL), err))
		default:
			return err
		}
	}
	defer resp.Body.Close()
	////////////////////////////////////////////////////////////
	util.LogResponse(resp, true)
	////////////////////////////////////////////////////////////

	log.Debug("forwarded")

	if resp.StatusCode == http.StatusUnauthorized && resp.Header.Get(headerTaAuthErr) != "" {
		// edo-auth で 401 Unauthorized なら、タイミングの問題なので startSession からやり直す。
		// 古いセッションは上書きされるので消す必要無し。
		return startSession(sys, w, r, taId)
	}

	return copyResponse(resp, w)
}

// セッション開始。
func startSession(sys *system, w http.ResponseWriter, r *http.Request, taId string) error {

	cli := &http.Client{}

	body, err := ioutil.ReadAll(r.Body)
	if err != nil {
		return erro.Wrap(err)
	}

	r.RequestURI = ""
	r.Body = ioutil.NopCloser(bytes.NewReader(body))

	////////////////////////////////////////////////////////////
	util.LogRequest(r, true)
	////////////////////////////////////////////////////////////
	resp, err := cli.Do(r)
	if err != nil {
		err = erro.Wrap(err)
		switch erro.Unwrap(err).(type) {
		case *net.OpError:
			return erro.Wrap(util.NewHttpStatusError(http.StatusNotFound, "cannot connect "+uriBase(r.URL), err))
		default:
			return err
		}
	}
	defer resp.Body.Close()
	////////////////////////////////////////////////////////////
	util.LogResponse(resp, true)
	////////////////////////////////////////////////////////////

	log.Debug("sent raw request")

	if resp.Header.Get(headerTaAuthErr) == "" || resp.StatusCode != http.StatusUnauthorized {
		// 相手側が TA 認証を必要としていなかったのかもしれない。
		return copyResponse(resp, w)
	}

	// 相手側 TA も認証始めた。
	log.Debug("authentication started")

	sess, sessToken := parseSession(resp)
	if sess == nil {
		return erro.Wrap(util.NewHttpStatusError(http.StatusForbidden, "no cookie "+cookieTaSess, nil))
	} else if sessToken == "" {
		return erro.Wrap(util.NewHttpStatusError(http.StatusForbidden, "no header field "+headerTaToken, nil))
	}

	expiDate := getExpirationDate(sess)

	// 認証用データが揃ってた。
	log.Debug("authentication data was found")

	priKey, _, err := sys.privateKey(taId, nil)
	if err != nil {
		return erro.Wrap(err)
	} else if priKey == nil {
		return erro.Wrap(util.NewHttpStatusError(http.StatusForbidden, "no private key of "+taId, nil))
	}

	// 秘密鍵を用意できた。
	log.Debug("private key of " + taId + " is exist")

	hashName := r.Header.Get(headerHashFunc)
	if hashName == "" {
		hashName = sys.hashName
	}

	tokenSign, err := sign(priKey, hashName, sessToken)
	if err != nil {
		return erro.Wrap(err)
	}

	// 署名できた。
	log.Debug("signed")

	r.AddCookie(&http.Cookie{Name: cookieTaSess, Value: sess.Value})
	r.Header.Set(headerTaId, taId)
	r.Header.Set(headerTaTokenSig, tokenSign)
	r.Header.Set(headerHashFunc, hashName)
	r.RequestURI = ""
	r.Body = ioutil.NopCloser(bytes.NewReader(body))

	////////////////////////////////////////////////////////////
	util.LogRequest(r, true)
	////////////////////////////////////////////////////////////
	resp, err = cli.Do(r)
	if err != nil {
		err = erro.Wrap(err)
		switch erro.Unwrap(err).(type) {
		case *net.OpError:
			return erro.Wrap(util.NewHttpStatusError(http.StatusNotFound, "cannot connect "+uriBase(r.URL), err))
		default:
			return err
		}
	}
	defer resp.Body.Close()
	////////////////////////////////////////////////////////////
	util.LogResponse(resp, true)
	////////////////////////////////////////////////////////////

	// 認証された。
	log.Debug("authentication finished")

	if resp.Header.Get(headerTaAuthErr) == "" {
		// セッションを保存。
		if _, err := sys.addSession(&session{id: sess.Value, uri: uriBase(r.URL), taId: taId, cli: cli}, expiDate); err != nil {
			err = erro.Wrap(err)
			log.Err(erro.Unwrap(err))
			log.Debug(err)
		} else {
			log.Debug("session was saved")
		}
	}

	return copyResponse(resp, w)
}

// クエリパラメータ等を除いた URL を得る。
func uriBase(url *url.URL) string {
	return url.Scheme + "://" + url.Host + url.Path
}

// プロキシ先がおかしいかどうか。
func isDestinationError(err error) bool {
	for {
		switch e := erro.Unwrap(err).(type) {
		case *net.OpError:
			return true
		case *url.Error:
			if e.Err != nil {
				err = e.Err
			} else {
				return false
			}
		case *erro.Tracer:
			err = e.Cause()
		case *util.HttpStatusError:
			if e.Cause() != nil {
				err = e.Cause()
			} else {
				return false
			}
		default:
			return false
		}
	}
}

// 相手側 TA の認証開始レスポンスから必要情報を抜き出す。
func parseSession(resp *http.Response) (sess *http.Cookie, sessToken string) {
	for _, cookie := range resp.Cookies() {
		if cookie.Name == cookieTaSess {
			sess = cookie
			break
		}
	}

	return sess, resp.Header.Get(headerTaToken)
}

// 相手側 TA からのレスポンスをリクエスト元へのレスポンスに写す。
func copyResponse(resp *http.Response, w http.ResponseWriter) error {
	// ヘッダフィールドのコピー。
	for key, values := range resp.Header {
		for _, value := range values {
			w.Header().Add(key, value)
		}
	}

	// ステータスのコピー。
	w.WriteHeader(resp.StatusCode)

	// ボディのコピー。
	if _, err := io.Copy(w, resp.Body); err != nil {
		return erro.Wrap(err)
	}

	return nil
}

// 相手側 TA からのお題に署名する。
func sign(priKey *rsa.PrivateKey, hashName, token string) (string, error) {
	hash, err := util.ParseHashFunction(hashName)
	if err != nil {
		return "", erro.Wrap(err)
	}

	h := hash.New()
	h.Write([]byte(token))
	buff, err := rsa.SignPKCS1v15(rand.Reader, priKey, hash, h.Sum(nil))
	if err != nil {
		return "", erro.Wrap(err)
	}

	return base64.StdEncoding.EncodeToString(buff), nil
}

// 相手側 TA が提示したセッションの有効期限を読み取る。
func getExpirationDate(sess *http.Cookie) (expiDate time.Time) {
	if sess.MaxAge != 0 {
		return time.Now().Add(time.Duration(sess.MaxAge))
	} else {
		return sess.Expires
	}
}
<|endoftext|>


<commit_before>package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"strconv"
)

type RestClient struct {
	client *http.Client
	cookie interface{}
}

func (c *RestClient) Do(req *http.Request) (mresp map[string]interface{}) {
	req.Header.Set("Content-Encoding", "application/json")
	if c.cookie != nil {
		req.AddCookie(c.cookie.(*http.Cookie))
	}

	resp, err := c.client.Do(req)
	if err != nil {
		log.Println("%v", err)
	}

	defer resp.Body.Close()
	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		log.Println("%v", err)
	}
	json.Unmarshal(body, &mresp)
	return
}

type SyncGatewayClient struct {
	baseURI      string
	baseAdminURI string
	client       *RestClient
}

func (c *SyncGatewayClient) Init(hostname, db string) {
	c.baseURI = fmt.Sprintf("http://%s:4984/%s", hostname, db)
	c.baseAdminURI = fmt.Sprintf("http://%s:4985/%s", hostname, db)
	c.client = &RestClient{&http.Client{}, nil}
}

func (c *SyncGatewayClient) AddCookie(cookie *http.Cookie) {
	c.client.cookie = cookie
}

func (c *SyncGatewayClient) PutSingleDoc(docid string, doc Doc) {
	b, _ := json.Marshal(doc)
	j := bytes.NewReader(b)
	uri := fmt.Sprintf("%s/%s?new_edits=true", c.baseURI, docid)
	req, _ := http.NewRequest("PUT", uri, j)
	c.client.Do(req)
}

func (c *SyncGatewayClient) GetBulkDocs(docs map[string][]map[string]string) {
	b, _ := json.Marshal(docs)
	j := bytes.NewReader(b)
	uri := fmt.Sprintf("%s/_bulk_get", c.baseURI)
	req, _ := http.NewRequest("POST", uri, j)

	c.client.Do(req)
}

func (c *SyncGatewayClient) GetSingleDoc(docid string) {
	uri := fmt.Sprintf("%s/%s", c.baseURI, docid)
	req, _ := http.NewRequest("GET", uri, nil)
	c.client.Do(req)
}

func (c *SyncGatewayClient) GetLastSeq() string {
	uri := fmt.Sprintf("%s/", c.baseURI)
	req, _ := http.NewRequest("GET", uri, nil)

	resp := c.client.Do(req)
	return strconv.FormatFloat(resp["committed_update_seq"].(float64), 'f', 0, 64)
}

func (c *SyncGatewayClient) GetChangesFeed(since string) map[string]interface{} {
	uri := fmt.Sprintf("%s/_changes?limit=10&feed=longpoll&since=%s", c.baseURI, since)
	req, _ := http.NewRequest("GET", uri, nil)

	return c.client.Do(req)
}

func (c *SyncGatewayClient) AddUser(name string, auth UserAuth) {
	b, _ := json.Marshal(auth)
	j := bytes.NewReader(b)
	uri := fmt.Sprintf("%s/_user/%s", c.baseAdminURI, name)
	req, _ := http.NewRequest("PUT", uri, j)

	log.Printf("Adding user %s", name)
	c.client.Do(req)
}

func (c *SyncGatewayClient) CreateSession(name string, session Session) http.Cookie {
	b, _ := json.Marshal(session)
	j := bytes.NewReader(b)
	uri := fmt.Sprintf("%s/_session", c.baseAdminURI)
	req, _ := http.NewRequest("POST", uri, j)

	resp := c.client.Do(req)
	return http.Cookie{
		Name:  resp["cookie_name"].(string),
		Value: resp["session_id"].(string),
	}
}
<commit_msg>fix error logging<commit_after>package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"strconv"
)

type RestClient struct {
	client *http.Client
	cookie interface{}
}

func (c *RestClient) Do(req *http.Request) (mresp map[string]interface{}) {
	req.Header.Set("Content-Encoding", "application/json")
	if c.cookie != nil {
		req.AddCookie(c.cookie.(*http.Cookie))
	}

	resp, err := c.client.Do(req)
	if err != nil {
		log.Printf("%v", err)
	}

	defer resp.Body.Close()
	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		log.Printf("%v", err)
	}
	json.Unmarshal(body, &mresp)
	return
}

type SyncGatewayClient struct {
	baseURI      string
	baseAdminURI string
	client       *RestClient
}

func (c *SyncGatewayClient) Init(hostname, db string) {
	c.baseURI = fmt.Sprintf("http://%s:4984/%s", hostname, db)
	c.baseAdminURI = fmt.Sprintf("http://%s:4985/%s", hostname, db)
	c.client = &RestClient{&http.Client{}, nil}
}

func (c *SyncGatewayClient) AddCookie(cookie *http.Cookie) {
	c.client.cookie = cookie
}

func (c *SyncGatewayClient) PutSingleDoc(docid string, doc Doc) {
	b, _ := json.Marshal(doc)
	j := bytes.NewReader(b)
	uri := fmt.Sprintf("%s/%s?new_edits=true", c.baseURI, docid)
	req, _ := http.NewRequest("PUT", uri, j)
	c.client.Do(req)
}

func (c *SyncGatewayClient) GetBulkDocs(docs map[string][]map[string]string) {
	b, _ := json.Marshal(docs)
	j := bytes.NewReader(b)
	uri := fmt.Sprintf("%s/_bulk_get", c.baseURI)
	req, _ := http.NewRequest("POST", uri, j)

	c.client.Do(req)
}

func (c *SyncGatewayClient) GetSingleDoc(docid string) {
	uri := fmt.Sprintf("%s/%s", c.baseURI, docid)
	req, _ := http.NewRequest("GET", uri, nil)
	c.client.Do(req)
}

func (c *SyncGatewayClient) GetLastSeq() string {
	uri := fmt.Sprintf("%s/", c.baseURI)
	req, _ := http.NewRequest("GET", uri, nil)

	resp := c.client.Do(req)
	return strconv.FormatFloat(resp["committed_update_seq"].(float64), 'f', 0, 64)
}

func (c *SyncGatewayClient) GetChangesFeed(since string) map[string]interface{} {
	uri := fmt.Sprintf("%s/_changes?limit=10&feed=longpoll&since=%s", c.baseURI, since)
	req, _ := http.NewRequest("GET", uri, nil)

	return c.client.Do(req)
}

func (c *SyncGatewayClient) AddUser(name string, auth UserAuth) {
	b, _ := json.Marshal(auth)
	j := bytes.NewReader(b)
	uri := fmt.Sprintf("%s/_user/%s", c.baseAdminURI, name)
	req, _ := http.NewRequest("PUT", uri, j)

	log.Printf("Adding user %s", name)
	c.client.Do(req)
}

func (c *SyncGatewayClient) CreateSession(name string, session Session) http.Cookie {
	b, _ := json.Marshal(session)
	j := bytes.NewReader(b)
	uri := fmt.Sprintf("%s/_session", c.baseAdminURI)
	req, _ := http.NewRequest("POST", uri, j)

	resp := c.client.Do(req)
	return http.Cookie{
		Name:  resp["cookie_name"].(string),
		Value: resp["session_id"].(string),
	}
}
<|endoftext|>


<commit_before>package gotel

import (
	"errors"
	"fmt"
	"github.com/emicklei/go-restful"
	"log"
	"net/http"
	"time"
)

// Response will hold a response sent back to the caller
type Response map[string]interface{}

func (ge *Endpoint) makeReservation(req *restful.Request, resp *restful.Response) {
	res := new(reservation)
	err := req.ReadEntity(&res)
	if err != nil {
		l.err("Unable to accept reservation")
	}

	err = validatereservation(res)
	if err != nil {
		l.warn("Invalid reservations [%q]", res)
		resp.WriteHeader(http.StatusBadRequest)
		resp.WriteAsJson(fmt.Sprintf("Unable to store reservation, validation failure [%v]", err))
		return
	}

	l.info("%q", res)

	_, err = storeReservation(ge.Db, res)
	if err != nil {
		l.err("Unable to store reservation %v", res)
		resp.WriteHeader(http.StatusBadRequest)
		resp.WriteAsJson("Unable to store reservation")
		return
	}
	resp.WriteAsJson("OK")
}

func (ge *Endpoint) listReservations(req *restful.Request, resp *restful.Response) {
	query := "select id, app, component, owner, notify, frequency, time_units, last_checkin_timestamp from reservations"
	rows, err := ge.Db.Query(query)
	if err != nil {
		l.err("Unable to list reservations [%v]", err)
		r := Response{"success": false, "message": "Unable to list reservations"}
		resp.WriteAsJson(r)
		return
	}
	reservations := []reservation{}
	defer rows.Close()
	for rows.Next() {
		res := reservation{}
		rows.Scan(&res.JobID, &res.App, &res.Component, &res.Owner, &res.Notify, &res.Frequency, &res.TimeUnits, &res.LastCheckin)
		reservations = append(reservations, res)
	}
	result := Response{"success": true, "result": reservations}
	resp.WriteAsJson(result)
	return
}

func (ge *Endpoint) doCheckin(req *restful.Request, resp *restful.Response) {
	c := new(checkin)
	err := req.ReadEntity(&c)
	if err != nil {
		l.err("Unable to accept checkin for %v", c)
		r := Response{"success": false, "message": "Unable to checkin: " + c.App}
		resp.WriteAsJson(r)
		return
	}

	now := time.Now().UTC().Unix()

	_, err = storeCheckin(ge.Db, *c, now)
	if err != nil {
		l.err("Unable to save checkin for %v", c)
		r := Response{"success": false, "message": "Unable to save checkin: " + c.App}
		resp.WriteAsJson(r)
		return
	}

	_, err = logHouseKeeping(ge.Db, *c, now)
	if err != nil {
		l.err("Unable to save checkin for %v", c)
		r := Response{"success": false, "message": "Unable to save checkin: " + c.App}
		resp.WriteAsJson(r)
		return
	}
	l.info("app [%s] component [%s] checked in %v", c.App, c.Component, time.Now())
	r := Response{"success": true, "message": "Application checked in: " + c.App}
	resp.WriteAsJson(r)
}

// used when you know your service will be offline for a bit and you want to pause alerts
func (ge *Endpoint) doPause(req *restful.Request, resp *restful.Response) {
	p := new(pause)
	err := req.ReadEntity(&p)
	if err != nil {
		l.err("Unable to accept pause for %v", p)
		r := Response{"success": false, "message": "Unable to checkin: " + p.App}
		resp.WriteAsJson(r)
		return
	}

	_, err = storePause(ge.Db, p)
	if err != nil {
		l.err("Unable to save pause for %v", p)
		r := Response{"success": false, "message": "Unable to save checkin: " + p.App}
		resp.WriteAsJson(r)
		return
	}

	r := Response{"success": true, "message": "Application paused: " + p.App}
	resp.WriteAsJson(r)

}

// used when you know your service will be offline for a bit and you want to pause alerts
func (ge *Endpoint) doCheckOut(req *restful.Request, resp *restful.Response) {
	p := new(checkOut)
	err := req.ReadEntity(&p)
	if err != nil {
		l.err("Unable to accept checkout for %v error [%s]", p, err)
		r := Response{"success": false, "message": "Unable to checkout: " + p.App}
		resp.WriteAsJson(r)
		return
	}
	_, err = storeCheckOut(ge.Db, p)
	if err != nil {
		l.err("Unable to save checkout for %v", p)
		r := Response{"success": false, "message": "Unable to save checkout: " + p.App}
		resp.WriteAsJson(r)
		return
	}
	r := Response{"success": true, "message": fmt.Sprintf("Application Removed [%s/%s] ", p.App, p.Component)}
	resp.WriteAsJson(r)
}

// IsCoordinator returns true if this node is the coordinator
func (ge *Endpoint) IsCoordinator(req *restful.Request, resp *restful.Response) {
	resp.WriteEntity(coordinator)
}

func validatereservation(res *reservation) error {
	timeUnits := map[string]int{"seconds": 1, "minutes": 1, "hours": 1}
	_, ok := timeUnits[res.TimeUnits]
	if !ok {
		return errors.New("Invalid time_units passed in")
	}
	return nil
}

// InitAPI initializes the webservice on the specific port
func InitAPI(ge *Endpoint, port int) {
	ws := new(restful.WebService)
	ws.
		Path("/").
		// You can specify consumes and produces per route as well.
		Consumes(restful.MIME_JSON, restful.MIME_XML).
		Produces(restful.MIME_JSON, restful.MIME_XML)

	ws.Route(ws.POST("/reservation").To(ge.makeReservation).Produces(restful.MIME_JSON))
	ws.Route(ws.GET("/reservation").To(ge.listReservations).Produces(restful.MIME_JSON))
	ws.Route(ws.POST("/checkin").To(ge.doCheckin).Produces(restful.MIME_JSON))
	ws.Route(ws.POST("/checkout").To(ge.doCheckOut).Produces(restful.MIME_JSON))
	ws.Route(ws.POST("/pause").To(ge.doPause).Produces(restful.MIME_JSON))
	ws.Route(ws.GET("/is-coordinator").To(ge.IsCoordinator).Produces(restful.MIME_JSON))

	restful.Add(ws)
	server := http.ListenAndServe(":8080", nil)
	log.Panic(server)
}
<commit_msg>Removed dependency on go-restful.<commit_after>package gotel

import (
	"errors"
	"fmt"
	"encoding/json"
	"log"
	"net/http"
	"time"
)

// Response will hold a response sent back to the caller
type Response map[string]interface{}

func writeError(w http.ResponseWriter, e interface{}) {
	w.WriteHeader(http.StatusBadRequest)
	w.Header().Set("Content-Type", "application/json")
	if bytes, err := json.Marshal(e); err != nil {
		w.Write([]byte("Could not encode error"))
		return
	} else {
		w.Write(bytes)
		return
	}
}

func writeResponse(w http.ResponseWriter, e interface{}) {
	w.Header().Set("Content-Type", "application/json")
	if bytes, err := json.Marshal(e); err != nil {
		w.Write([]byte("Could not encode response"))
	} else {
		w.Write(bytes)
		return
	}
}

func (ge *Endpoint) makeReservation(w http.ResponseWriter, req *http.Request) {
	res := new(reservation)
	decoder := json.NewDecoder(req.Body)
	err := decoder.Decode(&res)
	if err != nil {
		l.err("Unable to accept reservation")
	}

	err = validateReservation(res)
	if err != nil {
		l.warn("Invalid reservations [%q]", res)
		writeError(w, fmt.Sprintf("Unable to store reservation, validation failure [%v]", err))
		return
	}

	l.info("%q", res)

	_, err = storeReservation(ge.Db, res)
	if err != nil {
		l.err("Unable to store reservation %v", res)
		writeError(w, "Unable to store reservation")
		return
	}
	writeResponse(w, "OK")
}

func (ge *Endpoint) listReservations(w http.ResponseWriter, req *http.Request) {
	query := "SELECT id, app, component, owner, notify, frequency, time_units, last_checkin_timestamp FROM reservations"
	rows, err := ge.Db.Query(query)
	if err != nil {
		l.err("Unable to list reservations [%v]", err)
		r := Response{"success": false, "message": "Unable to list reservations"}
		writeResponse(w, r)
		return
	}
	reservations := []reservation{}
	defer rows.Close()
	for rows.Next() {
		res := reservation{}
		rows.Scan(&res.JobID, &res.App, &res.Component, &res.Owner, &res.Notify, &res.Frequency, &res.TimeUnits, &res.LastCheckin)
		reservations = append(reservations, res)
	}
	result := Response{"success": true, "result": reservations}
	writeResponse(w, result)
	return
}

func (ge *Endpoint) doCheckin(w http.ResponseWriter, req *http.Request) {
	c := new(checkin)
	decoder := json.NewDecoder(req.Body)
	err := decoder.Decode(&c)
	if err != nil {
		l.err("Unable to accept checkin for %v", c)
		r := Response{"success": false, "message": "Unable to checkin: " + c.App}
		writeResponse(w, r)
		return
	}

	now := time.Now().UTC().Unix()

	_, err = storeCheckin(ge.Db, *c, now)
	if err != nil {
		l.err("Unable to save checkin for %v", c)
		r := Response{"success": false, "message": "Unable to save checkin: " + c.App}
		writeResponse(w, r)
		return
	}

	_, err = logHouseKeeping(ge.Db, *c, now)
	if err != nil {
		l.err("Unable to save checkin for %v", c)
		r := Response{"success": false, "message": "Unable to save checkin: " + c.App}
		writeResponse(w, r)
		return
	}
	l.info("app [%s] component [%s] checked in %v", c.App, c.Component, time.Now())
	r := Response{"success": true, "message": "Application checked in: " + c.App}
	writeResponse(w, r)
}

// used when you know your service will be offline for a bit and you want to pause alerts
func (ge *Endpoint) doPause(w http.ResponseWriter, req *http.Request) {
	p := new(pause)
	decoder := json.NewDecoder(req.Body)
	err := decoder.Decode(&p)
	if err != nil {
		l.err("Unable to accept pause for %v", p)
		r := Response{"success": false, "message": "Unable to checkin: " + p.App}
		writeResponse(w, r)
		return
	}

	_, err = storePause(ge.Db, p)
	if err != nil {
		l.err("Unable to save pause for %v", p)
		r := Response{"success": false, "message": "Unable to save checkin: " + p.App}
		writeResponse(w, r)
		return
	}

	r := Response{"success": true, "message": "Application paused: " + p.App}
	writeResponse(w, r)

}

// used when you know your service will be offline for a bit and you want to pause alerts
func (ge *Endpoint) doCheckOut(w http.ResponseWriter, req *http.Request) {
	p := new(checkOut)
	decoder := json.NewDecoder(req.Body)
	err := decoder.Decode(&p)
	if err != nil {
		l.err("Unable to accept checkout for %v error [%s]", p, err)
		r := Response{"success": false, "message": "Unable to checkout: " + p.App}
		writeResponse(w, r)
		return
	}
	_, err = storeCheckOut(ge.Db, p)
	if err != nil {
		l.err("Unable to save checkout for %v", p)
		r := Response{"success": false, "message": "Unable to save checkout: " + p.App}
		writeResponse(w, r)
		return
	}
	r := Response{"success": true, "message": fmt.Sprintf("Application Removed [%s/%s] ", p.App, p.Component)}
  writeResponse(w, r)
}

func (ge *Endpoint) isCoordinator(w http.ResponseWriter, req *http.Request) {
	writeResponse(w, coordinator)
}

func validateReservation(res *reservation) error {
	timeUnits := map[string]int{"seconds": 1, "minutes": 1, "hours": 1}
	_, ok := timeUnits[res.TimeUnits]
	if !ok {
		return errors.New("Invalid time_units passed in")
	}
	return nil
}

// InitAPI initializes the webservice on the specific port
func InitAPI(ge *Endpoint, port int) {
	http.HandleFunc("/reservation", func(w http.ResponseWriter, r *http.Request) {
		if r.Method == "GET" {
			ge.listReservations(w, r)
			return
		} else if (r.Method == "POST") {
			ge.makeReservation(w, r)
			return
		}
		writeError(w, fmt.Sprintf("Invalid method %s", r.Method))
		return
	})
	http.HandleFunc("/checkin", func(w http.ResponseWriter, r *http.Request) {
		if r.Method == "POST" {
			ge.doCheckin(w, r)
			return
		}
		writeError(w, fmt.Sprintf("Invalid method %s", r.Method))
		return
	})
	http.HandleFunc("/checkout", func(w http.ResponseWriter, r *http.Request) {
		if r.Method == "POST" {
			ge.doCheckin(w, r)
			return
		}
		writeError(w, fmt.Sprintf("Invalid method %s", r.Method))
		return
	})
	http.HandleFunc("/pause", func(w http.ResponseWriter, r *http.Request) {
		if r.Method == "POST" {
			ge.doPause(w, r)
			return
		}
		writeError(w, fmt.Sprintf("Invalid method %s", r.Method))
		return
	})
	http.HandleFunc("/is-coordinator", func(w http.ResponseWriter, r *http.Request) {
		if r.Method == "GET" {
			ge.isCoordinator(w, r)
			return
		}
		writeError(w, fmt.Sprintf("Invalid method %s", r.Method))
		return
	})

	server := http.ListenAndServe(":8080", nil)
	log.Panic(server)
}
<|endoftext|>


<commit_before>package cli

import (
	"fmt"
	"html/template"
	"os"

	"reflect"

	"github.com/fatih/color"
	"github.com/olekukonko/tablewriter"
	log "github.com/sirupsen/logrus"
)

type App struct {
	Name            string
	Usage           string
	LongDescription string
	Copyright       string
	ParsingOrder    []ParsingType
	Command         *Command
	Authors         []Author
	HelpCommands    map[string]bool
	Action          func()
	ConfigFlag      string
	RemoveColor     bool
	args            []string
	activeCommands  []*Command
}

type ParsingType int

const (
	EnvironmentVariables ParsingType = iota
	JsonConfig
	TomlConfig
	CliFlags
)

var ParingTypeStringMap = map[ParsingType]string{
	EnvironmentVariables: "Environment",
	JsonConfig:           "JSON Config",
	TomlConfig:           "Toml Config",
	CliFlags:             "CLI Flag",
}

// Create a new application with default values set.
func NewApp() *App {
	return &App{
		Name:    "cli",
		Authors: make([]Author, 0),
		HelpCommands: map[string]bool{
			"--help": true,
			"-h":     true,
			"help":   true,
		},
		ParsingOrder: []ParsingType{
			EnvironmentVariables,
			JsonConfig,
			TomlConfig,
			CliFlags,
		},
	}
}

// Run the app. Should be called with:
// app := cli.NewApp()
// app.Run(os.Args)
func (a *App) Run(args []string) {
	a.args = args[1:]
	a.parseCommands()

	finalCommand := a.activeCommands[len(a.activeCommands)-1]
	finalCommand.Action()
}

func (a *App) parseCommands() {
	if a.Command == nil {
		log.Fatal("No command attached to the app!")
	}
	a.Command.buildTree(nil)
	a.Command.assignArguments(a.args)

	if helpCommand, isHelp := a.Command.isHelpCommand(a.HelpCommands); isHelp {
		fmt.Println("help.", helpCommand.Name)
		return
	}

	if err := a.Command.parseFlags(); err != nil {
		// TODO: Fix me.
		return
	}

	a.activeCommands = a.Command.GetActiveCommands()
	a.Command.findConfigVars()
	a.Command.parseConfigVars()
	settingsMap := a.parseByOrder()
	a.applySettingsMap(settingsMap)
}

func (a *App) parseByOrder() *mappedSettings {
	settingsMap := newMappedSettings()

	for _, order := range a.ParsingOrder {
		switch order {
		case EnvironmentVariables:
			settingsMap.addParsedArray(a.Command.parseEnvVars())

		case JsonConfig:
			vars := a.Command.getConfigVarsByType(JsonConfig)
			if len(vars) == 0 {
				continue
			}

		case TomlConfig:
			vars := a.Command.getConfigVarsByType(TomlConfig)
			if len(vars) == 0 {
				continue
			}
			setValues := a.Command.parseConfigValues(vars)
			settingsMap.addParsedArray(setValues)

		case CliFlags:
			settingsMap.addParsedArray(a.Command.getSetFlags())
		}
	}
	settingsMap.PrintDuplicates(a.activeCommands)
	settingsMap.PrintDuplicatesStdout(a.RemoveColor)
	return settingsMap
}

func (a *App) applySettingsMap(settingsMap *mappedSettings) {
	commandMap := a.Command.GetExpandedActiveCommmands()
	// loop through commands, ensure that the order of settings are constantly applied,
	// instead of looping through MainMap, which is not a consistent order.
	for _, command := range a.activeCommands {
		path := command.GetExpandedName()
		variableSettingsMap := settingsMap.MainMap[path]
		currCommand := commandMap[path]
		variableMap := currCommand.GetVariableMap()

		for _, setting := range variableSettingsMap {
			activeSetting := setting[len(setting)-1]
			currVariable := variableMap[activeSetting.VariableName]
			if _, ok := currVariable.(*ConfigVariable); ok {
				continue
			}
			currVariable.apply(activeSetting.Value)
		}
	}
}

type mappedSettings struct {
	MainMap map[string]map[string][]activeSetting `json:"main_map"`
}

func newMappedSettings() *mappedSettings {
	return &mappedSettings{
		MainMap: make(map[string]map[string][]activeSetting),
	}
}

func (m *mappedSettings) addParsedArray(settings []activeSetting) {
	for _, setting := range settings {
		if m.MainMap[setting.CommandPath] == nil {
			m.MainMap[setting.CommandPath] = make(map[string][]activeSetting)
		}
		if m.MainMap[setting.CommandPath][setting.VariableName] == nil {
			m.MainMap[setting.CommandPath][setting.VariableName] = make([]activeSetting, 0)
		}
		m.MainMap[setting.CommandPath][setting.VariableName] = append(m.MainMap[setting.CommandPath][setting.VariableName], setting)
	}
}

// Helper to print duplciates in table format to Stdout.
func (m *mappedSettings) PrintDuplicates(commands []*Command) {
	table := tablewriter.NewWriter(os.Stdout)
	table.SetHeader([]string{"Command", "Variable", "Source", "Value", "Type", "Status"})
	for _, command := range commands {
		expandedName := command.GetExpandedName()
		if m.MainMap[expandedName] == nil {
			continue
		}
		for _, settings := range m.MainMap[expandedName] {
			length := len(settings)
			for i, setting := range settings {
				var status string
				if i == length-1 {
					status = "✔ Used"
				} else {
					status = "x Ignored"
				}
				row := []string{
					expandedName,
					setting.VariableName,
					ParingTypeStringMap[setting.Source],
					fmt.Sprintf("%s", setting.Value),
					reflect.TypeOf(setting.Value).String(),
					status,
				}
				if setting.Source == EnvironmentVariables {
					row[1] += " (" + convertNameToOS(setting.VariableName) + ")"
				}
				table.Append(row)
			}
		}
	}
	table.Render()
}

// Use a custom formatted string to print duplicates on Stdout.
func (m *mappedSettings) PrintDuplicatesStdout(noColor bool) {
	t := template.New("duplicates")
	funcMap := template.FuncMap{
		"blue":  color.BlueString,
		"red":   color.RedString,
		"green": color.GreenString,
		"bold":  color.New(color.Bold).Sprint,
		"sourceString": func(setting activeSetting) string {
			if setting.Source == EnvironmentVariables {
				return fmt.Sprintf("%s (%s)", ParingTypeStringMap[setting.Source], convertNameToOS(setting.VariableName))
			} else if setting.Source == TomlConfig || setting.Source == JsonConfig || setting.Source == YamlConfig {
				return fmt.Sprintf("%s (%s)", ParingTypeStringMap[setting.Source], setting.SettingName)

			} else {
				return ParingTypeStringMap[setting.Source]
			}
		},
		"plus1": func(x int) int {
			return x + 1
		},
	}
	if noColor {
		funcMap["blue"] = identityString
		funcMap["red"] = identityString
		funcMap["green"] = identityString
		funcMap["bold"] = identityString
	}
	t.Funcs(funcMap)
	t.Parse(`{{ range $command, $variables := . -}}
-------------------------------------
{{ blue "Configuration:"}} {{ bold $command }}
{{ range $key, $vars := $variables -}}
-------------
{{ range $k, $var := $vars }}{{ $length := len $vars -}}
	{{ if eq $length (plus1 $k) -}}
		{{ green $key }} = {{ green $var.Value }}
	{{ green "set from" }} {{ sourceString $var -}}
	{{ else -}}
		{{ red $key }} = {{ red $var.Value }}
	{{ red "ignored from" }} {{ sourceString $var -}}
	{{ end }}
{{ end -}}
{{ end }}
{{ end }}`)
	t.Execute(os.Stdout, m.MainMap)
}

func identityString(s string) string {
	return s
}
<commit_msg>remove reference to YamlConfig type<commit_after>package cli

import (
	"fmt"
	"html/template"
	"os"

	"reflect"

	"github.com/fatih/color"
	"github.com/olekukonko/tablewriter"
	log "github.com/sirupsen/logrus"
)

type App struct {
	Name            string
	Usage           string
	LongDescription string
	Copyright       string
	ParsingOrder    []ParsingType
	Command         *Command
	Authors         []Author
	HelpCommands    map[string]bool
	Action          func()
	ConfigFlag      string
	RemoveColor     bool
	args            []string
	activeCommands  []*Command
}

type ParsingType int

const (
	EnvironmentVariables ParsingType = iota
	JsonConfig
	TomlConfig
	CliFlags
)

var ParingTypeStringMap = map[ParsingType]string{
	EnvironmentVariables: "Environment",
	JsonConfig:           "JSON Config",
	TomlConfig:           "Toml Config",
	CliFlags:             "CLI Flag",
}

// Create a new application with default values set.
func NewApp() *App {
	return &App{
		Name:    "cli",
		Authors: make([]Author, 0),
		HelpCommands: map[string]bool{
			"--help": true,
			"-h":     true,
			"help":   true,
		},
		ParsingOrder: []ParsingType{
			EnvironmentVariables,
			JsonConfig,
			TomlConfig,
			CliFlags,
		},
	}
}

// Run the app. Should be called with:
// app := cli.NewApp()
// app.Run(os.Args)
func (a *App) Run(args []string) {
	a.args = args[1:]
	a.parseCommands()

	finalCommand := a.activeCommands[len(a.activeCommands)-1]
	finalCommand.Action()
}

func (a *App) parseCommands() {
	if a.Command == nil {
		log.Fatal("No command attached to the app!")
	}
	a.Command.buildTree(nil)
	a.Command.assignArguments(a.args)

	if helpCommand, isHelp := a.Command.isHelpCommand(a.HelpCommands); isHelp {
		fmt.Println("help.", helpCommand.Name)
		return
	}

	if err := a.Command.parseFlags(); err != nil {
		// TODO: Fix me.
		return
	}

	a.activeCommands = a.Command.GetActiveCommands()
	a.Command.findConfigVars()
	a.Command.parseConfigVars()
	settingsMap := a.parseByOrder()
	a.applySettingsMap(settingsMap)
}

func (a *App) parseByOrder() *mappedSettings {
	settingsMap := newMappedSettings()

	for _, order := range a.ParsingOrder {
		switch order {
		case EnvironmentVariables:
			settingsMap.addParsedArray(a.Command.parseEnvVars())

		case JsonConfig:
			vars := a.Command.getConfigVarsByType(JsonConfig)
			if len(vars) == 0 {
				continue
			}

		case TomlConfig:
			vars := a.Command.getConfigVarsByType(TomlConfig)
			if len(vars) == 0 {
				continue
			}
			setValues := a.Command.parseConfigValues(vars)
			settingsMap.addParsedArray(setValues)

		case CliFlags:
			settingsMap.addParsedArray(a.Command.getSetFlags())
		}
	}
	settingsMap.PrintDuplicates(a.activeCommands)
	settingsMap.PrintDuplicatesStdout(a.RemoveColor)
	return settingsMap
}

func (a *App) applySettingsMap(settingsMap *mappedSettings) {
	commandMap := a.Command.GetExpandedActiveCommmands()
	// loop through commands, ensure that the order of settings are constantly applied,
	// instead of looping through MainMap, which is not a consistent order.
	for _, command := range a.activeCommands {
		path := command.GetExpandedName()
		variableSettingsMap := settingsMap.MainMap[path]
		currCommand := commandMap[path]
		variableMap := currCommand.GetVariableMap()

		for _, setting := range variableSettingsMap {
			activeSetting := setting[len(setting)-1]
			currVariable := variableMap[activeSetting.VariableName]
			if _, ok := currVariable.(*ConfigVariable); ok {
				continue
			}
			currVariable.apply(activeSetting.Value)
		}
	}
}

type mappedSettings struct {
	MainMap map[string]map[string][]activeSetting `json:"main_map"`
}

func newMappedSettings() *mappedSettings {
	return &mappedSettings{
		MainMap: make(map[string]map[string][]activeSetting),
	}
}

func (m *mappedSettings) addParsedArray(settings []activeSetting) {
	for _, setting := range settings {
		if m.MainMap[setting.CommandPath] == nil {
			m.MainMap[setting.CommandPath] = make(map[string][]activeSetting)
		}
		if m.MainMap[setting.CommandPath][setting.VariableName] == nil {
			m.MainMap[setting.CommandPath][setting.VariableName] = make([]activeSetting, 0)
		}
		m.MainMap[setting.CommandPath][setting.VariableName] = append(m.MainMap[setting.CommandPath][setting.VariableName], setting)
	}
}

// Helper to print duplciates in table format to Stdout.
func (m *mappedSettings) PrintDuplicates(commands []*Command) {
	table := tablewriter.NewWriter(os.Stdout)
	table.SetHeader([]string{"Command", "Variable", "Source", "Value", "Type", "Status"})
	for _, command := range commands {
		expandedName := command.GetExpandedName()
		if m.MainMap[expandedName] == nil {
			continue
		}
		for _, settings := range m.MainMap[expandedName] {
			length := len(settings)
			for i, setting := range settings {
				var status string
				if i == length-1 {
					status = "✔ Used"
				} else {
					status = "x Ignored"
				}
				row := []string{
					expandedName,
					setting.VariableName,
					ParingTypeStringMap[setting.Source],
					fmt.Sprintf("%s", setting.Value),
					reflect.TypeOf(setting.Value).String(),
					status,
				}
				if setting.Source == EnvironmentVariables {
					row[1] += " (" + convertNameToOS(setting.VariableName) + ")"
				}
				table.Append(row)
			}
		}
	}
	table.Render()
}

// Use a custom formatted string to print duplicates on Stdout.
func (m *mappedSettings) PrintDuplicatesStdout(noColor bool) {
	t := template.New("duplicates")
	funcMap := template.FuncMap{
		"blue":  color.BlueString,
		"red":   color.RedString,
		"green": color.GreenString,
		"bold":  color.New(color.Bold).Sprint,
		"sourceString": func(setting activeSetting) string {
			if setting.Source == EnvironmentVariables {
				return fmt.Sprintf("%s (%s)", ParingTypeStringMap[setting.Source], convertNameToOS(setting.VariableName))
			} else if setting.Source == TomlConfig || setting.Source == JsonConfig {
				return fmt.Sprintf("%s (%s)", ParingTypeStringMap[setting.Source], setting.SettingName)
			} else {
				return ParingTypeStringMap[setting.Source]
			}
		},
		"plus1": func(x int) int {
			return x + 1
		},
	}
	if noColor {
		funcMap["blue"] = identityString
		funcMap["red"] = identityString
		funcMap["green"] = identityString
		funcMap["bold"] = identityString
	}
	t.Funcs(funcMap)
	t.Parse(`{{ range $command, $variables := . -}}
-------------------------------------
{{ blue "Configuration:"}} {{ bold $command }}
{{ range $key, $vars := $variables -}}
-------------
{{ range $k, $var := $vars }}{{ $length := len $vars -}}
	{{ if eq $length (plus1 $k) -}}
		{{ green $key }} = {{ green $var.Value }}
	{{ green "set from" }} {{ sourceString $var -}}
	{{ else -}}
		{{ red $key }} = {{ red $var.Value }}
	{{ red "ignored from" }} {{ sourceString $var -}}
	{{ end }}
{{ end -}}
{{ end }}
{{ end }}`)
	t.Execute(os.Stdout, m.MainMap)
}

func identityString(s string) string {
	return s
}
<|endoftext|>


<commit_before>package lapi

import (
	"fmt"
	"net/http"
	"sync"

	"github.com/goline/errors"
)

// App is a central application
type App interface {
	AppLoader
	AppRunner
	AppRouter
	AppRescuer
	AppConfigger
	ContainerAware
}

// AppLoader handles application's loader
type AppLoader interface {
	// WithLoader allows to register application's loader
	WithLoader(loader Loader) App
}

type AppConfigger interface {
	// Config returns application's configuration
	Config() interface{}

	// WithConfig sets application's config
	WithConfig(config interface{}) App
}

// AppRouter handles router
type AppRouter interface {
	// Router returns an instance of Router
	Router() Router

	// WithRouter sets router
	WithRouter(router Router) App
}

// AppRescuer manages error handler
type AppRescuer interface {
	// Rescuer returns an instance of Rescuer
	Rescuer() Rescuer

	// WithRescuer sets error handler
	WithRescuer(handler Rescuer) App
}

// AppRunner runs application
type AppRunner interface {
	// Run brings application up
	// Any errors should manage inside this method
	Run(container Container, config interface{})
}

func NewApp() App {
	return &FactoryApp{
		loaders: make([]Loader, 0),
		router:  NewRouter(),
	}
}

type FactoryApp struct {
	config    interface{}
	container Container
	loaders   []Loader
	router    Router
	rescuer   Rescuer
}

func (a *FactoryApp) WithLoader(loader Loader) App {
	a.loaders = append(a.loaders, loader)
	return a
}

func (a *FactoryApp) Config() interface{} {
	return a.config
}

func (a *FactoryApp) WithConfig(config interface{}) App {
	a.config = config
	return a
}

func (a *FactoryApp) Router() Router {
	return a.router
}

func (a *FactoryApp) WithRouter(router Router) App {
	a.router = router
	return a
}

func (a *FactoryApp) Rescuer() Rescuer {
	if a.rescuer == nil {
		a.rescuer = NewRescuer()
	}

	return a.rescuer
}

func (a *FactoryApp) WithRescuer(handler Rescuer) App {
	a.rescuer = handler
	return a
}

func (a *FactoryApp) Container() Container {
	return a.container
}

func (a *FactoryApp) WithContainer(container Container) ContainerAware {
	a.container = container
	return a
}

func (a *FactoryApp) Run(container Container, config interface{}) {
	if container == nil || config == nil {
		panic("App requires container and config to run")
	}

	a.container = container
	a.config = config
	a.SetUp().Handle()
}

func (a *FactoryApp) SetUp() *FactoryApp {
	if a.container == nil {
		panic("App requires a container to run")
	}

	var wg sync.WaitGroup
	for _, loader := range a.loaders {
		wg.Add(1)
		go func(loader Loader) {
			defer wg.Done()
			loader.Load(a)
		}(loader)
	}
	wg.Wait()

	if a.rescuer == nil {
		a.WithRescuer(NewRescuer())
	}
	PanicOnError(a.container.Inject(a.rescuer))
	return a
}

func (a *FactoryApp) Handle() *FactoryApp {
	if a.router == nil {
		panic(errors.New(ERR_ROUTER_NOT_DEFINED, fmt.Sprint("Router is not defined yet.")))
	}

	http.Handle("/", a)
	if c, ok := a.config.(ServerConfig); ok == true {
		PanicOnError(http.ListenAndServe(c.Address(), nil))
	} else {
		panic(errors.New(ERR_SERVER_CONFIG_MISSING, fmt.Sprint("Server configuration is missing")))
	}
	return a
}

func (a *FactoryApp) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	connection := a.setUpConnection(w, r)
	defer a.forceSendResponse(connection)

	PanicOnError(a.router.Route(connection.Request()))
	for _, hook := range connection.Request().Route().Hooks() {
		PanicOnError(hook.SetUp(connection))
	}
	if connection.Response().IsSent() == true {
		return
	}

	handler := connection.Request().Route().Handler()
	if handler == nil {
		panic(errors.New(ERR_NO_HANDLER_FOUND, "No handler found"))
	}

	PanicOnError(a.container.Inject(handler))
	if h, ok := handler.(ContainerAware); ok == true {
		h.WithContainer(a.container)
	}
	result, err := handler.Handle(connection)
	for _, hook := range connection.Request().Route().Hooks() {
		PanicOnError(hook.TearDown(connection, result, err))
	}
}

func (a *FactoryApp) forceSendResponse(connection Connection) {
	if r := recover(); r != nil {
		PanicOnError(a.rescuer.Rescue(connection, r))
	}
	if connection.Response().IsSent() == false {
		connection.Response().Send()
	}
}

func (a *FactoryApp) setUpConnection(w http.ResponseWriter, r *http.Request) Connection {
	request := NewRequest(r)
	response := NewResponse(w)

	// let make request and response have same content-type and charset as default
	response.WithContentType(request.ContentType()).WithCharset(request.Charset())

	return NewConnection(request, response)
}
<commit_msg>Add http.Handler to app<commit_after>package lapi

import (
	"fmt"
	"net/http"
	"sync"

	"github.com/goline/errors"
)

// App is a central application
type App interface {
	AppLoader
	AppRunner
	AppRouter
	AppRescuer
	AppConfigger
	ContainerAware
	http.Handler
}

// AppLoader handles application's loader
type AppLoader interface {
	// WithLoader allows to register application's loader
	WithLoader(loader Loader) App
}

type AppConfigger interface {
	// Config returns application's configuration
	Config() interface{}

	// WithConfig sets application's config
	WithConfig(config interface{}) App
}

// AppRouter handles router
type AppRouter interface {
	// Router returns an instance of Router
	Router() Router

	// WithRouter sets router
	WithRouter(router Router) App
}

// AppRescuer manages error handler
type AppRescuer interface {
	// Rescuer returns an instance of Rescuer
	Rescuer() Rescuer

	// WithRescuer sets error handler
	WithRescuer(handler Rescuer) App
}

// AppRunner runs application
type AppRunner interface {
	// Run brings application up
	// Any errors should manage inside this method
	Run(container Container, config interface{})
}

func NewApp() App {
	return &FactoryApp{
		loaders: make([]Loader, 0),
		router:  NewRouter(),
	}
}

type FactoryApp struct {
	config    interface{}
	container Container
	loaders   []Loader
	router    Router
	rescuer   Rescuer
}

func (a *FactoryApp) WithLoader(loader Loader) App {
	a.loaders = append(a.loaders, loader)
	return a
}

func (a *FactoryApp) Config() interface{} {
	return a.config
}

func (a *FactoryApp) WithConfig(config interface{}) App {
	a.config = config
	return a
}

func (a *FactoryApp) Router() Router {
	return a.router
}

func (a *FactoryApp) WithRouter(router Router) App {
	a.router = router
	return a
}

func (a *FactoryApp) Rescuer() Rescuer {
	if a.rescuer == nil {
		a.rescuer = NewRescuer()
	}

	return a.rescuer
}

func (a *FactoryApp) WithRescuer(handler Rescuer) App {
	a.rescuer = handler
	return a
}

func (a *FactoryApp) Container() Container {
	return a.container
}

func (a *FactoryApp) WithContainer(container Container) ContainerAware {
	a.container = container
	return a
}

func (a *FactoryApp) Run(container Container, config interface{}) {
	if container == nil || config == nil {
		panic("App requires container and config to run")
	}

	a.container = container
	a.config = config
	a.SetUp().Handle()
}

func (a *FactoryApp) SetUp() *FactoryApp {
	if a.container == nil {
		panic("App requires a container to run")
	}

	var wg sync.WaitGroup
	for _, loader := range a.loaders {
		wg.Add(1)
		go func(loader Loader) {
			defer wg.Done()
			loader.Load(a)
		}(loader)
	}
	wg.Wait()

	if a.rescuer == nil {
		a.WithRescuer(NewRescuer())
	}
	PanicOnError(a.container.Inject(a.rescuer))
	return a
}

func (a *FactoryApp) Handle() *FactoryApp {
	if a.router == nil {
		panic(errors.New(ERR_ROUTER_NOT_DEFINED, fmt.Sprint("Router is not defined yet.")))
	}

	http.Handle("/", a)
	if c, ok := a.config.(ServerConfig); ok == true {
		PanicOnError(http.ListenAndServe(c.Address(), nil))
	} else {
		panic(errors.New(ERR_SERVER_CONFIG_MISSING, fmt.Sprint("Server configuration is missing")))
	}
	return a
}

func (a *FactoryApp) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	connection := a.setUpConnection(w, r)
	defer a.forceSendResponse(connection)

	PanicOnError(a.router.Route(connection.Request()))
	for _, hook := range connection.Request().Route().Hooks() {
		PanicOnError(hook.SetUp(connection))
	}
	if connection.Response().IsSent() == true {
		return
	}

	handler := connection.Request().Route().Handler()
	if handler == nil {
		panic(errors.New(ERR_NO_HANDLER_FOUND, "No handler found"))
	}

	PanicOnError(a.container.Inject(handler))
	if h, ok := handler.(ContainerAware); ok == true {
		h.WithContainer(a.container)
	}
	result, err := handler.Handle(connection)
	for _, hook := range connection.Request().Route().Hooks() {
		PanicOnError(hook.TearDown(connection, result, err))
	}
}

func (a *FactoryApp) forceSendResponse(connection Connection) {
	if r := recover(); r != nil {
		PanicOnError(a.rescuer.Rescue(connection, r))
	}
	if connection.Response().IsSent() == false {
		connection.Response().Send()
	}
}

func (a *FactoryApp) setUpConnection(w http.ResponseWriter, r *http.Request) Connection {
	request := NewRequest(r)
	response := NewResponse(w)

	// let make request and response have same content-type and charset as default
	response.WithContentType(request.ContentType()).WithCharset(request.Charset())

	return NewConnection(request, response)
}
<|endoftext|>


<commit_before>package mdqi

import (
	"fmt"
	"io"
	"os"
	"os/user"
	"path/filepath"
	"strings"

	"github.com/peterh/liner"
	"github.com/pkg/errors"
)

var (
	ErrSlashCommandNotFound    = errors.New("unknown SlashCommand")
	ErrNotASlashCommand        = errors.New("there are no SlashCommand")
	ErrSlashCommandInvalidArgs = errors.New("invalid args")
	ErrUnknownPrinterName      = errors.New("unknown printer name")
)

type App struct {
	// Alive turns into false, mdqi will exit.
	Alive bool

	// cmdPath is path to mdq command.
	cmdPath string

	// historyPath is path to command history file for liner.
	historyPath string

	// slashCommandDefinition holds SlashCommandDefinition.
	// app.slashCommandDefinition[category][name] = SlashCommandDefinition
	slashCommandDefinition map[string]map[string]SlashCommandDefinition

	// tag stores tag value for --tag option of mdq.
	tag string

	printer Printer
}

type Result struct {
	Database string
	Columns  []string
	Rows     []map[string]interface{}
}

func init() {
	defaultOutput = os.Stdout
}

func NewApp(conf Conf) (*App, error) {
	// TODO: Check if mdq command exists by exec.LookPath.

	// create history file
	historyPath := conf.Mdqi.History
	if historyPath == "" {
		var err error
		if historyPath, err = defaultHistoryPath(); err != nil {
			return nil, errors.Wrap(err, "failed to make default history path")
		}
	}

	err := createHistoryFile(historyPath)
	if err != nil {
		return nil, errors.Wrap(err, "failed to create history file")
	}

	debug.Println("conf (history):", historyPath)

	app := &App{
		Alive: true,

		cmdPath:                "mdq",
		historyPath:            historyPath,
		slashCommandDefinition: map[string]map[string]SlashCommandDefinition{},
		printer:                HorizontalPrinter{},
	}

	// set default tag
	if tag := conf.Mdqi.DefaultTag; tag != "" {
		app.SetTag(tag)
		debug.Println("conf (tag):", tag)
	}

	// set default display
	if display := conf.Mdqi.DefaultDisplay; display != "" {
		if err = app.SetPrinterByName(display); err != nil {
			return nil, errors.Wrap(err, "failed to set default printer")
		}

		debug.Println("conf (display):", display)
	}

	return app, nil
}

func createHistoryFile(path string) error {
	if _, err := os.Stat(path); os.IsNotExist(err) {
		if _, err := os.Create(path); err != nil {
			return errors.Wrap(err, "failed to create history file")
		}
	}

	return nil
}

func defaultHistoryPath() (string, error) {
	usr, err := user.Current()
	if err != nil {
		return "", errors.Wrap(err, "failed to get current user")
	}

	return filepath.Join(usr.HomeDir, ".mdqi_history"), err
}

func (app *App) slashCommandCategories() []string {
	defs := app.slashCommandDefinition
	keys := make([]string, 0, len(defs))

	for key := range defs {
		keys = append(keys, key)
	}

	return keys
}

func (app *App) slashCommandNames(category string) []string {
	defs := app.slashCommandDefinition[category]
	keys := make([]string, 0, len(defs))

	for key := range defs {
		keys = append(keys, key)
	}

	return keys
}

func (app *App) Run() {
	app.runLiner()
}

func (app *App) runLiner() {
	line := liner.NewLiner()
	defer line.Close()

	line.SetCtrlCAborts(true)

	app.initHistory(line)

LOOP:
	for {
		if !app.Alive {
			fmt.Println("bye")
			break LOOP
		}

		l, err := line.Prompt("mdq> ")

		switch err {
		case nil:
			l = strings.Trim(l, " \n")

			if l == "" {
				continue
			}

			line.AppendHistory(l)

			scmd, _ := ParseSlashCommand(l)
			if scmd != nil {
				app.runSlashCommand(scmd)
				continue
			}

			results, err := app.RunCmd(l, app.buildCmdArgs()...)
			if err != nil {
				logger.Println(err.Error())
			}

			Print(app.printer, results)
		case liner.ErrPromptAborted:
			logger.Println("aborted")
			break LOOP
		case io.EOF:
			fmt.Println("bye")
			break LOOP
		default:
			logger.Println("error on reading line: ", err)
			break LOOP
		}

		app.saveHistory(line)
	}
}

func (app *App) initHistory(line *liner.State) {
	if f, err := os.Open(app.historyPath); err == nil {
		line.ReadHistory(f)
		f.Close()
	} else {
		logger.Println("failed to read command history: ", err)
	}
}

func (app *App) saveHistory(line *liner.State) {
	if f, err := os.Create(app.historyPath); err == nil {
		if _, err := line.WriteHistory(f); err != nil {
			logger.Println("failed to write history: ", err)
		}

		f.Close()
	} else {
		logger.Println("failed to create history file: ", err)
	}
}

func (app *App) buildCmdArgs() []string {
	args := []string{}

	// tag
	if app.tag != "" {
		args = append(args, "--tag="+app.tag)
	}

	return args
}

func (app *App) runSlashCommand(scmd *SlashCommand) {
	sdef, err := app.FindSlashCommandDefinition(scmd.Category, scmd.Name)

	switch err {
	case nil:
		if err := sdef.Handle(app, scmd); err != nil {
			logger.Println("failed to handle slash command:", err)
		}
	case ErrSlashCommandNotFound:
		logger.Println("unknown slash command")
	}

	return
}

func (app *App) SetPrinterByName(name string) error {

	switch name {
	case "horizontal":
		app.printer = HorizontalPrinter{}
	case "vertical":
		app.printer = VerticalPrinter{}
	default:
		return ErrUnknownPrinterName
	}

	return nil
}

func (app *App) GetTag() string {
	return app.tag
}

func (app *App) SetTag(tag string) {
	app.tag = tag
}

func (app *App) ClearTag() {
	app.tag = ""
}
<commit_msg>codes around create history file were moved into createHistoryFile<commit_after>package mdqi

import (
	"fmt"
	"io"
	"os"
	"os/user"
	"path/filepath"
	"strings"

	"github.com/peterh/liner"
	"github.com/pkg/errors"
)

const (
	defaultHistoryFilename = ".mdqi_history"
)

var (
	ErrSlashCommandNotFound    = errors.New("unknown SlashCommand")
	ErrNotASlashCommand        = errors.New("there are no SlashCommand")
	ErrSlashCommandInvalidArgs = errors.New("invalid args")
	ErrUnknownPrinterName      = errors.New("unknown printer name")
)

type App struct {
	// Alive turns into false, mdqi will exit.
	Alive bool

	// cmdPath is path to mdq command.
	cmdPath string

	// historyPath is path to command history file for liner.
	historyPath string

	// slashCommandDefinition holds SlashCommandDefinition.
	// app.slashCommandDefinition[category][name] = SlashCommandDefinition
	slashCommandDefinition map[string]map[string]SlashCommandDefinition

	// tag stores tag value for --tag option of mdq.
	tag string

	printer Printer
}

type Result struct {
	Database string
	Columns  []string
	Rows     []map[string]interface{}
}

func init() {
	defaultOutput = os.Stdout
}

func NewApp(conf Conf) (*App, error) {
	// TODO: Check if mdq command exists by exec.LookPath.

	// create history file
	historyPath := conf.Mdqi.History
	if path := conf.Mdqi.History; path != "" {
		var err error
		if err = createHistoryFile(path); err != nil {
			return nil, errors.Wrapf(err, "failed to create history file at %s", path)
		}
		historyPath = path
		debug.Println("conf.Mdqi.History =", historyPath)
	}

	app := &App{
		Alive: true,

		cmdPath:                "mdq",
		historyPath:            historyPath,
		slashCommandDefinition: map[string]map[string]SlashCommandDefinition{},
		printer:                HorizontalPrinter{},
	}

	// set default tag
	if tag := conf.Mdqi.DefaultTag; tag != "" {
		app.SetTag(tag)
		debug.Println("conf (tag):", tag)
	}

	// set default display
	if display := conf.Mdqi.DefaultDisplay; display != "" {
		if err = app.SetPrinterByName(display); err != nil {
			return nil, errors.Wrap(err, "failed to set default printer")
		}

		debug.Println("conf (display):", display)
	}

	return app, nil
}

func createHistoryFile(path string) error {
	if _, err := os.Stat(path); os.IsNotExist(err) {
		if _, err := os.Create(path); err != nil {
			return errors.Wrap(err, "failed to create history file")
		}
	}

	return nil
}

func defaultHistoryPath() (string, error) {
	usr, err := user.Current()
	if err != nil {
		return "", errors.Wrap(err, "failed to get current user")
	}

	return filepath.Join(usr.HomeDir, defaultHistoryFilename), err
}

func (app *App) slashCommandCategories() []string {
	defs := app.slashCommandDefinition
	keys := make([]string, 0, len(defs))

	for key := range defs {
		keys = append(keys, key)
	}

	return keys
}

func (app *App) slashCommandNames(category string) []string {
	defs := app.slashCommandDefinition[category]
	keys := make([]string, 0, len(defs))

	for key := range defs {
		keys = append(keys, key)
	}

	return keys
}

func (app *App) Run() {
	app.runLiner()
}

func (app *App) runLiner() {
	line := liner.NewLiner()
	defer line.Close()

	line.SetCtrlCAborts(true)

	app.initHistory(line)

LOOP:
	for {
		if !app.Alive {
			fmt.Println("bye")
			break LOOP
		}

		l, err := line.Prompt("mdq> ")

		switch err {
		case nil:
			l = strings.Trim(l, " \n")

			if l == "" {
				continue
			}

			line.AppendHistory(l)

			scmd, _ := ParseSlashCommand(l)
			if scmd != nil {
				app.runSlashCommand(scmd)
				continue
			}

			results, err := app.RunCmd(l, app.buildCmdArgs()...)
			if err != nil {
				logger.Println(err.Error())
			}

			Print(app.printer, results)
		case liner.ErrPromptAborted:
			logger.Println("aborted")
			break LOOP
		case io.EOF:
			fmt.Println("bye")
			break LOOP
		default:
			logger.Println("error on reading line: ", err)
			break LOOP
		}

		app.saveHistory(line)
	}
}

func (app *App) initHistory(line *liner.State) {
	if f, err := os.Open(app.historyPath); err == nil {
		line.ReadHistory(f)
		f.Close()
	} else {
		logger.Println("failed to read command history: ", err)
	}
}

func (app *App) saveHistory(line *liner.State) {
	if f, err := os.Create(app.historyPath); err == nil {
		if _, err := line.WriteHistory(f); err != nil {
			logger.Println("failed to write history: ", err)
		}

		f.Close()
	} else {
		logger.Println("failed to create history file: ", err)
	}
}

func (app *App) buildCmdArgs() []string {
	args := []string{}

	// tag
	if app.tag != "" {
		args = append(args, "--tag="+app.tag)
	}

	return args
}

func (app *App) runSlashCommand(scmd *SlashCommand) {
	sdef, err := app.FindSlashCommandDefinition(scmd.Category, scmd.Name)

	switch err {
	case nil:
		if err := sdef.Handle(app, scmd); err != nil {
			logger.Println("failed to handle slash command:", err)
		}
	case ErrSlashCommandNotFound:
		logger.Println("unknown slash command")
	}

	return
}

func (app *App) SetPrinterByName(name string) error {

	switch name {
	case "horizontal":
		app.printer = HorizontalPrinter{}
	case "vertical":
		app.printer = VerticalPrinter{}
	default:
		return ErrUnknownPrinterName
	}

	return nil
}

func (app *App) GetTag() string {
	return app.tag
}

func (app *App) SetTag(tag string) {
	app.tag = tag
}

func (app *App) ClearTag() {
	app.tag = ""
}
<|endoftext|>


<commit_before>package main

import (
	"fmt"
	"html/template"
	"io"
	"net/http"
	"net/url"
	"strconv"
	"time"

	"golang.org/x/oauth2"
	"google.golang.org/api/plus/v1"

	"google-musicmanager-go"
)

var funcMap = map[string]interface{}{
	"incr":    func(i int) int { return i + 1 },
	"reverse": reversed,
	"time":    unix2Time,
}
var scopes = []string{musicmanager.MusicManagerScope, plus.PlusMeScope}
var conf = googleMustConfigFromFile("credentials.json", scopes...)
var tpls = template.Must(template.New("static").
	Funcs(funcMap).
	ParseGlob("static/*.tpl"))

func init() {
	http.Handle("/static/", http.FileServer(http.Dir(".")))
	http.Handle("/auth", &REST{Get: auth})
	http.Handle("/oauth2callback", &REST{Get: oauth2callback})
	http.Handle("/register", &REST{
		Init: initMusicManager,
		Get:  register,
	})
	http.Handle("/tracks/", &REST{
		Init:   initMusicManager,
		Get:    tracksGet,
		List:   tracksList,
		Insert: tracksInsert,
	})
	/*
		http.Handle("/jobs/", &REST{
			Init:   initMusicManager,
			List:   jobsList,
			Delete: jobsCancel,
		})
	*/
}

func auth(_ interface{}, w http.ResponseWriter, r *http.Request) error {
	state, err := nonce(32)
	if err != nil {
		return err
	}
	// BUG(lor): Google Play Music Web Manager ignores the
	// "redirect_uris" property of the credentials file.  Instead,
	// a redirect URL is always created dynamically by appending
	// "/oauth2callback" to the scheme and host of the domain on
	// which Google Play Music Web Manager received the request for
	// "/auth".  This is so that the redirect URL can be generated
	// correctly on the App Engine dev server.
	conf.RedirectURL = getRedirectURL(getContext(r))
	httpSetCookie(w, r, "state", state)
	httpSetCookie(w, r, "redirect", r.FormValue("redirect"))
	http.Redirect(w, r, conf.AuthCodeURL(state), http.StatusFound)
	return nil
}

func oauth2callback(_ interface{}, w http.ResponseWriter, r *http.Request) error {
	// Confirm that the state matches the nonce we stored
	// (See https://tools.ietf.org/html/rfc6749#section-10.12.)
	rstate := r.FormValue("state")
	astate, err := r.Cookie("state")
	if err != nil || rstate != astate.Value {
		return &RESTError{
			Code: http.StatusBadRequest,
			Message: "state parameter and cookie mismatch" +
				"; have you perhaps disabled cookies?",
		}
	}
	// Exchange the authorization code for an access token.
	c := getContext(r)
	tok, err := conf.Exchange(c, r.FormValue("code"))
	if err != nil {
		return err
	}
	// BUG(lor): Google Play Music does not allow downloading tracks
	// with an uploader_id that is not sufficiently
	// "MAC address-like" (perhaps it only checks for a colon?)
	// The /oauth2callback endpoint generates the uploader_id by
	// injecting a colon between every two digits of the user's
	// Google Account ID, which appears to suffice.
	client := conf.Client(c, tok)
	plus, err := plus.New(client)
	if err != nil {
		return err
	}
	person, err := plus.People.Get("me").Do()
	if err != nil {
		return err
	}
	id, err := regexpReplaceAllString(`(..)`, person.Id, "$1:")
	if err != nil {
		return err
	}
	// Wipe the state and redirect cookies, no longer necessary,
	// and store the access token and uploader ID as cookies.
	httpSetCookie(w, r, "state", "")
	httpSetCookie(w, r, "redirect", "")
	httpSetCookie(w, r, "access_token", tok.AccessToken)
	httpSetCookie(w, r, "uploader_id", id)
	// We autoredirect to the registration endpoint for convenience.
	// If the redirect cookie was provided, remember to tell the
	// registration endpoint to continue there after success.
	registerURL := "/register"
	redirect, err := r.Cookie("redirect")
	if err == nil && redirect.Value != "" {
		registerURL += "?redirect=" + url.QueryEscape(redirect.Value)
	}
	http.Redirect(w, r, registerURL, http.StatusFound)
	return nil
}

func initMusicManager(r *http.Request) (interface{}, error) {
	// If either the access token or uploader ID cookie is missing,
	// autoredirect to the start of the authorization flow rather
	// than just report an error.  The redirect parameter lets the
	// user continue right where they left off.

	// BUG(lor): If the access_token cookie expires just before
	// uploading new tracks, they will need to be resubmitted after
	// the auth flow has finished, as it cannot preserve POST data.
	tok, _ := r.Cookie("access_token")
	id, _ := r.Cookie("uploader_id")
	if tok == nil || id == nil {
		path := url.QueryEscape(r.URL.Path + "?" + r.URL.RawQuery)
		return nil, &RESTError{
			Code:     http.StatusFound,
			Message:  "missing credentials",
			Location: "/auth?redirect=" + path,
		}
	}
	// Create and return a new Music Manager service.  On App
	// Engine, fixTransport turns off SSL verification for the
	// transport so that access to the android.clients.google.com
	// server works fine.
	c := getContext(r)
	client := conf.Client(c, &oauth2.Token{AccessToken: tok.Value})
	fixTransport(client.Transport.(*oauth2.Transport).Base)
	return musicmanager.New(client, id.Value)
}

func register(client interface{}, w http.ResponseWriter, r *http.Request) error {
	name := r.FormValue("name")
	if name == "" {
		name = "Google Play Music Web Manager"
	}
	err := client.(*musicmanager.Service).Register(name).Do()
	if err != nil {
		return err
	}
	if redirect := r.FormValue("redirect"); redirect != "" {
		http.Redirect(w, r, redirect, http.StatusFound)
	}
	fmt.Fprintln(w, musicmanager.GetRegisterError("OK"))
	return nil
}

func tracksGet(client interface{}, w http.ResponseWriter, r *http.Request) error {
	id := r.URL.Path
	track, err := client.(*musicmanager.Service).Tracks.Get(id).Do()
	if err != nil {
		return err
	}
	if name := track.Name(); name != "" {
		name = url.QueryEscape(name)
		// url.QueryEscape encodes spaces as plus signs, which
		// popular browsers don't understand.  Try to
		// percent-encode them instead.
		if tmp, err := regexpReplaceAllString(`\+`, name, "%20"); err == nil {
			name = tmp
		}
		name = "attachment; filename*=UTF8-''" + name
		w.Header().Set("Content-Disposition", name)
	}
	if size := track.Size(); size > 0 {
		size := strconv.FormatInt(size, 10)
		w.Header().Set("Content-Length", size)
	}
	w.Header().Set("Content-Type", "audio/mpeg")
	io.Copy(w, track)
	return nil
}

func tracksList(client interface{}, w http.ResponseWriter, r *http.Request) error {
	list := client.(*musicmanager.Service).Tracks.List()
	// Parse the options.
	if t, err := time.Parse(time.RFC3339Nano, r.FormValue("updatedMin")); err == nil {
		list.UpdatedMin(t.UnixNano()/1000)
	}
	purchasedOnly, err := strconv.ParseBool(r.FormValue("purchasedOnly"))
	if err == nil {
		list.PurchasedOnly(purchasedOnly)
	}
	if pageToken := r.FormValue("pageToken"); pageToken != "" {
		list.PageToken(pageToken)
	}
	// Execute the query.
	res, err := list.Do()
	if err != nil {
		return err
	}
	// Print the results.
	// GetTracksToExportResponse doesn't report back whether it
	// was obtained with *ExportType == ALL or
	// *ExportType == PURCHASED_AND_PROMOTIONAL, and I don't want
	// to define a new type just to pass this information to the
	// template.  Fortunately, the Go protobuf compiler includes
	// an XXX_unrecognized []byte field with every struct, which
	// is perfect for smuggling this information to the template.
	res.XXX_unrecognized = nil
	if purchasedOnly {
		res.XXX_unrecognized = []byte("true")
	}
	return tpls.ExecuteTemplate(w, "list.tpl", res)
}

func tracksInsert(client interface{}, w http.ResponseWriter, r *http.Request) error {
	f, _, err := r.FormFile("track")
	if err != nil {
		return err
	}
	defer f.Close()
	if err := checkSize(f); err != nil {
		return err
	}
	serverID, err := client.(*musicmanager.Service).Tracks.Insert(f).Do()
	if err != nil {
		return err
	}
	http.Redirect(w, r, "/tracks/", http.StatusFound)
	fmt.Fprintln(w, serverID)
	return nil
}

/*
func jobsList(client interface{}, w http.ResponseWriter, r *http.Request) error {
	jobs, err := client.(*musicmanager.Service).Jobs.List().Do()
	if err != nil {
		return err
	}
	for _, job := range jobs {
		fmt.Fprintf(w, "%+v\n", job)
	}
	return nil
}

func jobsCancel(client interface{}, w http.ResponseWriter, r *http.Request) error {
	err := client.(*musicmanager.Service).Jobs.Cancel().Do()
	if err != nil {
		return err
	}
	http.Redirect(w, r, "/jobs/", http.StatusSeeOther)
	return nil
}
*/
<commit_msg>Remove commented-out Jobs service stub<commit_after>package main

import (
	"fmt"
	"html/template"
	"io"
	"net/http"
	"net/url"
	"strconv"
	"time"

	"golang.org/x/oauth2"
	"google.golang.org/api/plus/v1"

	"google-musicmanager-go"
)

var funcMap = map[string]interface{}{
	"incr":    func(i int) int { return i + 1 },
	"reverse": reversed,
	"time":    unix2Time,
}
var scopes = []string{musicmanager.MusicManagerScope, plus.PlusMeScope}
var conf = googleMustConfigFromFile("credentials.json", scopes...)
var tpls = template.Must(template.New("static").
	Funcs(funcMap).
	ParseGlob("static/*.tpl"))

func init() {
	http.Handle("/static/", http.FileServer(http.Dir(".")))
	http.Handle("/auth", &REST{Get: auth})
	http.Handle("/oauth2callback", &REST{Get: oauth2callback})
	http.Handle("/register", &REST{
		Init: initMusicManager,
		Get:  register,
	})
	http.Handle("/tracks/", &REST{
		Init:   initMusicManager,
		Get:    tracksGet,
		List:   tracksList,
		Insert: tracksInsert,
	})
}

func auth(_ interface{}, w http.ResponseWriter, r *http.Request) error {
	state, err := nonce(32)
	if err != nil {
		return err
	}
	// BUG(lor): Google Play Music Web Manager ignores the
	// "redirect_uris" property of the credentials file.  Instead,
	// a redirect URL is always created dynamically by appending
	// "/oauth2callback" to the scheme and host of the domain on
	// which Google Play Music Web Manager received the request for
	// "/auth".  This is so that the redirect URL can be generated
	// correctly on the App Engine dev server.
	conf.RedirectURL = getRedirectURL(getContext(r))
	httpSetCookie(w, r, "state", state)
	httpSetCookie(w, r, "redirect", r.FormValue("redirect"))
	http.Redirect(w, r, conf.AuthCodeURL(state), http.StatusFound)
	return nil
}

func oauth2callback(_ interface{}, w http.ResponseWriter, r *http.Request) error {
	// Confirm that the state matches the nonce we stored
	// (See https://tools.ietf.org/html/rfc6749#section-10.12.)
	rstate := r.FormValue("state")
	astate, err := r.Cookie("state")
	if err != nil || rstate != astate.Value {
		return &RESTError{
			Code: http.StatusBadRequest,
			Message: "state parameter and cookie mismatch" +
				"; have you perhaps disabled cookies?",
		}
	}
	// Exchange the authorization code for an access token.
	c := getContext(r)
	tok, err := conf.Exchange(c, r.FormValue("code"))
	if err != nil {
		return err
	}
	// BUG(lor): Google Play Music does not allow downloading tracks
	// with an uploader_id that is not sufficiently
	// "MAC address-like" (perhaps it only checks for a colon?)
	// The /oauth2callback endpoint generates the uploader_id by
	// injecting a colon between every two digits of the user's
	// Google Account ID, which appears to suffice.
	client := conf.Client(c, tok)
	plus, err := plus.New(client)
	if err != nil {
		return err
	}
	person, err := plus.People.Get("me").Do()
	if err != nil {
		return err
	}
	id, err := regexpReplaceAllString(`(..)`, person.Id, "$1:")
	if err != nil {
		return err
	}
	// Wipe the state and redirect cookies, no longer necessary,
	// and store the access token and uploader ID as cookies.
	httpSetCookie(w, r, "state", "")
	httpSetCookie(w, r, "redirect", "")
	httpSetCookie(w, r, "access_token", tok.AccessToken)
	httpSetCookie(w, r, "uploader_id", id)
	// We autoredirect to the registration endpoint for convenience.
	// If the redirect cookie was provided, remember to tell the
	// registration endpoint to continue there after success.
	registerURL := "/register"
	redirect, err := r.Cookie("redirect")
	if err == nil && redirect.Value != "" {
		registerURL += "?redirect=" + url.QueryEscape(redirect.Value)
	}
	http.Redirect(w, r, registerURL, http.StatusFound)
	return nil
}

func initMusicManager(r *http.Request) (interface{}, error) {
	// If either the access token or uploader ID cookie is missing,
	// autoredirect to the start of the authorization flow rather
	// than just report an error.  The redirect parameter lets the
	// user continue right where they left off.

	// BUG(lor): If the access_token cookie expires just before
	// uploading new tracks, they will need to be resubmitted after
	// the auth flow has finished, as it cannot preserve POST data.
	tok, _ := r.Cookie("access_token")
	id, _ := r.Cookie("uploader_id")
	if tok == nil || id == nil {
		path := url.QueryEscape(r.URL.Path + "?" + r.URL.RawQuery)
		return nil, &RESTError{
			Code:     http.StatusFound,
			Message:  "missing credentials",
			Location: "/auth?redirect=" + path,
		}
	}
	// Create and return a new Music Manager service.  On App
	// Engine, fixTransport turns off SSL verification for the
	// transport so that access to the android.clients.google.com
	// server works fine.
	c := getContext(r)
	client := conf.Client(c, &oauth2.Token{AccessToken: tok.Value})
	fixTransport(client.Transport.(*oauth2.Transport).Base)
	return musicmanager.New(client, id.Value)
}

func register(client interface{}, w http.ResponseWriter, r *http.Request) error {
	name := r.FormValue("name")
	if name == "" {
		name = "Google Play Music Web Manager"
	}
	err := client.(*musicmanager.Service).Register(name).Do()
	if err != nil {
		return err
	}
	if redirect := r.FormValue("redirect"); redirect != "" {
		http.Redirect(w, r, redirect, http.StatusFound)
	}
	fmt.Fprintln(w, musicmanager.GetRegisterError("OK"))
	return nil
}

func tracksGet(client interface{}, w http.ResponseWriter, r *http.Request) error {
	id := r.URL.Path
	track, err := client.(*musicmanager.Service).Tracks.Get(id).Do()
	if err != nil {
		return err
	}
	if name := track.Name(); name != "" {
		name = url.QueryEscape(name)
		// url.QueryEscape encodes spaces as plus signs, which
		// popular browsers don't understand.  Try to
		// percent-encode them instead.
		if tmp, err := regexpReplaceAllString(`\+`, name, "%20"); err == nil {
			name = tmp
		}
		name = "attachment; filename*=UTF8-''" + name
		w.Header().Set("Content-Disposition", name)
	}
	if size := track.Size(); size > 0 {
		size := strconv.FormatInt(size, 10)
		w.Header().Set("Content-Length", size)
	}
	w.Header().Set("Content-Type", "audio/mpeg")
	io.Copy(w, track)
	return nil
}

func tracksList(client interface{}, w http.ResponseWriter, r *http.Request) error {
	list := client.(*musicmanager.Service).Tracks.List()
	// Parse the options.
	if t, err := time.Parse(time.RFC3339Nano, r.FormValue("updatedMin")); err == nil {
		list.UpdatedMin(t.UnixNano() / 1000)
	}
	purchasedOnly, err := strconv.ParseBool(r.FormValue("purchasedOnly"))
	if err == nil {
		list.PurchasedOnly(purchasedOnly)
	}
	if pageToken := r.FormValue("pageToken"); pageToken != "" {
		list.PageToken(pageToken)
	}
	// Execute the query.
	res, err := list.Do()
	if err != nil {
		return err
	}
	// Print the results.
	// GetTracksToExportResponse doesn't report back whether it
	// was obtained with *ExportType == ALL or
	// *ExportType == PURCHASED_AND_PROMOTIONAL, and I don't want
	// to define a new type just to pass this information to the
	// template.  Fortunately, the Go protobuf compiler includes
	// an XXX_unrecognized []byte field with every struct, which
	// is perfect for smuggling this information to the template.
	res.XXX_unrecognized = nil
	if purchasedOnly {
		res.XXX_unrecognized = []byte("true")
	}
	return tpls.ExecuteTemplate(w, "list.tpl", res)
}

func tracksInsert(client interface{}, w http.ResponseWriter, r *http.Request) error {
	f, _, err := r.FormFile("track")
	if err != nil {
		return err
	}
	defer f.Close()
	if err := checkSize(f); err != nil {
		return err
	}
	serverID, err := client.(*musicmanager.Service).Tracks.Insert(f).Do()
	if err != nil {
		return err
	}
	http.Redirect(w, r, "/tracks/", http.StatusFound)
	fmt.Fprintln(w, serverID)
	return nil
}
<|endoftext|>


<commit_before>package main

import (
	"net/http"
	"os"

	"fmt"
	"strconv"
	"time"

	"github.com/Financial-Times/base-ft-rw-app-go/baseftrwapp"
	"github.com/Financial-Times/go-fthealth/v1a"
	"github.com/Financial-Times/http-handlers-go/httphandlers"
	"github.com/Financial-Times/neo-utils-go/neoutils"
	"github.com/Financial-Times/public-people-api/people"
	status "github.com/Financial-Times/service-status-go/httphandlers"
	log "github.com/Sirupsen/logrus"
	"github.com/gorilla/mux"
	"github.com/jawher/mow.cli"
	"github.com/rcrowley/go-metrics"
)

func main() {
	app := cli.App("public-people-api-neo4j", "A public RESTful API for accessing People in neo4j")
	neoURL := app.String(cli.StringOpt{
		Name:   "neo-url",
		Value:  "http://localhost:7474/db/data",
		Desc:   "neo4j endpoint URL",
		EnvVar: "NEO_URL",
	})
	logLevel := app.String(cli.StringOpt{
		Name:   "log-level",
		Value:  "INFO",
		Desc:   "Log level to use",
		EnvVar: "LOG_LEVEL",
	})
	port := app.String(cli.StringOpt{
		Name:   "port",
		Value:  "8080",
		Desc:   "Port to listen on",
		EnvVar: "APP_PORT",
	})
	graphiteTCPAddress := app.String(cli.StringOpt{
		Name:   "graphiteTCPAddress",
		Value:  "",
		Desc:   "Graphite TCP address, e.g. graphite.ft.com:2003. Leave as default if you do NOT want to output to graphite (e.g. if running locally)",
		EnvVar: "GRAPHITE_ADDRESS",
	})
	graphitePrefix := app.String(cli.StringOpt{
		Name:   "graphitePrefix",
		Value:  "",
		Desc:   "Prefix to use. Should start with content, include the environment, and the host name. e.g. content.test.public.content.by.concept.api.ftaps59382-law1a-eu-t",
		EnvVar: "GRAPHITE_PREFIX",
	})
	logMetrics := app.Bool(cli.BoolOpt{
		Name:   "logMetrics",
		Value:  false,
		Desc:   "Whether to log metrics. Set to true if running locally and you want metrics output",
		EnvVar: "LOG_METRICS",
	})
	env := app.String(cli.StringOpt{
		Name:  "env",
		Value: "local",
		Desc:  "environment this app is running in",
	})
	cacheDuration := app.String(cli.StringOpt{
		Name:   "cache-duration",
		Value:  "30s",
		Desc:   "Duration Get requests should be cached for. e.g. 2h45m would set the max-age value to '7440' seconds",
		EnvVar: "CACHE_DURATION",
	})

	app.Action = func() {
		parsedLogLevel, err := log.ParseLevel(*logLevel)
		if err != nil {
			log.WithFields(log.Fields{"logLevel": logLevel, "err": err}).Fatal("Incorrect log level")
		}
		log.SetLevel(parsedLogLevel)

		baseftrwapp.OutputMetricsIfRequired(*graphiteTCPAddress, *graphitePrefix, *logMetrics)

		log.Infof("public-people-api will listen on port: %s, connecting to: %s", *port, *neoURL)
		runServer(*neoURL, *port, *cacheDuration, *env)
	}
	log.Infof("Application started with args %s", os.Args)
	app.Run(os.Args)
}

func runServer(neoURL string, port string, cacheDuration string, env string) {

	if duration, durationErr := time.ParseDuration(cacheDuration); durationErr != nil {
		log.Fatalf("Failed to parse cache duration string, %v", durationErr)
	} else {
		people.CacheControlHeader = fmt.Sprintf("max-age=%s, public", strconv.FormatFloat(duration.Seconds(), 'f', 0, 64))
	}

	conf := neoutils.ConnectionConfig{
		BatchSize:     1024,
		Transactional: false,
		HTTPClient: &http.Client{
			Transport: &http.Transport{
				MaxIdleConnsPerHost: 100,
			},
			Timeout: 1 * time.Minute,
		},
		BackgroundConnect: true,
	}
	db, err := neoutils.Connect(neoURL, &conf)

	if err != nil {
		log.Fatalf("Error connecting to neo4j %s", err)
	}

	people.PeopleDriver = people.NewCypherDriver(db, env)

	servicesRouter := mux.NewRouter()

	// Health checks and standards first
	servicesRouter.HandleFunc("/__health", v1a.Handler("PeopleReadWriteNeo4j Healthchecks",
		"Checks for accessing neo4j", people.HealthCheck()))

	// Then API specific ones:
	servicesRouter.HandleFunc("/people/{uuid}", people.GetPerson).Methods("GET")
	servicesRouter.HandleFunc("/people/{uuid}", people.MethodNotAllowedHandler)

	var monitoringRouter http.Handler = servicesRouter
	monitoringRouter = httphandlers.TransactionAwareRequestLoggingHandler(log.StandardLogger(), monitoringRouter)
	monitoringRouter = httphandlers.HTTPMetricsHandler(metrics.DefaultRegistry, monitoringRouter)

	// The following endpoints should not be monitored or logged (varnish calls one of these every second, depending on config)
	// The top one of these build info endpoints feels more correct, but the lower one matches what we have in Dropwizard,
	// so it's what apps expect currently same as ping, the content of build-info needs more definition
	http.HandleFunc(status.PingPath, status.PingHandler)
	http.HandleFunc(status.PingPathDW, status.PingHandler)
	http.HandleFunc(status.BuildInfoPath, status.BuildInfoHandler)
	http.HandleFunc(status.BuildInfoPathDW, status.BuildInfoHandler)
	http.HandleFunc("/__gtg", people.GoodToGo)
	http.Handle("/", monitoringRouter)

	if err := http.ListenAndServe(":"+port, nil); err != nil {
		log.Fatalf("Unable to start server: %v", err)
	}
}
<commit_msg>Updated the to sort the log level setting out<commit_after>package main

import (
	"net/http"
	"os"

	"fmt"
	"strconv"
	"time"

	"github.com/Financial-Times/base-ft-rw-app-go/baseftrwapp"
	"github.com/Financial-Times/go-fthealth/v1a"
	"github.com/Financial-Times/http-handlers-go/httphandlers"
	"github.com/Financial-Times/neo-utils-go/neoutils"
	"github.com/Financial-Times/public-people-api/people"
	status "github.com/Financial-Times/service-status-go/httphandlers"
	log "github.com/Sirupsen/logrus"
	"github.com/gorilla/mux"
	"github.com/jawher/mow.cli"
	"github.com/rcrowley/go-metrics"
)

func main() {
	app := cli.App("public-people-api-neo4j", "A public RESTful API for accessing People in neo4j")
	neoURL := app.String(cli.StringOpt{
		Name:   "neo-url",
		Value:  "http://localhost:7474/db/data",
		Desc:   "neo4j endpoint URL",
		EnvVar: "NEO_URL",
	})
	logLevel := app.String(cli.StringOpt{
		Name:   "log-level",
		Value:  "INFO",
		Desc:   "Log level to use",
		EnvVar: "LOG_LEVEL",
	})
	port := app.String(cli.StringOpt{
		Name:   "port",
		Value:  "8080",
		Desc:   "Port to listen on",
		EnvVar: "APP_PORT",
	})
	graphiteTCPAddress := app.String(cli.StringOpt{
		Name:   "graphiteTCPAddress",
		Value:  "",
		Desc:   "Graphite TCP address, e.g. graphite.ft.com:2003. Leave as default if you do NOT want to output to graphite (e.g. if running locally)",
		EnvVar: "GRAPHITE_ADDRESS",
	})
	graphitePrefix := app.String(cli.StringOpt{
		Name:   "graphitePrefix",
		Value:  "",
		Desc:   "Prefix to use. Should start with content, include the environment, and the host name. e.g. content.test.public.content.by.concept.api.ftaps59382-law1a-eu-t",
		EnvVar: "GRAPHITE_PREFIX",
	})
	logMetrics := app.Bool(cli.BoolOpt{
		Name:   "logMetrics",
		Value:  false,
		Desc:   "Whether to log metrics. Set to true if running locally and you want metrics output",
		EnvVar: "LOG_METRICS",
	})
	env := app.String(cli.StringOpt{
		Name:  "env",
		Value: "local",
		Desc:  "environment this app is running in",
	})
	cacheDuration := app.String(cli.StringOpt{
		Name:   "cache-duration",
		Value:  "30s",
		Desc:   "Duration Get requests should be cached for. e.g. 2h45m would set the max-age value to '7440' seconds",
		EnvVar: "CACHE_DURATION",
	})

	app.Action = func() {
		lvl, err := log.ParseLevel(*logLevel)
		if err != nil {
			log.Warnf("Log level %s could not be parsed, defaulting to info")
			lvl = log.InfoLevel
		}
		log.SetLevel(lvl)
		log.Info(lvl.String() + ": log level set")
		log.SetFormatter(&log.JSONFormatter{})

		baseftrwapp.OutputMetricsIfRequired(*graphiteTCPAddress, *graphitePrefix, *logMetrics)

		log.Infof("public-people-api will listen on port: %s, connecting to: %s", *port, *neoURL)
		runServer(*neoURL, *port, *cacheDuration, *env)
	}
	log.Infof("Application started with args %s", os.Args)
	app.Run(os.Args)
}

func runServer(neoURL string, port string, cacheDuration string, env string) {

	if duration, durationErr := time.ParseDuration(cacheDuration); durationErr != nil {
		log.Fatalf("Failed to parse cache duration string, %v", durationErr)
	} else {
		people.CacheControlHeader = fmt.Sprintf("max-age=%s, public", strconv.FormatFloat(duration.Seconds(), 'f', 0, 64))
	}

	conf := neoutils.ConnectionConfig{
		BatchSize:     1024,
		Transactional: false,
		HTTPClient: &http.Client{
			Transport: &http.Transport{
				MaxIdleConnsPerHost: 100,
			},
			Timeout: 1 * time.Minute,
		},
		BackgroundConnect: true,
	}
	db, err := neoutils.Connect(neoURL, &conf)

	if err != nil {
		log.Fatalf("Error connecting to neo4j %s", err)
	}

	people.PeopleDriver = people.NewCypherDriver(db, env)

	servicesRouter := mux.NewRouter()

	// Health checks and standards first
	servicesRouter.HandleFunc("/__health", v1a.Handler("PeopleReadWriteNeo4j Healthchecks",
		"Checks for accessing neo4j", people.HealthCheck()))

	// Then API specific ones:
	servicesRouter.HandleFunc("/people/{uuid}", people.GetPerson).Methods("GET")
	servicesRouter.HandleFunc("/people/{uuid}", people.MethodNotAllowedHandler)

	var monitoringRouter http.Handler = servicesRouter
	monitoringRouter = httphandlers.TransactionAwareRequestLoggingHandler(log.StandardLogger(), monitoringRouter)
	monitoringRouter = httphandlers.HTTPMetricsHandler(metrics.DefaultRegistry, monitoringRouter)

	// The following endpoints should not be monitored or logged (varnish calls one of these every second, depending on config)
	// The top one of these build info endpoints feels more correct, but the lower one matches what we have in Dropwizard,
	// so it's what apps expect currently same as ping, the content of build-info needs more definition
	http.HandleFunc(status.PingPath, status.PingHandler)
	http.HandleFunc(status.PingPathDW, status.PingHandler)
	http.HandleFunc(status.BuildInfoPath, status.BuildInfoHandler)
	http.HandleFunc(status.BuildInfoPathDW, status.BuildInfoHandler)
	http.HandleFunc("/__gtg", people.GoodToGo)
	http.Handle("/", monitoringRouter)

	if err := http.ListenAndServe(":"+port, nil); err != nil {
		log.Fatalf("Unable to start server: %v", err)
	}
}
<|endoftext|>