1package couchbase
2
3import (
4	"encoding/json"
5	"errors"
6	"fmt"
7	"io/ioutil"
8	"math/rand"
9	"net/http"
10	"net/url"
11	"time"
12)
13
14// ViewRow represents a single result from a view.
15//
16// Doc is present only if include_docs was set on the request.
17type ViewRow struct {
18	ID    string
19	Key   interface{}
20	Value interface{}
21	Doc   *interface{}
22}
23
24// A ViewError is a node-specific error indicating a partial failure
25// within a view result.
26type ViewError struct {
27	From   string
28	Reason string
29}
30
31func (ve ViewError) Error() string {
32	return "Node: " + ve.From + ", reason: " + ve.Reason
33}
34
35// ViewResult holds the entire result set from a view request,
36// including the rows and the errors.
37type ViewResult struct {
38	TotalRows int `json:"total_rows"`
39	Rows      []ViewRow
40	Errors    []ViewError
41}
42
43func (b *Bucket) randomBaseURL() (*url.URL, error) {
44	nodes := b.HealthyNodes()
45	if len(nodes) == 0 {
46		return nil, errors.New("no available couch rest URLs")
47	}
48	nodeNo := rand.Intn(len(nodes))
49	node := nodes[nodeNo]
50
51	b.RLock()
52	name := b.Name
53	pool := b.pool
54	b.RUnlock()
55
56	u, err := ParseURL(node.CouchAPIBase)
57	if err != nil {
58		return nil, fmt.Errorf("config error: Bucket %q node #%d CouchAPIBase=%q: %v",
59			name, nodeNo, node.CouchAPIBase, err)
60	} else if pool != nil {
61		u.User = pool.client.BaseURL.User
62	}
63	return u, err
64}
65
66const START_NODE_ID = -1
67
68func (b *Bucket) randomNextURL(lastNode int) (*url.URL, int, error) {
69	nodes := b.HealthyNodes()
70	if len(nodes) == 0 {
71		return nil, -1, errors.New("no available couch rest URLs")
72	}
73
74	var nodeNo int
75	if lastNode == START_NODE_ID || lastNode >= len(nodes) {
76		// randomly select a node if the value of lastNode is invalid
77		nodeNo = rand.Intn(len(nodes))
78	} else {
79		// wrap around the node list
80		nodeNo = (lastNode + 1) % len(nodes)
81	}
82
83	b.RLock()
84	name := b.Name
85	pool := b.pool
86	b.RUnlock()
87
88	node := nodes[nodeNo]
89	u, err := ParseURL(node.CouchAPIBase)
90	if err != nil {
91		return nil, -1, fmt.Errorf("config error: Bucket %q node #%d CouchAPIBase=%q: %v",
92			name, nodeNo, node.CouchAPIBase, err)
93	} else if pool != nil {
94		u.User = pool.client.BaseURL.User
95	}
96	return u, nodeNo, err
97}
98
99// DocID is the document ID type for the startkey_docid parameter in
100// views.
101type DocID string
102
103func qParam(k, v string) string {
104	format := `"%s"`
105	switch k {
106	case "startkey_docid", "endkey_docid", "stale":
107		format = "%s"
108	}
109	return fmt.Sprintf(format, v)
110}
111
112// ViewURL constructs a URL for a view with the given ddoc, view name,
113// and parameters.
114func (b *Bucket) ViewURL(ddoc, name string,
115	params map[string]interface{}) (string, error) {
116	u, err := b.randomBaseURL()
117	if err != nil {
118		return "", err
119	}
120
121	values := url.Values{}
122	for k, v := range params {
123		switch t := v.(type) {
124		case DocID:
125			values[k] = []string{string(t)}
126		case string:
127			values[k] = []string{qParam(k, t)}
128		case int:
129			values[k] = []string{fmt.Sprintf(`%d`, t)}
130		case bool:
131			values[k] = []string{fmt.Sprintf(`%v`, t)}
132		default:
133			b, err := json.Marshal(v)
134			if err != nil {
135				return "", fmt.Errorf("unsupported value-type %T in Query, "+
136					"json encoder said %v", t, err)
137			}
138			values[k] = []string{fmt.Sprintf(`%v`, string(b))}
139		}
140	}
141
142	if ddoc == "" && name == "_all_docs" {
143		u.Path = fmt.Sprintf("/%s/_all_docs", b.GetName())
144	} else {
145		u.Path = fmt.Sprintf("/%s/_design/%s/_view/%s", b.GetName(), ddoc, name)
146	}
147	u.RawQuery = values.Encode()
148
149	return u.String(), nil
150}
151
152// ViewCallback is called for each view invocation.
153var ViewCallback func(ddoc, name string, start time.Time, err error)
154
155// ViewCustom performs a view request that can map row values to a
156// custom type.
157//
158// See the source to View for an example usage.
159func (b *Bucket) ViewCustom(ddoc, name string, params map[string]interface{},
160	vres interface{}) (err error) {
161	if SlowServerCallWarningThreshold > 0 {
162		defer slowLog(time.Now(), "call to ViewCustom(%q, %q)", ddoc, name)
163	}
164
165	if ViewCallback != nil {
166		defer func(t time.Time) { ViewCallback(ddoc, name, t, err) }(time.Now())
167	}
168
169	u, err := b.ViewURL(ddoc, name, params)
170	if err != nil {
171		return err
172	}
173
174	req, err := http.NewRequest("GET", u, nil)
175	if err != nil {
176		return err
177	}
178
179	ah := b.authHandler(false /* bucket not yet locked */)
180	maybeAddAuth(req, ah)
181
182	res, err := doHTTPRequest(req)
183	if err != nil {
184		return fmt.Errorf("error starting view req at %v: %v", u, err)
185	}
186	defer res.Body.Close()
187
188	if res.StatusCode != 200 {
189		bod := make([]byte, 512)
190		l, _ := res.Body.Read(bod)
191		return fmt.Errorf("error executing view req at %v: %v - %s",
192			u, res.Status, bod[:l])
193	}
194
195	body, err := ioutil.ReadAll(res.Body)
196	if err := json.Unmarshal(body, vres); err != nil {
197		return nil
198	}
199
200	return nil
201}
202
203// View executes a view.
204//
205// The ddoc parameter is just the bare name of your design doc without
206// the "_design/" prefix.
207//
208// Parameters are string keys with values that correspond to couchbase
209// view parameters.  Primitive should work fairly naturally (booleans,
210// ints, strings, etc...) and other values will attempt to be JSON
211// marshaled (useful for array indexing on on view keys, for example).
212//
213// Example:
214//
215//   res, err := couchbase.View("myddoc", "myview", map[string]interface{}{
216//       "group_level": 2,
217//       "startkey_docid":    []interface{}{"thing"},
218//       "endkey_docid":      []interface{}{"thing", map[string]string{}},
219//       "stale": false,
220//       })
221func (b *Bucket) View(ddoc, name string, params map[string]interface{}) (ViewResult, error) {
222	vres := ViewResult{}
223
224	if err := b.ViewCustom(ddoc, name, params, &vres); err != nil {
225		//error in accessing views. Retry once after a bucket refresh
226		b.Refresh()
227		return vres, b.ViewCustom(ddoc, name, params, &vres)
228	} else {
229		return vres, nil
230	}
231}
232