1package main
2
3import (
4	"net/http"
5	"time"
6
7	"github.com/emicklei/go-restful"
8	"github.com/emicklei/go-restful-swagger12"
9	"google.golang.org/appengine"
10	"google.golang.org/appengine/datastore"
11	"google.golang.org/appengine/user"
12)
13
14// This example demonstrates a reasonably complete suite of RESTful operations backed
15// by DataStore on Google App Engine.
16
17// Our simple example struct.
18type Profile struct {
19	LastModified time.Time `json:"-" xml:"-"`
20	Email        string    `json:"-" xml:"-"`
21	FirstName    string    `json:"first_name" xml:"first-name"`
22	NickName     string    `json:"nick_name" xml:"nick-name"`
23	LastName     string    `json:"last_name" xml:"last-name"`
24}
25
26type ProfileApi struct {
27	Path string
28}
29
30func gaeUrl() string {
31	if appengine.IsDevAppServer() {
32		return "http://localhost:8080"
33	} else {
34		// Include your URL on App Engine here.
35		// I found no way to get AppID without appengine.Context and this always
36		// based on a http.Request.
37		return "http://federatedservices.appspot.com"
38	}
39}
40
41func init() {
42	u := ProfileApi{Path: "/profiles"}
43	u.register()
44
45	// Optionally, you can install the Swagger Service which provides a nice Web UI on your REST API
46	// You need to download the Swagger HTML5 assets and change the FilePath location in the config below.
47	// Open <your_app_id>.appspot.com/apidocs and enter
48	// Place the Swagger UI files into a folder called static/swagger if you wish to use Swagger
49	// http://<your_app_id>.appspot.com/apidocs.json in the api input field.
50	// For testing, you can use http://localhost:8080/apidocs.json
51	config := swagger.Config{
52		// You control what services are visible
53		WebServices:    restful.RegisteredWebServices(),
54		WebServicesUrl: gaeUrl(),
55		ApiPath:        "/apidocs.json",
56
57		// Optionally, specify where the UI is located
58		SwaggerPath: "/apidocs/",
59
60		// GAE support static content which is configured in your app.yaml.
61		// This example expect the swagger-ui in static/swagger so you should place it there :)
62		SwaggerFilePath: "static/swagger"}
63	swagger.InstallSwaggerService(config)
64}
65
66func (u ProfileApi) register() {
67	ws := new(restful.WebService)
68
69	ws.
70		Path(u.Path).
71		// You can specify consumes and produces per route as well.
72		Consumes(restful.MIME_JSON, restful.MIME_XML).
73		Produces(restful.MIME_JSON, restful.MIME_XML)
74
75	ws.Route(ws.POST("").To(u.insert).
76		// Swagger documentation.
77		Doc("insert a new profile").
78		Param(ws.BodyParameter("Profile", "representation of a profile").DataType("main.Profile")).
79		Reads(Profile{}))
80
81	ws.Route(ws.GET("/{profile-id}").To(u.read).
82		// Swagger documentation.
83		Doc("read a profile").
84		Param(ws.PathParameter("profile-id", "identifier for a profile").DataType("string")).
85		Writes(Profile{}))
86
87	ws.Route(ws.PUT("/{profile-id}").To(u.update).
88		// Swagger documentation.
89		Doc("update an existing profile").
90		Param(ws.PathParameter("profile-id", "identifier for a profile").DataType("string")).
91		Param(ws.BodyParameter("Profile", "representation of a profile").DataType("main.Profile")).
92		Reads(Profile{}))
93
94	ws.Route(ws.DELETE("/{profile-id}").To(u.remove).
95		// Swagger documentation.
96		Doc("remove a profile").
97		Param(ws.PathParameter("profile-id", "identifier for a profile").DataType("string")))
98
99	restful.Add(ws)
100}
101
102// POST http://localhost:8080/profiles
103// {"first_name": "Ivan", "nick_name": "Socks", "last_name": "Hawkes"}
104//
105func (u *ProfileApi) insert(r *restful.Request, w *restful.Response) {
106	c := appengine.NewContext(r.Request)
107
108	// Marshall the entity from the request into a struct.
109	p := new(Profile)
110	err := r.ReadEntity(&p)
111	if err != nil {
112		w.WriteError(http.StatusNotAcceptable, err)
113		return
114	}
115
116	// Ensure we start with a sensible value for this field.
117	p.LastModified = time.Now()
118
119	// The profile belongs to this user.
120	p.Email = user.Current(c).String()
121
122	k, err := datastore.Put(c, datastore.NewIncompleteKey(c, "profiles", nil), p)
123	if err != nil {
124		http.Error(w, err.Error(), http.StatusInternalServerError)
125		return
126	}
127
128	// Let them know the location of the newly created resource.
129	// TODO: Use a safe Url path append function.
130	w.AddHeader("Location", u.Path+"/"+k.Encode())
131
132	// Return the resultant entity.
133	w.WriteHeader(http.StatusCreated)
134	w.WriteEntity(p)
135}
136
137// GET http://localhost:8080/profiles/ahdkZXZ-ZmVkZXJhdGlvbi1zZXJ2aWNlc3IVCxIIcHJvZmlsZXMYgICAgICAgAoM
138//
139func (u ProfileApi) read(r *restful.Request, w *restful.Response) {
140	c := appengine.NewContext(r.Request)
141
142	// Decode the request parameter to determine the key for the entity.
143	k, err := datastore.DecodeKey(r.PathParameter("profile-id"))
144	if err != nil {
145		http.Error(w, err.Error(), http.StatusBadRequest)
146		return
147	}
148
149	// Retrieve the entity from the datastore.
150	p := Profile{}
151	if err := datastore.Get(c, k, &p); err != nil {
152		if err.Error() == "datastore: no such entity" {
153			http.Error(w, err.Error(), http.StatusNotFound)
154		} else {
155			http.Error(w, err.Error(), http.StatusInternalServerError)
156		}
157		return
158	}
159
160	// Check we own the profile before allowing them to view it.
161	// Optionally, return a 404 instead to help prevent guessing ids.
162	// TODO: Allow admins access.
163	if p.Email != user.Current(c).String() {
164		http.Error(w, "You do not have access to this resource", http.StatusForbidden)
165		return
166	}
167
168	w.WriteEntity(p)
169}
170
171// PUT http://localhost:8080/profiles/ahdkZXZ-ZmVkZXJhdGlvbi1zZXJ2aWNlc3IVCxIIcHJvZmlsZXMYgICAgICAgAoM
172// {"first_name": "Ivan", "nick_name": "Socks", "last_name": "Hawkes"}
173//
174func (u *ProfileApi) update(r *restful.Request, w *restful.Response) {
175	c := appengine.NewContext(r.Request)
176
177	// Decode the request parameter to determine the key for the entity.
178	k, err := datastore.DecodeKey(r.PathParameter("profile-id"))
179	if err != nil {
180		http.Error(w, err.Error(), http.StatusBadRequest)
181		return
182	}
183
184	// Marshall the entity from the request into a struct.
185	p := new(Profile)
186	err = r.ReadEntity(&p)
187	if err != nil {
188		w.WriteError(http.StatusNotAcceptable, err)
189		return
190	}
191
192	// Retrieve the old entity from the datastore.
193	old := Profile{}
194	if err := datastore.Get(c, k, &old); err != nil {
195		if err.Error() == "datastore: no such entity" {
196			http.Error(w, err.Error(), http.StatusNotFound)
197		} else {
198			http.Error(w, err.Error(), http.StatusInternalServerError)
199		}
200		return
201	}
202
203	// Check we own the profile before allowing them to update it.
204	// Optionally, return a 404 instead to help prevent guessing ids.
205	// TODO: Allow admins access.
206	if old.Email != user.Current(c).String() {
207		http.Error(w, "You do not have access to this resource", http.StatusForbidden)
208		return
209	}
210
211	// Since the whole entity is re-written, we need to assign any invariant fields again
212	// e.g. the owner of the entity.
213	p.Email = user.Current(c).String()
214
215	// Keep track of the last modification date.
216	p.LastModified = time.Now()
217
218	// Attempt to overwrite the old entity.
219	_, err = datastore.Put(c, k, p)
220	if err != nil {
221		http.Error(w, err.Error(), http.StatusInternalServerError)
222		return
223	}
224
225	// Let them know it succeeded.
226	w.WriteHeader(http.StatusNoContent)
227}
228
229// DELETE http://localhost:8080/profiles/ahdkZXZ-ZmVkZXJhdGlvbi1zZXJ2aWNlc3IVCxIIcHJvZmlsZXMYgICAgICAgAoM
230//
231func (u *ProfileApi) remove(r *restful.Request, w *restful.Response) {
232	c := appengine.NewContext(r.Request)
233
234	// Decode the request parameter to determine the key for the entity.
235	k, err := datastore.DecodeKey(r.PathParameter("profile-id"))
236	if err != nil {
237		http.Error(w, err.Error(), http.StatusBadRequest)
238		return
239	}
240
241	// Retrieve the old entity from the datastore.
242	old := Profile{}
243	if err := datastore.Get(c, k, &old); err != nil {
244		if err.Error() == "datastore: no such entity" {
245			http.Error(w, err.Error(), http.StatusNotFound)
246		} else {
247			http.Error(w, err.Error(), http.StatusInternalServerError)
248		}
249		return
250	}
251
252	// Check we own the profile before allowing them to delete it.
253	// Optionally, return a 404 instead to help prevent guessing ids.
254	// TODO: Allow admins access.
255	if old.Email != user.Current(c).String() {
256		http.Error(w, "You do not have access to this resource", http.StatusForbidden)
257		return
258	}
259
260	// Delete the entity.
261	if err := datastore.Delete(c, k); err != nil {
262		http.Error(w, err.Error(), http.StatusInternalServerError)
263	}
264
265	// Success notification.
266	w.WriteHeader(http.StatusNoContent)
267}
268